From 1a97c2c447ee73f63e485211b9619328c31fdfa4 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Tue, 9 Jun 2026 12:07:18 +0100 Subject: [PATCH 01/28] Add fetchPowerRanking function for team power rankings --- lua/wikis/fortnite/Infobox/Team/Custom.lua | 29 +++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/lua/wikis/fortnite/Infobox/Team/Custom.lua b/lua/wikis/fortnite/Infobox/Team/Custom.lua index c6495cfd6bc..2dd86d35edf 100644 --- a/lua/wikis/fortnite/Infobox/Team/Custom.lua +++ b/lua/wikis/fortnite/Infobox/Team/Custom.lua @@ -40,8 +40,25 @@ local MAXIMUM_NUMBER_OF_PLAYERS_IN_PLACEMENTS = Info.config.defaultMaxPlayersPer local CustomInjector = Class.new(Injector) +---@param team string +---@return string? score +---@return string? rank +local function fetchPowerRanking(team) + local key = string.lower((team or ''):gsub('[%s_]', '')) + local data = mw.ext.LiquipediaDB.lpdb('datapoint', { + limit = 1, + order = 'date DESC', + conditions = '[[type::FTN_ORG_PR]] AND [[name::' .. key .. ']]', + query = 'information, extradata', + })[1] + if not data then + return + end + return (data.extradata or {}).score, data.information +end + ---@param frame Frame ----@return VNode +---@return Widget function CustomTeam.run(frame) local team = CustomTeam(frame) team:setWidgetInjector(CustomInjector(team)) @@ -52,8 +69,8 @@ function CustomTeam.run(frame) end ---@param id string ----@param widgets Renderable[] ----@return Renderable[] +---@param widgets Widget[] +---@return Widget[] function CustomInjector:parse(id, widgets) if id == 'earnings' then local playerEarnings = self.caller.totalPlayerEarnings @@ -61,6 +78,12 @@ function CustomInjector:parse(id, widgets) name = PLAYER_EARNINGS_ABBREVIATION, children = {playerEarnings ~= 0 and ('$' .. mw.getContentLanguage():formatNum(Math.round(playerEarnings))) or nil} }) + elseif id == 'custom' then + local prScore, prRank = fetchPowerRanking(self.caller.pagename) + table.insert(widgets, Cell{ + name = '[[Fortnite Power Rankings/Organizations|LPRating]]', + children = {prScore and prRank and (prScore .. ' (Rank #' .. prRank .. ')') or nil} + }) end return widgets From b9efab2385be7bab52f4087e3b572c385bb1440d Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Tue, 9 Jun 2026 12:11:04 +0100 Subject: [PATCH 02/28] Add power ranking fetching functionality to Custom.lua --- .../fortnite/Infobox/Person/Player/Custom.lua | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua b/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua index bf3bbce0207..71020656476 100644 --- a/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua +++ b/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua @@ -23,12 +23,34 @@ local Widgets = Lua.import('Module:Widget/All') local Cell = Widgets.Cell local CURRENT_YEAR = tonumber(os.date('%Y')) +local POWER_RANKINGS_DATA = 'Module:PowerRankings/Data' local CustomPlayer = Class.new(Player) local CustomInjector = Class.new(Injector) +---@param key string? +---@return string +local function normalizeKey(key) + return string.lower((key or ''):gsub('[%s_]', '')) +end + +---@param pagename string +---@param displayName string? +---@return integer? points +---@return integer? rank +local function fetchPowerRanking(pagename, displayName) + local data = mw.loadData(POWER_RANKINGS_DATA) + local targets = {normalizeKey(pagename), normalizeKey(displayName)} + for _, player in ipairs(data.players or {}) do + local entryKey = normalizeKey(player.link or player.name) + if entryKey == targets[1] or entryKey == targets[2] then + return tonumber(player.points), tonumber(player.rank) + end + end +end + ---@param frame Frame ----@return VNode +---@return Widget function CustomPlayer.run(frame) local player = CustomPlayer(frame) player:setWidgetInjector(CustomInjector(player)) @@ -39,8 +61,8 @@ function CustomPlayer.run(frame) end ---@param id string ----@param widgets Renderable[] ----@return Renderable[] +---@param widgets Widget[] +---@return Widget[] function CustomInjector:parse(id, widgets) local caller = self.caller local args = caller.args @@ -54,6 +76,8 @@ function CustomInjector:parse(id, widgets) currentYearEarnings = '$' .. mw.getContentLanguage():formatNum(currentYearEarnings) end + local prPoints, prRank = fetchPowerRanking(caller.pagename, caller.name) + return { Cell{name = 'Approx. Winnings ' .. CURRENT_YEAR, children = {currentYearEarnings}}, Cell{name = 'Years active', children = {yearsActive}}, @@ -64,6 +88,10 @@ function CustomInjector:parse(id, widgets) }, children = {args.creatorcode} }, + Cell{ + name = '[[Fortnite Power Rankings|Fortnite PR]]', + children = {prPoints and prRank and (prPoints .. ' (Rank #' .. prRank .. ')') or nil} + }, } elseif id == 'region' then return {} end From c5cf87d8a31e8a6529b23f64dfc84b9537284345 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Tue, 9 Jun 2026 13:29:59 +0100 Subject: [PATCH 03/28] Update lua/wikis/fortnite/Infobox/Team/Custom.lua Co-authored-by: hjpalpha <75081997+hjpalpha@users.noreply.github.com> --- lua/wikis/fortnite/Infobox/Team/Custom.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/wikis/fortnite/Infobox/Team/Custom.lua b/lua/wikis/fortnite/Infobox/Team/Custom.lua index 2dd86d35edf..631ad479f8b 100644 --- a/lua/wikis/fortnite/Infobox/Team/Custom.lua +++ b/lua/wikis/fortnite/Infobox/Team/Custom.lua @@ -82,7 +82,7 @@ function CustomInjector:parse(id, widgets) local prScore, prRank = fetchPowerRanking(self.caller.pagename) table.insert(widgets, Cell{ name = '[[Fortnite Power Rankings/Organizations|LPRating]]', - children = {prScore and prRank and (prScore .. ' (Rank #' .. prRank .. ')') or nil} + children = prScore and prRank and (prScore .. ' (Rank #' .. prRank .. ')') or nil }) end From cfad6814514abd90317d79a9fb4bd06a0a73a3fc Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Tue, 9 Jun 2026 13:32:30 +0100 Subject: [PATCH 04/28] Update lua/wikis/fortnite/Infobox/Person/Player/Custom.lua Co-authored-by: hjpalpha <75081997+hjpalpha@users.noreply.github.com> --- lua/wikis/fortnite/Infobox/Person/Player/Custom.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua b/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua index 71020656476..d5e149343c1 100644 --- a/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua +++ b/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua @@ -23,7 +23,7 @@ local Widgets = Lua.import('Module:Widget/All') local Cell = Widgets.Cell local CURRENT_YEAR = tonumber(os.date('%Y')) -local POWER_RANKINGS_DATA = 'Module:PowerRankings/Data' +local POWER_RANKINGS_DATA = Lua.import('Module:PowerRankings/Data', {loadData = true}) local CustomPlayer = Class.new(Player) local CustomInjector = Class.new(Injector) From 7531cd2e5748c0c6d0ea93b6cc0a81a99a5bc9f7 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Tue, 9 Jun 2026 13:45:43 +0100 Subject: [PATCH 05/28] Update lua/wikis/fortnite/Infobox/Person/Player/Custom.lua Co-authored-by: hjpalpha <75081997+hjpalpha@users.noreply.github.com> --- .../fortnite/Infobox/Person/Player/Custom.lua | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua b/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua index d5e149343c1..c9eac6cfd16 100644 --- a/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua +++ b/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua @@ -34,19 +34,19 @@ local function normalizeKey(key) return string.lower((key or ''):gsub('[%s_]', '')) end ----@param pagename string +---@param pageName string ---@param displayName string? ---@return integer? points ---@return integer? rank -local function fetchPowerRanking(pagename, displayName) - local data = mw.loadData(POWER_RANKINGS_DATA) - local targets = {normalizeKey(pagename), normalizeKey(displayName)} - for _, player in ipairs(data.players or {}) do +local function fetchPowerRanking(pageName, displayName) + local pageKey = normalizeKey(pageName) + local nameKey = normalizeKey(displayName) + local entry = Array.find(POWER_RANKINGS_DATA.players or {}, function(player) local entryKey = normalizeKey(player.link or player.name) - if entryKey == targets[1] or entryKey == targets[2] then - return tonumber(player.points), tonumber(player.rank) - end - end + return entryKey == pageKey or entryKey == nameKey + end) + + return tonumber(entry.points), tonumber(entry.rank) end ---@param frame Frame From 80faf8e13412eb35ce1794d05754e81517e24ae2 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Tue, 9 Jun 2026 13:45:59 +0100 Subject: [PATCH 06/28] Update lua/wikis/fortnite/Infobox/Person/Player/Custom.lua Co-authored-by: hjpalpha <75081997+hjpalpha@users.noreply.github.com> --- lua/wikis/fortnite/Infobox/Person/Player/Custom.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua b/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua index c9eac6cfd16..ad8069ccf0a 100644 --- a/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua +++ b/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua @@ -90,7 +90,7 @@ function CustomInjector:parse(id, widgets) }, Cell{ name = '[[Fortnite Power Rankings|Fortnite PR]]', - children = {prPoints and prRank and (prPoints .. ' (Rank #' .. prRank .. ')') or nil} + children = prPoints and prRank and (prPoints .. ' (Rank #' .. prRank .. ')') or nil }, } elseif id == 'region' then return {} From 612d1f1ef805683cd1855f4bb68514869802b248 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Tue, 9 Jun 2026 14:08:14 +0100 Subject: [PATCH 07/28] Refactor fetchPowerRanking function and imports --- .../fortnite/Infobox/Person/Player/Custom.lua | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua b/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua index ad8069ccf0a..a3d4e15d0ff 100644 --- a/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua +++ b/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua @@ -9,6 +9,7 @@ local Lua = require('Module:Lua') local Abbreviation = Lua.import('Module:Abbreviation') local ActiveYears = Lua.import('Module:YearsActive') +local Array = Lua.import('Module:Array') local Class = Lua.import('Module:Class') local Region = Lua.import('Module:Region') local Math = Lua.import('Module:MathUtil') @@ -21,6 +22,7 @@ local PlayerAchievements = Lua.import('Module:Infobox/Extension/Achievements') local Widgets = Lua.import('Module:Widget/All') local Cell = Widgets.Cell +local Link = Lua.import('Module:Widget/Basic/Link') local CURRENT_YEAR = tonumber(os.date('%Y')) local POWER_RANKINGS_DATA = Lua.import('Module:PowerRankings/Data', {loadData = true}) @@ -28,29 +30,22 @@ local POWER_RANKINGS_DATA = Lua.import('Module:PowerRankings/Data', {loadData = local CustomPlayer = Class.new(Player) local CustomInjector = Class.new(Injector) ----@param key string? ----@return string -local function normalizeKey(key) - return string.lower((key or ''):gsub('[%s_]', '')) -end - ---@param pageName string ----@param displayName string? ---@return integer? points ---@return integer? rank -local function fetchPowerRanking(pageName, displayName) - local pageKey = normalizeKey(pageName) - local nameKey = normalizeKey(displayName) +local function fetchPowerRanking(pageName) local entry = Array.find(POWER_RANKINGS_DATA.players or {}, function(player) - local entryKey = normalizeKey(player.link or player.name) - return entryKey == pageKey or entryKey == nameKey + return ((player.link or player.name):gsub(' ', '_')) == (pageName:gsub(' ', '_')) end) + if not entry then + return + end - return tonumber(entry.points), tonumber(entry.rank) + return entry.points, entry.rank end ---@param frame Frame ----@return Widget +---@return VNode function CustomPlayer.run(frame) local player = CustomPlayer(frame) player:setWidgetInjector(CustomInjector(player)) @@ -61,8 +56,8 @@ function CustomPlayer.run(frame) end ---@param id string ----@param widgets Widget[] ----@return Widget[] +---@param widgets Renderable[] +---@return Renderable[] function CustomInjector:parse(id, widgets) local caller = self.caller local args = caller.args @@ -76,7 +71,7 @@ function CustomInjector:parse(id, widgets) currentYearEarnings = '$' .. mw.getContentLanguage():formatNum(currentYearEarnings) end - local prPoints, prRank = fetchPowerRanking(caller.pagename, caller.name) + local prPoints, prRank = fetchPowerRanking(caller.pagename) return { Cell{name = 'Approx. Winnings ' .. CURRENT_YEAR, children = {currentYearEarnings}}, @@ -89,7 +84,7 @@ function CustomInjector:parse(id, widgets) children = {args.creatorcode} }, Cell{ - name = '[[Fortnite Power Rankings|Fortnite PR]]', + name = Link{link = 'Fortnite Power Rankings', children = 'Fortnite PR'}, children = prPoints and prRank and (prPoints .. ' (Rank #' .. prRank .. ')') or nil }, } From b11e795b370196505686036c86b5263c04c39728 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Tue, 9 Jun 2026 14:08:51 +0100 Subject: [PATCH 08/28] Refactor power ranking link in Custom.lua --- lua/wikis/fortnite/Infobox/Team/Custom.lua | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lua/wikis/fortnite/Infobox/Team/Custom.lua b/lua/wikis/fortnite/Infobox/Team/Custom.lua index 631ad479f8b..53570ff8dc6 100644 --- a/lua/wikis/fortnite/Infobox/Team/Custom.lua +++ b/lua/wikis/fortnite/Infobox/Team/Custom.lua @@ -22,6 +22,7 @@ local Opponent = Lua.import('Module:Opponent/Custom') local Widgets = Lua.import('Module:Widget/All') local Cell = Widgets.Cell +local Link = Lua.import('Module:Widget/Basic/Link') local Condition = Lua.import('Module:Condition') local ConditionTree = Condition.Tree @@ -45,10 +46,14 @@ local CustomInjector = Class.new(Injector) ---@return string? rank local function fetchPowerRanking(team) local key = string.lower((team or ''):gsub('[%s_]', '')) + local conditions = ConditionTree(BooleanOperator.all):add{ + ConditionNode(ColumnName('type'), Comparator.eq, 'FTN_ORG_PR'), + ConditionNode(ColumnName('name'), Comparator.eq, key), + } local data = mw.ext.LiquipediaDB.lpdb('datapoint', { limit = 1, order = 'date DESC', - conditions = '[[type::FTN_ORG_PR]] AND [[name::' .. key .. ']]', + conditions = conditions:toString(), query = 'information, extradata', })[1] if not data then @@ -58,7 +63,7 @@ local function fetchPowerRanking(team) end ---@param frame Frame ----@return Widget +---@return VNode function CustomTeam.run(frame) local team = CustomTeam(frame) team:setWidgetInjector(CustomInjector(team)) @@ -69,8 +74,8 @@ function CustomTeam.run(frame) end ---@param id string ----@param widgets Widget[] ----@return Widget[] +---@param widgets Renderable[] +---@return Renderable[] function CustomInjector:parse(id, widgets) if id == 'earnings' then local playerEarnings = self.caller.totalPlayerEarnings @@ -81,7 +86,7 @@ function CustomInjector:parse(id, widgets) elseif id == 'custom' then local prScore, prRank = fetchPowerRanking(self.caller.pagename) table.insert(widgets, Cell{ - name = '[[Fortnite Power Rankings/Organizations|LPRating]]', + name = Link{link = 'Fortnite Power Rankings/Organizations', children = 'Fortnite PR'}, children = prScore and prRank and (prScore .. ' (Rank #' .. prRank .. ')') or nil }) end From 3ccf9bfc38e509508c274eccf8c515e5a1d7970e Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Tue, 9 Jun 2026 14:54:26 +0100 Subject: [PATCH 09/28] Add PowerRankings module for Fortnite --- lua/wikis/fortnite/PowerRankings.lua | 171 +++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 lua/wikis/fortnite/PowerRankings.lua diff --git a/lua/wikis/fortnite/PowerRankings.lua b/lua/wikis/fortnite/PowerRankings.lua new file mode 100644 index 00000000000..7bb8056961c --- /dev/null +++ b/lua/wikis/fortnite/PowerRankings.lua @@ -0,0 +1,171 @@ +--- +-- @Liquipedia +-- page=Module:PowerRankings +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Lua = require('Module:Lua') + +local Arguments = Lua.import('Module:Arguments') +local DateExt = Lua.import('Module:Date/Ext') +local Flags = Lua.import('Module:Flags') +local Icon = Lua.import('Module:Icon') +local Logic = Lua.import('Module:Logic') +local Page = Lua.import('Module:Page') +local PlayerExt = Lua.import('Module:Player/Ext') +local String = Lua.import('Module:StringUtils') + +local TableWidgets = Lua.import('Module:Widget/Table2/All') +local HtmlWidgets = Lua.import('Module:Widget/Html/All') +local Link = Lua.import('Module:Widget/Basic/Link') + +local Condition = Lua.import('Module:Condition') +local ConditionTree = Condition.Tree +local ConditionNode = Condition.Node +local Comparator = Condition.Comparator +local BooleanOperator = Condition.BooleanOperator +local ColumnName = Condition.ColumnName + +local PowerRankingsData = Lua.import('Module:PowerRankings/Data', {loadData = true}) +local DISPLAY_PAGE = 'Fortnite Power Rankings' + +local CONTAINER_STYLE = 'display: inline-flex; align-items: center; white-space: nowrap; ' + .. 'line-height: 1; font-size: 1em; vertical-align: middle;' +local FLAG_SPACING = '5px' + +local p = {} + +local function renderPlayer(name, link) + if String.isEmpty(name) then + return '' + end + local date = DateExt.toYmdInUtc(DateExt.getContextualDateOrNow()) + local pageNameFromLink, displayNameFromLink = PlayerExt.extractFromLink(name) + local player = { + displayName = displayNameFromLink or name, + pageName = String.nilIfEmpty(link) or pageNameFromLink, + } + PlayerExt.syncPlayer(player, {date = date}) + + local items = {} + if String.isNotEmpty(player.flag) then + local flagIcon = String.nilIfEmpty(Flags.Icon{flag = player.flag, shouldLink = false}) + if flagIcon then + items[#items + 1] = string.format( + '%s', + FLAG_SPACING, flagIcon) + end + end + items[#items + 1] = string.format('[[%s|%s]]', player.pageName, player.displayName) + + return string.format('%s', CONTAINER_STYLE, table.concat(items)) +end + +local function queryPlayerOrg(name) + if Logic.isEmpty(name) then + return '' + end + local conditions = ConditionTree(BooleanOperator.any):add{ + ConditionNode(ColumnName('pagename'), Comparator.eq, Page.pageifyLink(name)), + ConditionNode(ColumnName('id'), Comparator.eq, name), + } + local row = mw.ext.LiquipediaDB.lpdb('player', { + query = 'team', + conditions = conditions:toString(), + limit = 1, + })[1] or {} + return row.team or '' +end + +local function renderOrg(name, frame) + local org = queryPlayerOrg(name) + if Logic.isEmpty(org) then + return '' + end + return frame:expandTemplate{title = 'Team', args = {org}} +end + +local function buildTitle(updated) + local textChildren = {HtmlWidgets.B{children = 'Fortnite Power Rankings'}} + if Logic.isNotEmpty(updated) then + textChildren[#textChildren + 1] = HtmlWidgets.Span{children = 'Last updated: ' .. updated} + end + return HtmlWidgets.Div{ + children = { + HtmlWidgets.Div{children = textChildren, classes = {'ranking-table__top-row-text'}}, + HtmlWidgets.Div{ + children = {HtmlWidgets.Span{children = 'Data by Epic Games'}}, + classes = {'ranking-table__top-row-logo-container'}, + }, + }, + classes = {'ranking-table__top-row'}, + } +end + +local function buildFooter() + return Link{ + link = DISPLAY_PAGE, + linktype = 'internal', + children = { + HtmlWidgets.Div{ + children = {'See Rankings Page', Icon.makeIcon{iconName = 'goto'}}, + classes = {'ranking-table__footer-button'}, + }, + }, + } +end + +function p.main(frame) + local args = Arguments.getArgs(frame) + local limit = tonumber(args.limit) + local showMore = Logic.readBool(args.showMore) + + local players = PowerRankingsData.players or {} + + local updated = '' + if Logic.isNotEmpty(PowerRankingsData.updated) then + updated = PowerRankingsData.updated .. ' ' .. frame:expandTemplate{title = 'Abbr/UTC'} + end + + local rows = {} + for i, player in ipairs(players) do + if limit and i > limit then break end + local name = player.name or '' + local link = Logic.nilIfEmpty(player.link) + local orgKey = link or name + + rows[#rows + 1] = TableWidgets.Row{children = { + TableWidgets.Cell{children = HtmlWidgets.B{children = player.rank}}, + TableWidgets.Cell{children = HtmlWidgets.B{children = player.points}}, + TableWidgets.Cell{children = renderPlayer(name, link)}, + TableWidgets.Cell{children = renderOrg(orgKey, frame)}, + }} + end + + return tostring(TableWidgets.Table{ + title = buildTitle(updated), + sortable = false, + columns = { + {align = 'center', sortType = 'number'}, + {align = 'center', sortType = 'number'}, + {align = 'left'}, + {align = 'left'}, + }, + footer = showMore and buildFooter() or nil, + css = {width = '100%'}, + children = { + TableWidgets.TableHeader{children = { + TableWidgets.Row{children = { + TableWidgets.CellHeader{children = 'Rank'}, + TableWidgets.CellHeader{children = 'Points'}, + TableWidgets.CellHeader{children = 'Player'}, + TableWidgets.CellHeader{children = 'Organization'}, + }}, + }}, + TableWidgets.TableBody{children = rows}, + }, + }) +end + +return p From 4be4212a04e6bacb87a9760d7a578ea450f4f156 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Tue, 9 Jun 2026 14:57:07 +0100 Subject: [PATCH 10/28] Add Lua module for Fortnite organization power rankings --- lua/wikis/fortnite/PowerRankings/Orgs.lua | 452 ++++++++++++++++++++++ 1 file changed, 452 insertions(+) create mode 100644 lua/wikis/fortnite/PowerRankings/Orgs.lua diff --git a/lua/wikis/fortnite/PowerRankings/Orgs.lua b/lua/wikis/fortnite/PowerRankings/Orgs.lua new file mode 100644 index 00000000000..df495f82953 --- /dev/null +++ b/lua/wikis/fortnite/PowerRankings/Orgs.lua @@ -0,0 +1,452 @@ +--- +-- @Liquipedia +-- page=Module:PowerRankings/Orgs +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Lua = require('Module:Lua') + +local Arguments = Lua.import('Module:Arguments') +local DateExt = Lua.import('Module:Date/Ext') +local Flags = Lua.import('Module:Flags') +local Icon = Lua.import('Module:Icon') +local Logic = Lua.import('Module:Logic') +local Lpdb = Lua.import('Module:Lpdb') +local Page = Lua.import('Module:Page') +local PlayerExt = Lua.import('Module:Player/Ext') +local String = Lua.import('Module:StringUtils') +local Team = Lua.import('Module:Team') + +local HtmlWidgets = Lua.import('Module:Widget/Html/All') +local Link = Lua.import('Module:Widget/Basic/Link') + +local Condition = Lua.import('Module:Condition') +local ConditionTree = Condition.Tree +local ConditionNode = Condition.Node +local Comparator = Condition.Comparator +local BooleanOperator = Condition.BooleanOperator +local ColumnName = Condition.ColumnName + +local PowerRankingsData = Lua.import('Module:PowerRankings/Data', {loadData = true}) +local DISPLAY_PAGE = 'Fortnite Power Rankings/Organizations' +local TOP_N = 200 +local MAX_PLAYERS_PER_ORG = 4 +local DEFAULT_WEIGHTS = {count = 0.12, pr = 0.35, cash = 0.45} + +local CONTAINER_STYLE = 'display: inline-flex; align-items: center; white-space: nowrap; ' + .. 'line-height: 1; font-size: 1em; vertical-align: middle;' +local FLAG_SPACING = '5px' + +local p = {} + +local function renderPlayer(name, link) + if String.isEmpty(name) then + return '' + end + local date = DateExt.toYmdInUtc(DateExt.getContextualDateOrNow()) + local pageNameFromLink, displayNameFromLink = PlayerExt.extractFromLink(name) + local player = { + displayName = displayNameFromLink or name, + pageName = String.nilIfEmpty(link) or pageNameFromLink, + } + PlayerExt.syncPlayer(player, {date = date}) + + local items = {} + if String.isNotEmpty(player.flag) then + local flagIcon = String.nilIfEmpty(Flags.Icon{flag = player.flag, shouldLink = false}) + if flagIcon then + items[#items + 1] = string.format( + '%s', + FLAG_SPACING, flagIcon) + end + end + items[#items + 1] = string.format('[[%s|%s]]', player.pageName, player.displayName) + + return string.format('%s', CONTAINER_STYLE, table.concat(items)) +end + +local function queryPlayerOrg(name) + if Logic.isEmpty(name) then + return '' + end + local conditions = ConditionTree(BooleanOperator.any):add{ + ConditionNode(ColumnName('pagename'), Comparator.eq, Page.pageifyLink(name)), + ConditionNode(ColumnName('id'), Comparator.eq, name), + } + local row = mw.ext.LiquipediaDB.lpdb('player', { + query = 'team', + conditions = conditions:toString(), + limit = 1, + })[1] or {} + return row.team or '' +end + +local function wrap(tbl) + return tostring(mw.html.create('div') + :addClass('table-responsive') + :css('overflow-x', 'auto') + :css('width', '100%') + :node(tbl)) +end + +local function themedText(content) + return '' .. content .. '' + .. '' .. content .. '' +end + +local function formatNumber(n) + local s = tostring(math.floor((tonumber(n) or 0) + 0.5)) + return (s:reverse():gsub('(%d%d%d)', '%1,'):reverse():gsub('^,', '')) +end + +local function resolvePlayerTeams(players) + local result = {} + for i, pl in ipairs(players) do + result[i] = Logic.nilIfEmpty(queryPlayerOrg(pl.link or pl.name)) + end + return result +end + +local function resolveOrgFlags(list) + local pageByTeam = {} + local pages = {} + for _, o in ipairs(list) do + local ok, raw = pcall(mw.ext.TeamTemplate.raw, o.team) + if ok and type(raw) == 'table' and Logic.isNotEmpty(raw.page) then + local page = raw.page:gsub(' ', '_') + pageByTeam[o.team] = page + table.insert(pages, page) + end + end + + local locByPage = {} + local CHUNK = 50 + for start = 1, #pages, CHUNK do + local conditions = ConditionTree(BooleanOperator.any) + for k = start, math.min(start + CHUNK - 1, #pages) do + conditions:add{ConditionNode(ColumnName('pagename'), Comparator.eq, pages[k])} + end + local rows = mw.ext.LiquipediaDB.lpdb('team', { + conditions = conditions:toString(), + query = 'pagename, location', + limit = 1000, + }) or {} + for _, r in ipairs(rows) do + if Logic.isNotEmpty(r.pagename) then + locByPage[r.pagename:gsub(' ', '_')] = r.location + end + end + end + + for _, o in ipairs(list) do + local page = pageByTeam[o.team] + o.flag = page and Logic.nilIfEmpty(locByPage[page]) or nil + end +end + +local function normalizeName(name) + return string.lower((name or ''):gsub('[%s_]', '')) +end + +local function orgPageKey(team) + local ok, raw = pcall(mw.ext.TeamTemplate.raw, team) + local page = (ok and type(raw) == 'table' and Logic.isNotEmpty(raw.page)) and raw.page or team + return normalizeName(page) +end + +local function storeOrgRankings(list) + for rank, o in ipairs(list) do + local key = orgPageKey(o.team) + mw.ext.LiquipediaDB.lpdb_datapoint('FTN_ORG_PR_' .. key, { + type = 'FTN_ORG_PR', + name = key, + information = rank, + extradata = {score = string.format('%.1f', o.score)}, + }) + end +end + +local function tournamentIcon(icon, icondark, page, size) + if Logic.isEmpty(icon) then return '' end + icondark = Logic.nilIfEmpty(icondark) or icon + return '[[File:' .. icon .. '|' .. size .. '|link=' .. page .. ']]' + .. '[[File:' .. icondark .. '|' .. size .. '|link=' .. page .. ']]' +end + +local function gatherPlacementData(year) + local earnings = {} + local winPages = {} + local pageSet = {} + + local function process(item) + local indiv = tonumber(item.individualprizemoney) or 0 + local page = item.pagename + local isSTierWin = item.placement == '1' + and (item.liquipediatier == '1' or item.liquipediatier == 'S-Tier') + and item.liquipediatiertype ~= 'Qualifier' + and item.liquipediatiertype ~= 'Showmatch' + local opPlayers = item.opponentplayers or {} + local opType = item.opponenttype + local opName = item.opponentname + + for i = 1, 10 do + local pName = opPlayers['p' .. i] + if Logic.isNotEmpty(pName) then + local teamRaw = opPlayers['p' .. i .. 'team'] + if Logic.isEmpty(teamRaw) and opType == 'team' then + teamRaw = opName + end + if Logic.isNotEmpty(teamRaw) then + local norm = normalizeName(teamRaw) + earnings[norm] = (earnings[norm] or 0) + indiv + if isSTierWin and Logic.isNotEmpty(page) then + winPages[norm] = winPages[norm] or {} + if not winPages[norm][page] then + winPages[norm][page] = true + pageSet[page] = true + end + end + end + end + end + end + + local placementConditions = ConditionTree(BooleanOperator.all):add{ + ConditionNode(ColumnName('date_year'), Comparator.eq, tostring(year)), + ConditionNode(ColumnName('prizemoney'), Comparator.gt, '0'), + } + + Lpdb.executeMassQuery('placement', { + conditions = placementConditions:toString(), + query = 'pagename, opponentname, opponenttype, opponentplayers, ' + .. 'individualprizemoney, placement, liquipediatier, liquipediatiertype', + limit = 5000, + }, process) + + local pages = {} + for page in pairs(pageSet) do table.insert(pages, page) end + + local details = {} + local CHUNK = 40 + for start = 1, #pages, CHUNK do + local conditions = ConditionTree(BooleanOperator.any) + for k = start, math.min(start + CHUNK - 1, #pages) do + conditions:add{ConditionNode(ColumnName('pagename'), Comparator.eq, pages[k])} + end + local rows = mw.ext.LiquipediaDB.lpdb('tournament', { + conditions = conditions:toString(), + query = 'pagename, name, icon, icondark', + limit = 100, + }) or {} + for _, t in ipairs(rows) do + details[t.pagename] = { + name = Logic.nilIfEmpty(t.name) or t.pagename:gsub('_', ' '), + icon = t.icon, + icondark = t.icondark, + } + end + end + + return earnings, winPages, details +end + +function p.main(frame) + local args = Arguments.getArgs(frame) + local limit = tonumber(args.limit) + local showMore = Logic.readBool(args.showMore) + local wrapped = Logic.readBool(args.wrapped) + local colspan = wrapped and 5 or 8 + local year = tonumber(args.year) or tonumber(os.date('!%Y')) + local weights = { + count = tonumber(args.wCount) or DEFAULT_WEIGHTS.count, + pr = tonumber(args.wPR) or DEFAULT_WEIGHTS.pr, + cash = tonumber(args.wCash) or DEFAULT_WEIGHTS.cash, + } + local weightSum = weights.count + weights.pr + weights.cash + + local players = {} + for _, pl in ipairs(PowerRankingsData.players or {}) do + if (tonumber(pl.rank) or 0) <= TOP_N then + table.insert(players, pl) + end + end + local teams = resolvePlayerTeams(players) + + local byOrg = {} + for i, pl in ipairs(players) do + local team = teams[i] + if team then + byOrg[team] = byOrg[team] or {} + table.insert(byOrg[team], { + name = pl.name, + link = pl.link, + points = tonumber(pl.points) or 0, + }) + end + end + + local list = {} + for team, members in pairs(byOrg) do + table.sort(members, function(a, b) return a.points > b.points end) + local count = math.min(#members, MAX_PLAYERS_PER_ORG) + local sum = 0 + for k = 1, count do sum = sum + members[k].points end + table.insert(list, { + team = team, + members = members, + count = count, + avgPR = sum / count, + }) + end + + local teamEarnings, teamWinPages, tournamentDetails = gatherPlacementData(year) + for _, o in ipairs(list) do + o.histNames = Team.queryHistoricalNames(o.team) or {o.team} + local cash = 0 + for _, nm in ipairs(o.histNames) do + cash = cash + (teamEarnings[normalizeName(nm)] or 0) + end + o.cash = cash + end + + local n = #list + local function rankNormalize(getValue, field) + if n <= 1 then + for _, o in ipairs(list) do o[field] = 1 end + return + end + local sorted = {} + for _, o in ipairs(list) do table.insert(sorted, o) end + table.sort(sorted, function(a, b) return getValue(a) < getValue(b) end) + local i = 1 + while i <= n do + local j = i + while j < n and getValue(sorted[j + 1]) == getValue(sorted[i]) do j = j + 1 end + local norm = ((i + j) / 2 - 1) / (n - 1) + for k = i, j do sorted[k][field] = norm end + i = j + 1 + end + end + + rankNormalize(function(o) return o.count end, 'nCount') + rankNormalize(function(o) return o.avgPR end, 'nPR') + rankNormalize(function(o) return o.cash end, 'nCash') + + for _, o in ipairs(list) do + o.score = 100 * (weights.count * o.nCount + weights.pr * o.nPR + weights.cash * o.nCash) / weightSum + end + + table.sort(list, function(a, b) + if a.score ~= b.score then return a.score > b.score end + return a.avgPR > b.avgPR + end) + + if not wrapped then + storeOrgRankings(list) + end + + local display = {} + for i, o in ipairs(list) do + if limit and i > limit then break end + table.insert(display, o) + end + + if not wrapped then + for _, o in ipairs(display) do + local achievements = {} + local seen = {} + for _, nm in ipairs(o.histNames) do + local pagesForTeam = teamWinPages[normalizeName(nm)] + if pagesForTeam then + for page in pairs(pagesForTeam) do + if not seen[page] then + seen[page] = true + local d = tournamentDetails[page] or {name = page:gsub('_', ' '), icon = '', icondark = ''} + table.insert(achievements, {page = page, name = d.name, icon = d.icon, icondark = d.icondark}) + end + end + end + end + o.achievements = achievements + end + end + + resolveOrgFlags(display) + + local tbl = mw.html.create('table') + :addClass('table2__table wikitable wikitable-striped wikitable-bordered') + :css('width', '100%') + :css('border-collapse', 'collapse') + + local title = "'''Fortnite Organization Power Rankings'''" + if Logic.isNotEmpty(PowerRankingsData.updated) then + local utc = frame:expandTemplate{title = 'Abbr/UTC'} + title = title .. "
''Last Updated: " .. PowerRankingsData.updated .. ' ' .. utc .. "''" + end + tbl:tag('tr'):tag('th') + :attr('colspan', colspan) + :wikitext(themedText(title)) + + local header = tbl:tag('tr') + header:tag('th'):css('width', '1%'):css('white-space', 'nowrap'):wikitext('Rank') + header:tag('th'):css('width', '1%'):css('padding', '0 8px'):wikitext('') + header:tag('th'):css('text-align', 'left'):wikitext('Organization') + header:tag('th'):css('text-align', 'left'):wikitext('Four Best Players (In Top 200)') + if not wrapped then + header:tag('th'):css('text-align', 'center'):wikitext('Recent Achievements') + end + header:tag('th'):css('width', '1%'):css('white-space', 'nowrap'):wikitext('Score') + if not wrapped then + header:tag('th'):css('width', '1%'):css('white-space', 'nowrap'):wikitext('Average Players PR') + header:tag('th'):css('width', '1%'):css('white-space', 'nowrap'):wikitext('Earnings (' .. year .. ')') + end + + for i, o in ipairs(display) do + local row = tbl:tag('tr') + row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap'):wikitext('' .. i .. '') + local flagCell = Logic.isNotEmpty(o.flag) and Flags.Icon{flag = o.flag, shouldLink = false} or '' + row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap'):css('padding', '0 8px'):wikitext(flagCell) + row:tag('td'):css('text-align', 'left'):css('white-space', 'nowrap'):wikitext(frame:expandTemplate{title = 'Team', args = {o.team}}) + local names = {} + for k = 1, o.count do + local m = o.members[k] + local rendered = renderPlayer(m.name, m.link) + table.insert(names, '' .. rendered .. '') + end + row:tag('td'):css('text-align', 'left') + :wikitext(table.concat(names, ', ') .. ' (' .. o.count .. ')') + if not wrapped then + local achText = '' + for _, a in ipairs(o.achievements or {}) do + achText = achText .. tournamentIcon(a.icon, a.icondark, a.page, '30x30px') + end + row:tag('td'):css('text-align', 'center'):wikitext(achText) + end + row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap'):wikitext('' .. string.format('%.1f', o.score) .. '') + if not wrapped then + row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap'):wikitext(formatNumber(o.avgPR)) + row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap'):wikitext('$' .. formatNumber(o.cash)) + end + end + + if showMore then + local footer = Link{ + link = DISPLAY_PAGE, + linktype = 'internal', + children = { + HtmlWidgets.Div{ + children = {'See Rankings Page', Icon.makeIcon{iconName = 'goto'}}, + classes = {'ranking-table__footer-button'}, + }, + }, + } + tbl:tag('tr'):tag('td') + :attr('colspan', colspan) + :wikitext(tostring(footer)) + end + + return wrap(tbl) +end + +return p From ad6d5fd921ae93aa74c6996869fedded16567720 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Tue, 9 Jun 2026 15:02:43 +0100 Subject: [PATCH 11/28] Update PowerRankings.lua --- lua/wikis/fortnite/PowerRankings.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/wikis/fortnite/PowerRankings.lua b/lua/wikis/fortnite/PowerRankings.lua index 7bb8056961c..b07022343df 100644 --- a/lua/wikis/fortnite/PowerRankings.lua +++ b/lua/wikis/fortnite/PowerRankings.lua @@ -57,7 +57,8 @@ local function renderPlayer(name, link) FLAG_SPACING, flagIcon) end end - items[#items + 1] = string.format('[[%s|%s]]', player.pageName, player.displayName) + local nameLink = Link{link = player.pageName, linktype = 'internal', children = player.displayName} + items[#items + 1] = string.format('%s', tostring(nameLink)) return string.format('%s', CONTAINER_STYLE, table.concat(items)) end From b87d42f78bc82748df1321d006720cf162691e65 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Tue, 9 Jun 2026 15:05:48 +0100 Subject: [PATCH 12/28] Update Orgs.lua --- lua/wikis/fortnite/PowerRankings/Orgs.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/wikis/fortnite/PowerRankings/Orgs.lua b/lua/wikis/fortnite/PowerRankings/Orgs.lua index df495f82953..d302c122d90 100644 --- a/lua/wikis/fortnite/PowerRankings/Orgs.lua +++ b/lua/wikis/fortnite/PowerRankings/Orgs.lua @@ -61,7 +61,8 @@ local function renderPlayer(name, link) FLAG_SPACING, flagIcon) end end - items[#items + 1] = string.format('[[%s|%s]]', player.pageName, player.displayName) + local nameLink = Link{link = player.pageName, linktype = 'internal', children = player.displayName} + items[#items + 1] = string.format('%s', tostring(nameLink)) return string.format('%s', CONTAINER_STYLE, table.concat(items)) end From 05eb714d26a01da2d2fb15221a8cf08ad720b458 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Tue, 9 Jun 2026 15:14:23 +0100 Subject: [PATCH 13/28] Update Orgs.lua --- lua/wikis/fortnite/PowerRankings/Orgs.lua | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lua/wikis/fortnite/PowerRankings/Orgs.lua b/lua/wikis/fortnite/PowerRankings/Orgs.lua index d302c122d90..b729120f93d 100644 --- a/lua/wikis/fortnite/PowerRankings/Orgs.lua +++ b/lua/wikis/fortnite/PowerRankings/Orgs.lua @@ -171,8 +171,12 @@ end local function tournamentIcon(icon, icondark, page, size) if Logic.isEmpty(icon) then return '' end icondark = Logic.nilIfEmpty(icondark) or icon - return '[[File:' .. icon .. '|' .. size .. '|link=' .. page .. ']]' - .. '[[File:' .. icondark .. '|' .. size .. '|link=' .. page .. ']]' + local function modeSpan(mode, image) + return string.format( + '[[File:%s|%s|link=%s]]', + mode, image, size, page) + end + return modeSpan('lightmode', icon) .. modeSpan('darkmode', icondark) end local function gatherPlacementData(year) @@ -408,7 +412,8 @@ function p.main(frame) row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap'):wikitext('' .. i .. '') local flagCell = Logic.isNotEmpty(o.flag) and Flags.Icon{flag = o.flag, shouldLink = false} or '' row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap'):css('padding', '0 8px'):wikitext(flagCell) - row:tag('td'):css('text-align', 'left'):css('white-space', 'nowrap'):wikitext(frame:expandTemplate{title = 'Team', args = {o.team}}) + row:tag('td'):css('text-align', 'left'):css('white-space', 'nowrap') + :wikitext(frame:expandTemplate{title = 'Team', args = {o.team}}) local names = {} for k = 1, o.count do local m = o.members[k] @@ -424,7 +429,8 @@ function p.main(frame) end row:tag('td'):css('text-align', 'center'):wikitext(achText) end - row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap'):wikitext('' .. string.format('%.1f', o.score) .. '') + row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap') + :wikitext('' .. string.format('%.1f', o.score) .. '') if not wrapped then row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap'):wikitext(formatNumber(o.avgPR)) row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap'):wikitext('$' .. formatNumber(o.cash)) From 05e400d50a1d152172f7e8819332acd5dbd523bb Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Tue, 9 Jun 2026 15:41:04 +0100 Subject: [PATCH 14/28] Update Custom.lua --- lua/wikis/fortnite/Infobox/Team/Custom.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/wikis/fortnite/Infobox/Team/Custom.lua b/lua/wikis/fortnite/Infobox/Team/Custom.lua index 53570ff8dc6..f1960cbeb00 100644 --- a/lua/wikis/fortnite/Infobox/Team/Custom.lua +++ b/lua/wikis/fortnite/Infobox/Team/Custom.lua @@ -86,7 +86,7 @@ function CustomInjector:parse(id, widgets) elseif id == 'custom' then local prScore, prRank = fetchPowerRanking(self.caller.pagename) table.insert(widgets, Cell{ - name = Link{link = 'Fortnite Power Rankings/Organizations', children = 'Fortnite PR'}, + name = Link{link = 'Fortnite Power Rankings/Organizations', children = 'Fortnite Org PR'}, children = prScore and prRank and (prScore .. ' (Rank #' .. prRank .. ')') or nil }) end From 8c9144e3ec044712cba0f962442af1269dbb761e Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Wed, 10 Jun 2026 16:23:47 +0100 Subject: [PATCH 15/28] Update PowerRankings.lua --- lua/wikis/fortnite/PowerRankings.lua | 134 +++++++++------------------ 1 file changed, 44 insertions(+), 90 deletions(-) diff --git a/lua/wikis/fortnite/PowerRankings.lua b/lua/wikis/fortnite/PowerRankings.lua index b07022343df..658d29eb4ff 100644 --- a/lua/wikis/fortnite/PowerRankings.lua +++ b/lua/wikis/fortnite/PowerRankings.lua @@ -8,105 +8,50 @@ local Lua = require('Module:Lua') local Arguments = Lua.import('Module:Arguments') +local Array = Lua.import('Module:Array') local DateExt = Lua.import('Module:Date/Ext') -local Flags = Lua.import('Module:Flags') local Icon = Lua.import('Module:Icon') local Logic = Lua.import('Module:Logic') -local Page = Lua.import('Module:Page') + +local Opponent = Lua.import('Module:Opponent/Custom') +local OpponentDisplay = Lua.import('Module:OpponentDisplay/Custom') +local PlayerDisplay = Lua.import('Module:Player/Display') local PlayerExt = Lua.import('Module:Player/Ext') -local String = Lua.import('Module:StringUtils') local TableWidgets = Lua.import('Module:Widget/Table2/All') local HtmlWidgets = Lua.import('Module:Widget/Html/All') +local WidgetUtil = Lua.import('Module:Widget/Util') local Link = Lua.import('Module:Widget/Basic/Link') -local Condition = Lua.import('Module:Condition') -local ConditionTree = Condition.Tree -local ConditionNode = Condition.Node -local Comparator = Condition.Comparator -local BooleanOperator = Condition.BooleanOperator -local ColumnName = Condition.ColumnName - local PowerRankingsData = Lua.import('Module:PowerRankings/Data', {loadData = true}) -local DISPLAY_PAGE = 'Fortnite Power Rankings' - -local CONTAINER_STYLE = 'display: inline-flex; align-items: center; white-space: nowrap; ' - .. 'line-height: 1; font-size: 1em; vertical-align: middle;' -local FLAG_SPACING = '5px' local p = {} -local function renderPlayer(name, link) - if String.isEmpty(name) then - return '' - end - local date = DateExt.toYmdInUtc(DateExt.getContextualDateOrNow()) - local pageNameFromLink, displayNameFromLink = PlayerExt.extractFromLink(name) - local player = { - displayName = displayNameFromLink or name, - pageName = String.nilIfEmpty(link) or pageNameFromLink, - } - PlayerExt.syncPlayer(player, {date = date}) - - local items = {} - if String.isNotEmpty(player.flag) then - local flagIcon = String.nilIfEmpty(Flags.Icon{flag = player.flag, shouldLink = false}) - if flagIcon then - items[#items + 1] = string.format( - '%s', - FLAG_SPACING, flagIcon) - end - end - local nameLink = Link{link = player.pageName, linktype = 'internal', children = player.displayName} - items[#items + 1] = string.format('%s', tostring(nameLink)) - - return string.format('%s', CONTAINER_STYLE, table.concat(items)) -end - -local function queryPlayerOrg(name) - if Logic.isEmpty(name) then - return '' - end - local conditions = ConditionTree(BooleanOperator.any):add{ - ConditionNode(ColumnName('pagename'), Comparator.eq, Page.pageifyLink(name)), - ConditionNode(ColumnName('id'), Comparator.eq, name), - } - local row = mw.ext.LiquipediaDB.lpdb('player', { - query = 'team', - conditions = conditions:toString(), - limit = 1, - })[1] or {} - return row.team or '' -end - -local function renderOrg(name, frame) - local org = queryPlayerOrg(name) - if Logic.isEmpty(org) then - return '' - end - return frame:expandTemplate{title = 'Team', args = {org}} -end - +---@param updated string? +---@return Renderable local function buildTitle(updated) - local textChildren = {HtmlWidgets.B{children = 'Fortnite Power Rankings'}} - if Logic.isNotEmpty(updated) then - textChildren[#textChildren + 1] = HtmlWidgets.Span{children = 'Last updated: ' .. updated} - end return HtmlWidgets.Div{ + classes = {'ranking-table__top-row'}, children = { - HtmlWidgets.Div{children = textChildren, classes = {'ranking-table__top-row-text'}}, + HtmlWidgets.Div{ + children = WidgetUtil.collect( + HtmlWidgets.B{children = 'Fortnite Power Rankings'}, + Logic.isNotEmpty(updated) and HtmlWidgets.Span{children = {'Last updated: ', updated}} or nil + ), + classes = {'ranking-table__top-row-text'}, + }, HtmlWidgets.Div{ children = {HtmlWidgets.Span{children = 'Data by Epic Games'}}, classes = {'ranking-table__top-row-logo-container'}, }, }, - classes = {'ranking-table__top-row'}, } end +---@return Renderable local function buildFooter() return Link{ - link = DISPLAY_PAGE, + link = 'Fortnite Power Rankings', linktype = 'internal', children = { HtmlWidgets.Div{ @@ -117,34 +62,43 @@ local function buildFooter() } end +---@param frame Frame +---@return VNode function p.main(frame) local args = Arguments.getArgs(frame) local limit = tonumber(args.limit) local showMore = Logic.readBool(args.showMore) local players = PowerRankingsData.players or {} + if limit then + players = Array.sub(players, 1, limit) + end - local updated = '' + local updated if Logic.isNotEmpty(PowerRankingsData.updated) then - updated = PowerRankingsData.updated .. ' ' .. frame:expandTemplate{title = 'Abbr/UTC'} + updated = PowerRankingsData.updated .. ' ' .. DateExt.defaultTimezone end - local rows = {} - for i, player in ipairs(players) do - if limit and i > limit then break end - local name = player.name or '' - local link = Logic.nilIfEmpty(player.link) - local orgKey = link or name - - rows[#rows + 1] = TableWidgets.Row{children = { - TableWidgets.Cell{children = HtmlWidgets.B{children = player.rank}}, - TableWidgets.Cell{children = HtmlWidgets.B{children = player.points}}, - TableWidgets.Cell{children = renderPlayer(name, link)}, - TableWidgets.Cell{children = renderOrg(orgKey, frame)}, + local rows = Array.map(players, function(entry) + local player = { + displayName = entry.name, + pageName = Logic.nilIfEmpty(entry.link) or entry.name, + } + PlayerExt.syncPlayer(player) + local teamTemplate = PlayerExt.syncTeam(player.pageName) + + return TableWidgets.Row{children = { + TableWidgets.Cell{children = HtmlWidgets.B{children = entry.rank}}, + TableWidgets.Cell{children = HtmlWidgets.B{children = entry.points}}, + TableWidgets.Cell{children = PlayerDisplay.BlockPlayer{player = player}}, + TableWidgets.Cell{children = teamTemplate and OpponentDisplay.BlockOpponent{opponent = { + type = Opponent.team, + template = teamTemplate, + }} or nil}, }} - end + end) - return tostring(TableWidgets.Table{ + return TableWidgets.Table{ title = buildTitle(updated), sortable = false, columns = { @@ -166,7 +120,7 @@ function p.main(frame) }}, TableWidgets.TableBody{children = rows}, }, - }) + } end return p From b6a93b2254d3aeff2b346b483d21da89d74f99c9 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Wed, 10 Jun 2026 16:24:13 +0100 Subject: [PATCH 16/28] Update Orgs.lua --- lua/wikis/fortnite/PowerRankings/Orgs.lua | 41 ++++------------------- 1 file changed, 7 insertions(+), 34 deletions(-) diff --git a/lua/wikis/fortnite/PowerRankings/Orgs.lua b/lua/wikis/fortnite/PowerRankings/Orgs.lua index b729120f93d..b10711dbcaf 100644 --- a/lua/wikis/fortnite/PowerRankings/Orgs.lua +++ b/lua/wikis/fortnite/PowerRankings/Orgs.lua @@ -8,14 +8,13 @@ local Lua = require('Module:Lua') local Arguments = Lua.import('Module:Arguments') -local DateExt = Lua.import('Module:Date/Ext') local Flags = Lua.import('Module:Flags') local Icon = Lua.import('Module:Icon') local Logic = Lua.import('Module:Logic') local Lpdb = Lua.import('Module:Lpdb') local Page = Lua.import('Module:Page') +local PlayerDisplay = Lua.import('Module:Player/Display') local PlayerExt = Lua.import('Module:Player/Ext') -local String = Lua.import('Module:StringUtils') local Team = Lua.import('Module:Team') local HtmlWidgets = Lua.import('Module:Widget/Html/All') @@ -34,39 +33,8 @@ local TOP_N = 200 local MAX_PLAYERS_PER_ORG = 4 local DEFAULT_WEIGHTS = {count = 0.12, pr = 0.35, cash = 0.45} -local CONTAINER_STYLE = 'display: inline-flex; align-items: center; white-space: nowrap; ' - .. 'line-height: 1; font-size: 1em; vertical-align: middle;' -local FLAG_SPACING = '5px' - local p = {} -local function renderPlayer(name, link) - if String.isEmpty(name) then - return '' - end - local date = DateExt.toYmdInUtc(DateExt.getContextualDateOrNow()) - local pageNameFromLink, displayNameFromLink = PlayerExt.extractFromLink(name) - local player = { - displayName = displayNameFromLink or name, - pageName = String.nilIfEmpty(link) or pageNameFromLink, - } - PlayerExt.syncPlayer(player, {date = date}) - - local items = {} - if String.isNotEmpty(player.flag) then - local flagIcon = String.nilIfEmpty(Flags.Icon{flag = player.flag, shouldLink = false}) - if flagIcon then - items[#items + 1] = string.format( - '%s', - FLAG_SPACING, flagIcon) - end - end - local nameLink = Link{link = player.pageName, linktype = 'internal', children = player.displayName} - items[#items + 1] = string.format('%s', tostring(nameLink)) - - return string.format('%s', CONTAINER_STYLE, table.concat(items)) -end - local function queryPlayerOrg(name) if Logic.isEmpty(name) then return '' @@ -417,7 +385,12 @@ function p.main(frame) local names = {} for k = 1, o.count do local m = o.members[k] - local rendered = renderPlayer(m.name, m.link) + local player = { + displayName = m.name, + pageName = Logic.nilIfEmpty(m.link) or m.name, + } + PlayerExt.syncPlayer(player) + local rendered = tostring(PlayerDisplay.InlinePlayer{player = player}) table.insert(names, '' .. rendered .. '') end row:tag('td'):css('text-align', 'left') From c6c568245e023de4de8b451072d3585425dc858c Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Thu, 11 Jun 2026 11:13:13 +0100 Subject: [PATCH 17/28] Update Orgs.lua --- lua/wikis/fortnite/PowerRankings/Orgs.lua | 485 +++++++++++----------- 1 file changed, 246 insertions(+), 239 deletions(-) diff --git a/lua/wikis/fortnite/PowerRankings/Orgs.lua b/lua/wikis/fortnite/PowerRankings/Orgs.lua index b10711dbcaf..3a2aa2eb03e 100644 --- a/lua/wikis/fortnite/PowerRankings/Orgs.lua +++ b/lua/wikis/fortnite/PowerRankings/Orgs.lua @@ -8,17 +8,28 @@ local Lua = require('Module:Lua') local Arguments = Lua.import('Module:Arguments') +local Array = Lua.import('Module:Array') +local DateExt = Lua.import('Module:Date/Ext') local Flags = Lua.import('Module:Flags') local Icon = Lua.import('Module:Icon') local Logic = Lua.import('Module:Logic') local Lpdb = Lua.import('Module:Lpdb') +local MathUtil = Lua.import('Module:MathUtil') local Page = Lua.import('Module:Page') -local PlayerDisplay = Lua.import('Module:Player/Display') -local PlayerExt = Lua.import('Module:Player/Ext') +local Table = Lua.import('Module:Table') local Team = Lua.import('Module:Team') +local TeamTemplate = Lua.import('Module:TeamTemplate') +local Variables = Lua.import('Module:Variables') -local HtmlWidgets = Lua.import('Module:Widget/Html/All') +local Opponent = Lua.import('Module:Opponent/Custom') +local OpponentDisplay = Lua.import('Module:OpponentDisplay/Custom') +local PlayerDisplay = Lua.import('Module:Player/Display/Custom') +local PlayerExt = Lua.import('Module:Player/Ext/Custom') + +local HtmlWidgets = Lua.import('Module:Widget/Html') local Link = Lua.import('Module:Widget/Basic/Link') +local TableWidgets = Lua.import('Module:Widget/Table2/All') +local WidgetUtil = Lua.import('Module:Widget/Util') local Condition = Lua.import('Module:Condition') local ConditionTree = Condition.Tree @@ -26,16 +37,22 @@ local ConditionNode = Condition.Node local Comparator = Condition.Comparator local BooleanOperator = Condition.BooleanOperator local ColumnName = Condition.ColumnName +local ConditionUtil = Condition.Util local PowerRankingsData = Lua.import('Module:PowerRankings/Data', {loadData = true}) local DISPLAY_PAGE = 'Fortnite Power Rankings/Organizations' local TOP_N = 200 local MAX_PLAYERS_PER_ORG = 4 +local MAX_PLAYERS_PER_PLACEMENT = 10 local DEFAULT_WEIGHTS = {count = 0.12, pr = 0.35, cash = 0.45} -local p = {} +local PowerRankingsOrgs = {} + +local function normalizeName(name) + return string.lower((name or ''):gsub('[%s_]', '')) +end -local function queryPlayerOrg(name) +local function resolvePrimaryTeam(name) if Logic.isEmpty(name) then return '' end @@ -48,92 +65,36 @@ local function queryPlayerOrg(name) conditions = conditions:toString(), limit = 1, })[1] or {} - return row.team or '' -end - -local function wrap(tbl) - return tostring(mw.html.create('div') - :addClass('table-responsive') - :css('overflow-x', 'auto') - :css('width', '100%') - :node(tbl)) -end - -local function themedText(content) - return '' .. content .. '' - .. '' .. content .. '' -end - -local function formatNumber(n) - local s = tostring(math.floor((tonumber(n) or 0) + 0.5)) - return (s:reverse():gsub('(%d%d%d)', '%1,'):reverse():gsub('^,', '')) -end - -local function resolvePlayerTeams(players) - local result = {} - for i, pl in ipairs(players) do - result[i] = Logic.nilIfEmpty(queryPlayerOrg(pl.link or pl.name)) - end - return result -end - -local function resolveOrgFlags(list) - local pageByTeam = {} - local pages = {} - for _, o in ipairs(list) do - local ok, raw = pcall(mw.ext.TeamTemplate.raw, o.team) - if ok and type(raw) == 'table' and Logic.isNotEmpty(raw.page) then - local page = raw.page:gsub(' ', '_') - pageByTeam[o.team] = page - table.insert(pages, page) - end - end - - local locByPage = {} - local CHUNK = 50 - for start = 1, #pages, CHUNK do - local conditions = ConditionTree(BooleanOperator.any) - for k = start, math.min(start + CHUNK - 1, #pages) do - conditions:add{ConditionNode(ColumnName('pagename'), Comparator.eq, pages[k])} - end - local rows = mw.ext.LiquipediaDB.lpdb('team', { - conditions = conditions:toString(), - query = 'pagename, location', - limit = 1000, - }) or {} - for _, r in ipairs(rows) do - if Logic.isNotEmpty(r.pagename) then - locByPage[r.pagename:gsub(' ', '_')] = r.location - end - end - end - - for _, o in ipairs(list) do - local page = pageByTeam[o.team] - o.flag = page and Logic.nilIfEmpty(locByPage[page]) or nil + local primary = Logic.nilIfEmpty(row.team) + if primary then + return primary end + return PlayerExt.syncTeam(Page.pageifyLink(name)) or '' end -local function normalizeName(name) - return string.lower((name or ''):gsub('[%s_]', '')) +local function formatNumber(value) + return mw.getContentLanguage():formatNum(MathUtil.round(tonumber(value) or 0)) end local function orgPageKey(team) - local ok, raw = pcall(mw.ext.TeamTemplate.raw, team) - local page = (ok and type(raw) == 'table' and Logic.isNotEmpty(raw.page)) and raw.page or team + local raw = TeamTemplate.getRawOrNil(team) + local page = raw and Logic.nilIfEmpty(raw.page) or team return normalizeName(page) end local function storeOrgRankings(list) - for rank, o in ipairs(list) do + if Logic.readBool(Variables.varDefault('disable_LPDB_storage')) then + return + end + Array.forEach(list, function(o, rank) local key = orgPageKey(o.team) mw.ext.LiquipediaDB.lpdb_datapoint('FTN_ORG_PR_' .. key, { type = 'FTN_ORG_PR', name = key, information = rank, - extradata = {score = string.format('%.1f', o.score)}, + extradata = {score = MathUtil.formatRounded{value = o.score, precision = 1}}, }) - end + end) end local function tournamentIcon(icon, icondark, page, size) @@ -163,25 +124,32 @@ local function gatherPlacementData(year) local opType = item.opponenttype local opName = item.opponentname - for i = 1, 10 do - local pName = opPlayers['p' .. i] - if Logic.isNotEmpty(pName) then - local teamRaw = opPlayers['p' .. i .. 'team'] - if Logic.isEmpty(teamRaw) and opType == 'team' then - teamRaw = opName - end - if Logic.isNotEmpty(teamRaw) then - local norm = normalizeName(teamRaw) - earnings[norm] = (earnings[norm] or 0) + indiv - if isSTierWin and Logic.isNotEmpty(page) then - winPages[norm] = winPages[norm] or {} - if not winPages[norm][page] then - winPages[norm][page] = true - pageSet[page] = true - end - end - end + local function processPlayer(i) + if Logic.isEmpty(opPlayers['p' .. i]) then + return + end + local teamRaw = opPlayers['p' .. i .. 'team'] + if Logic.isEmpty(teamRaw) and opType == 'team' then + teamRaw = opName end + if Logic.isEmpty(teamRaw) then + return + end + local norm = normalizeName(teamRaw) + earnings[norm] = (earnings[norm] or 0) + indiv + if not (isSTierWin and Logic.isNotEmpty(page)) then + return + end + winPages[norm] = winPages[norm] or {} + if winPages[norm][page] then + return + end + winPages[norm][page] = true + pageSet[page] = page + end + + for i = 1, MAX_PLAYERS_PER_PLACEMENT do + processPlayer(i) end end @@ -197,40 +165,139 @@ local function gatherPlacementData(year) limit = 5000, }, process) - local pages = {} - for page in pairs(pageSet) do table.insert(pages, page) end + local pages = Array.extractValues(pageSet) local details = {} - local CHUNK = 40 - for start = 1, #pages, CHUNK do - local conditions = ConditionTree(BooleanOperator.any) - for k = start, math.min(start + CHUNK - 1, #pages) do - conditions:add{ConditionNode(ColumnName('pagename'), Comparator.eq, pages[k])} - end - local rows = mw.ext.LiquipediaDB.lpdb('tournament', { - conditions = conditions:toString(), + if Logic.isNotEmpty(pages) then + local tournamentRows = mw.ext.LiquipediaDB.lpdb('tournament', { + conditions = ConditionUtil.anyOf(ColumnName('pagename'), pages):toString(), query = 'pagename, name, icon, icondark', limit = 100, }) or {} - for _, t in ipairs(rows) do - details[t.pagename] = { - name = Logic.nilIfEmpty(t.name) or t.pagename:gsub('_', ' '), - icon = t.icon, - icondark = t.icondark, + Array.forEach(tournamentRows, function(tournamentRow) + details[tournamentRow.pagename] = { + name = Logic.nilIfEmpty(tournamentRow.name) or tournamentRow.pagename:gsub('_', ' '), + icon = tournamentRow.icon, + icondark = tournamentRow.icondark, } - end + end) end return earnings, winPages, details end -function p.main(frame) +local function resolveOrgFlags(list) + local pageByTeam = {} + local pages = {} + Array.forEach(list, function(o) + local raw = TeamTemplate.getRawOrNil(o.team) + if raw and Logic.isNotEmpty(raw.page) then + local page = raw.page:gsub(' ', '_') + pageByTeam[o.team] = page + table.insert(pages, page) + end + end) + + local locByPage = {} + if Logic.isNotEmpty(pages) then + local teamRows = mw.ext.LiquipediaDB.lpdb('team', { + conditions = ConditionUtil.anyOf(ColumnName('pagename'), pages):toString(), + query = 'pagename, location', + limit = 1000, + }) or {} + Array.forEach(teamRows, function(teamRow) + if Logic.isNotEmpty(teamRow.pagename) then + locByPage[teamRow.pagename] = teamRow.location + end + end) + end + + Array.forEach(list, function(o) + local page = pageByTeam[o.team] + o.flag = page and Logic.nilIfEmpty(locByPage[page]) or nil + end) +end + +---@param updated string? +---@return Widget +local function buildTitle(updated) + return HtmlWidgets.Div{children = WidgetUtil.collect( + HtmlWidgets.B{children = 'Fortnite Organization Power Rankings'}, + Logic.isNotEmpty(updated) and HtmlWidgets.Span{ + css = {['font-weight'] = 'normal'}, + children = {HtmlWidgets.Br{}, 'Last Updated: ', updated}, + } or nil + )} +end + +---@return Widget +local function buildFooter() + return Link{ + link = DISPLAY_PAGE, + linktype = 'internal', + children = { + HtmlWidgets.Div{ + children = {'See Rankings Page', Icon.makeIcon{iconName = 'goto'}}, + classes = {'ranking-table__footer-button'}, + }, + }, + } +end + +---@param wrapped boolean +---@param year integer +---@return Widget +local function buildHeader(wrapped, year) + return TableWidgets.Row{children = WidgetUtil.collect( + TableWidgets.CellHeader{children = 'Rank'}, + TableWidgets.CellHeader{children = ''}, + TableWidgets.CellHeader{children = 'Organization'}, + TableWidgets.CellHeader{children = 'Four Best Players (In Top ' .. TOP_N .. ')'}, + not wrapped and TableWidgets.CellHeader{children = 'Recent Achievements'} or nil, + TableWidgets.CellHeader{children = 'Score'}, + not wrapped and TableWidgets.CellHeader{children = 'Average Players PR'} or nil, + not wrapped and TableWidgets.CellHeader{children = 'Earnings (' .. year .. ')'} or nil + )} +end + +---@param rank integer +---@param o table +---@param wrapped boolean +---@return Widget +local function buildRow(rank, o, wrapped) + local flagCell = Logic.isNotEmpty(o.flag) and Flags.Icon{flag = o.flag, shouldLink = false} or '' + local memberDisplays = Array.map(Array.sub(o.members, 1, o.count), function(m) + local player = {displayName = m.name, pageName = Logic.nilIfEmpty(m.link) or m.name} + PlayerExt.syncPlayer(player) + return tostring(PlayerDisplay.InlinePlayer{player = player}) + end) + local membersText = table.concat(memberDisplays, ', ') .. ' (' .. o.count .. ')' + local achievementsText = table.concat(Array.map(o.achievements or {}, function(a) + return tournamentIcon(a.icon, a.icondark, a.page, '30x30px') + end)) + + return TableWidgets.Row{children = WidgetUtil.collect( + TableWidgets.Cell{children = HtmlWidgets.B{children = rank}}, + TableWidgets.Cell{children = flagCell}, + TableWidgets.Cell{children = OpponentDisplay.BlockOpponent{ + opponent = {type = Opponent.team, template = o.team}, + }}, + TableWidgets.Cell{children = membersText}, + not wrapped and TableWidgets.Cell{children = achievementsText} or nil, + TableWidgets.Cell{children = HtmlWidgets.B{children = MathUtil.formatRounded{value = o.score, precision = 1}}}, + not wrapped and TableWidgets.Cell{children = formatNumber(o.avgPR)} or nil, + not wrapped and TableWidgets.Cell{children = '$' .. formatNumber(o.cash)} or nil + )} +end + +---@param frame Frame +---@return Widget +function PowerRankingsOrgs.main(frame) local args = Arguments.getArgs(frame) local limit = tonumber(args.limit) local showMore = Logic.readBool(args.showMore) local wrapped = Logic.readBool(args.wrapped) - local colspan = wrapped and 5 or 8 - local year = tonumber(args.year) or tonumber(os.date('!%Y')) + local year = tonumber(args.year) or DateExt.getYearOf(DateExt.getContextualDateOrNow()) local weights = { count = tonumber(args.wCount) or DEFAULT_WEIGHTS.count, pr = tonumber(args.wPR) or DEFAULT_WEIGHTS.pr, @@ -238,59 +305,56 @@ function p.main(frame) } local weightSum = weights.count + weights.pr + weights.cash - local players = {} - for _, pl in ipairs(PowerRankingsData.players or {}) do - if (tonumber(pl.rank) or 0) <= TOP_N then - table.insert(players, pl) - end + local updated + if Logic.isNotEmpty(PowerRankingsData.updated) then + updated = PowerRankingsData.updated .. ' ' .. DateExt.defaultTimezone end - local teams = resolvePlayerTeams(players) + + local players = Array.filter(PowerRankingsData.players or {}, function(pl) + return (tonumber(pl.rank) or 0) <= TOP_N + end) local byOrg = {} - for i, pl in ipairs(players) do - local team = teams[i] - if team then - byOrg[team] = byOrg[team] or {} - table.insert(byOrg[team], { - name = pl.name, - link = pl.link, - points = tonumber(pl.points) or 0, - }) + Array.forEach(players, function(pl) + local team = Logic.nilIfEmpty(resolvePrimaryTeam(pl.link or pl.name)) + if not team then + return end - end + byOrg[team] = byOrg[team] or {} + table.insert(byOrg[team], {name = pl.name, link = pl.link, points = tonumber(pl.points) or 0}) + end) local list = {} for team, members in pairs(byOrg) do table.sort(members, function(a, b) return a.points > b.points end) local count = math.min(#members, MAX_PLAYERS_PER_ORG) - local sum = 0 - for k = 1, count do sum = sum + members[k].points end + local topPoints = Array.map(Array.sub(members, 1, count), function(m) return m.points end) table.insert(list, { team = team, members = members, count = count, - avgPR = sum / count, + avgPR = MathUtil.sum(topPoints) / count, }) end local teamEarnings, teamWinPages, tournamentDetails = gatherPlacementData(year) - for _, o in ipairs(list) do - o.histNames = Team.queryHistoricalNames(o.team) or {o.team} - local cash = 0 - for _, nm in ipairs(o.histNames) do - cash = cash + (teamEarnings[normalizeName(nm)] or 0) + Array.forEach(list, function(o) + o.histNames = Team.queryHistoricalNames(o.team) + if Logic.isEmpty(o.histNames) then + o.histNames = {o.team} end - o.cash = cash - end + o.cash = Array.reduce(o.histNames, function(total, nm) + return total + (teamEarnings[normalizeName(nm)] or 0) + end, 0) + end) local n = #list local function rankNormalize(getValue, field) if n <= 1 then - for _, o in ipairs(list) do o[field] = 1 end + Array.forEach(list, function(o) o[field] = 1 end) return end - local sorted = {} - for _, o in ipairs(list) do table.insert(sorted, o) end + local sorted = Table.copy(list) table.sort(sorted, function(a, b) return getValue(a) < getValue(b) end) local i = 1 while i <= n do @@ -306,9 +370,9 @@ function p.main(frame) rankNormalize(function(o) return o.avgPR end, 'nPR') rankNormalize(function(o) return o.cash end, 'nCash') - for _, o in ipairs(list) do + Array.forEach(list, function(o) o.score = 100 * (weights.count * o.nCount + weights.pr * o.nPR + weights.cash * o.nCash) / weightSum - end + end) table.sort(list, function(a, b) if a.score ~= b.score then return a.score > b.score end @@ -319,114 +383,57 @@ function p.main(frame) storeOrgRankings(list) end - local display = {} - for i, o in ipairs(list) do - if limit and i > limit then break end - table.insert(display, o) - end + local display = limit and Array.sub(list, 1, limit) or list if not wrapped then - for _, o in ipairs(display) do + Array.forEach(display, function(o) local achievements = {} local seen = {} - for _, nm in ipairs(o.histNames) do + Array.forEach(o.histNames, function(nm) local pagesForTeam = teamWinPages[normalizeName(nm)] - if pagesForTeam then - for page in pairs(pagesForTeam) do - if not seen[page] then - seen[page] = true - local d = tournamentDetails[page] or {name = page:gsub('_', ' '), icon = '', icondark = ''} - table.insert(achievements, {page = page, name = d.name, icon = d.icon, icondark = d.icondark}) - end + if not pagesForTeam then + return + end + for page in pairs(pagesForTeam) do + if not seen[page] then + seen[page] = true + local d = tournamentDetails[page] or {name = page:gsub('_', ' '), icon = '', icondark = ''} + table.insert(achievements, {page = page, name = d.name, icon = d.icon, icondark = d.icondark}) end end - end + end) o.achievements = achievements - end + end) end resolveOrgFlags(display) - local tbl = mw.html.create('table') - :addClass('table2__table wikitable wikitable-striped wikitable-bordered') - :css('width', '100%') - :css('border-collapse', 'collapse') - - local title = "'''Fortnite Organization Power Rankings'''" - if Logic.isNotEmpty(PowerRankingsData.updated) then - local utc = frame:expandTemplate{title = 'Abbr/UTC'} - title = title .. "
''Last Updated: " .. PowerRankingsData.updated .. ' ' .. utc .. "''" - end - tbl:tag('tr'):tag('th') - :attr('colspan', colspan) - :wikitext(themedText(title)) - - local header = tbl:tag('tr') - header:tag('th'):css('width', '1%'):css('white-space', 'nowrap'):wikitext('Rank') - header:tag('th'):css('width', '1%'):css('padding', '0 8px'):wikitext('') - header:tag('th'):css('text-align', 'left'):wikitext('Organization') - header:tag('th'):css('text-align', 'left'):wikitext('Four Best Players (In Top 200)') - if not wrapped then - header:tag('th'):css('text-align', 'center'):wikitext('Recent Achievements') - end - header:tag('th'):css('width', '1%'):css('white-space', 'nowrap'):wikitext('Score') - if not wrapped then - header:tag('th'):css('width', '1%'):css('white-space', 'nowrap'):wikitext('Average Players PR') - header:tag('th'):css('width', '1%'):css('white-space', 'nowrap'):wikitext('Earnings (' .. year .. ')') - end - - for i, o in ipairs(display) do - local row = tbl:tag('tr') - row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap'):wikitext('' .. i .. '') - local flagCell = Logic.isNotEmpty(o.flag) and Flags.Icon{flag = o.flag, shouldLink = false} or '' - row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap'):css('padding', '0 8px'):wikitext(flagCell) - row:tag('td'):css('text-align', 'left'):css('white-space', 'nowrap') - :wikitext(frame:expandTemplate{title = 'Team', args = {o.team}}) - local names = {} - for k = 1, o.count do - local m = o.members[k] - local player = { - displayName = m.name, - pageName = Logic.nilIfEmpty(m.link) or m.name, - } - PlayerExt.syncPlayer(player) - local rendered = tostring(PlayerDisplay.InlinePlayer{player = player}) - table.insert(names, '' .. rendered .. '') - end - row:tag('td'):css('text-align', 'left') - :wikitext(table.concat(names, ', ') .. ' (' .. o.count .. ')') - if not wrapped then - local achText = '' - for _, a in ipairs(o.achievements or {}) do - achText = achText .. tournamentIcon(a.icon, a.icondark, a.page, '30x30px') - end - row:tag('td'):css('text-align', 'center'):wikitext(achText) - end - row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap') - :wikitext('' .. string.format('%.1f', o.score) .. '') - if not wrapped then - row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap'):wikitext(formatNumber(o.avgPR)) - row:tag('td'):css('text-align', 'center'):css('white-space', 'nowrap'):wikitext('$' .. formatNumber(o.cash)) - end - end - - if showMore then - local footer = Link{ - link = DISPLAY_PAGE, - linktype = 'internal', - children = { - HtmlWidgets.Div{ - children = {'See Rankings Page', Icon.makeIcon{iconName = 'goto'}}, - classes = {'ranking-table__footer-button'}, - }, - }, - } - tbl:tag('tr'):tag('td') - :attr('colspan', colspan) - :wikitext(tostring(footer)) - end + local columns = WidgetUtil.collect( + {align = 'center'}, + {align = 'center'}, + {align = 'left'}, + {align = 'left'}, + not wrapped and {align = 'center'} or nil, + {align = 'center'}, + not wrapped and {align = 'center'} or nil, + not wrapped and {align = 'center'} or nil + ) + + local rows = Array.map(display, function(o, rank) + return buildRow(rank, o, wrapped) + end) - return wrap(tbl) + return TableWidgets.Table{ + title = buildTitle(updated), + sortable = false, + columns = columns, + footer = showMore and buildFooter() or nil, + css = {width = '100%'}, + children = { + TableWidgets.TableHeader{children = {buildHeader(wrapped, year)}}, + TableWidgets.TableBody{children = rows}, + }, + } end -return p +return PowerRankingsOrgs From cb220f705a1959ae013ce49c6857cf9f1528a272 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Thu, 11 Jun 2026 11:13:48 +0100 Subject: [PATCH 18/28] Update PowerRankings.lua --- lua/wikis/fortnite/PowerRankings.lua | 42 ++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/lua/wikis/fortnite/PowerRankings.lua b/lua/wikis/fortnite/PowerRankings.lua index 658d29eb4ff..e424392f816 100644 --- a/lua/wikis/fortnite/PowerRankings.lua +++ b/lua/wikis/fortnite/PowerRankings.lua @@ -12,20 +12,46 @@ local Array = Lua.import('Module:Array') local DateExt = Lua.import('Module:Date/Ext') local Icon = Lua.import('Module:Icon') local Logic = Lua.import('Module:Logic') +local Page = Lua.import('Module:Page') local Opponent = Lua.import('Module:Opponent/Custom') local OpponentDisplay = Lua.import('Module:OpponentDisplay/Custom') -local PlayerDisplay = Lua.import('Module:Player/Display') -local PlayerExt = Lua.import('Module:Player/Ext') +local PlayerDisplay = Lua.import('Module:Player/Display/Custom') +local PlayerExt = Lua.import('Module:Player/Ext/Custom') -local TableWidgets = Lua.import('Module:Widget/Table2/All') local HtmlWidgets = Lua.import('Module:Widget/Html/All') -local WidgetUtil = Lua.import('Module:Widget/Util') local Link = Lua.import('Module:Widget/Basic/Link') +local TableWidgets = Lua.import('Module:Widget/Table2/All') +local WidgetUtil = Lua.import('Module:Widget/Util') + +local Condition = Lua.import('Module:Condition') +local ConditionTree = Condition.Tree +local ConditionNode = Condition.Node +local Comparator = Condition.Comparator +local BooleanOperator = Condition.BooleanOperator +local ColumnName = Condition.ColumnName local PowerRankingsData = Lua.import('Module:PowerRankings/Data', {loadData = true}) -local p = {} +local PowerRankings = {} + +---@param name string +---@return string? +local function resolvePrimaryTeam(name) + if Logic.isEmpty(name) then + return nil + end + local conditions = ConditionTree(BooleanOperator.any):add{ + ConditionNode(ColumnName('pagename'), Comparator.eq, Page.pageifyLink(name)), + ConditionNode(ColumnName('id'), Comparator.eq, name), + } + local row = mw.ext.LiquipediaDB.lpdb('player', { + query = 'team', + conditions = conditions:toString(), + limit = 1, + })[1] or {} + return Logic.nilIfEmpty(row.team) or Logic.nilIfEmpty(PlayerExt.syncTeam(Page.pageifyLink(name))) +end ---@param updated string? ---@return Renderable @@ -64,7 +90,7 @@ end ---@param frame Frame ---@return VNode -function p.main(frame) +function PowerRankings.main(frame) local args = Arguments.getArgs(frame) local limit = tonumber(args.limit) local showMore = Logic.readBool(args.showMore) @@ -85,7 +111,7 @@ function p.main(frame) pageName = Logic.nilIfEmpty(entry.link) or entry.name, } PlayerExt.syncPlayer(player) - local teamTemplate = PlayerExt.syncTeam(player.pageName) + local teamTemplate = resolvePrimaryTeam(entry.link or entry.name) return TableWidgets.Row{children = { TableWidgets.Cell{children = HtmlWidgets.B{children = entry.rank}}, @@ -123,4 +149,4 @@ function p.main(frame) } end -return p +return PowerRankings From 43fc5be525261d1ad9db49ee3991c4142ed31aab Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Thu, 11 Jun 2026 21:21:21 +0100 Subject: [PATCH 19/28] Update PowerRankings.lua --- lua/wikis/fortnite/PowerRankings.lua | 30 ++-------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/lua/wikis/fortnite/PowerRankings.lua b/lua/wikis/fortnite/PowerRankings.lua index e424392f816..ea825291b2d 100644 --- a/lua/wikis/fortnite/PowerRankings.lua +++ b/lua/wikis/fortnite/PowerRankings.lua @@ -12,47 +12,21 @@ local Array = Lua.import('Module:Array') local DateExt = Lua.import('Module:Date/Ext') local Icon = Lua.import('Module:Icon') local Logic = Lua.import('Module:Logic') -local Page = Lua.import('Module:Page') local Opponent = Lua.import('Module:Opponent/Custom') local OpponentDisplay = Lua.import('Module:OpponentDisplay/Custom') local PlayerDisplay = Lua.import('Module:Player/Display/Custom') local PlayerExt = Lua.import('Module:Player/Ext/Custom') -local HtmlWidgets = Lua.import('Module:Widget/Html/All') +local HtmlWidgets = Lua.import('Module:Widget/Html') local Link = Lua.import('Module:Widget/Basic/Link') local TableWidgets = Lua.import('Module:Widget/Table2/All') local WidgetUtil = Lua.import('Module:Widget/Util') -local Condition = Lua.import('Module:Condition') -local ConditionTree = Condition.Tree -local ConditionNode = Condition.Node -local Comparator = Condition.Comparator -local BooleanOperator = Condition.BooleanOperator -local ColumnName = Condition.ColumnName - local PowerRankingsData = Lua.import('Module:PowerRankings/Data', {loadData = true}) local PowerRankings = {} ----@param name string ----@return string? -local function resolvePrimaryTeam(name) - if Logic.isEmpty(name) then - return nil - end - local conditions = ConditionTree(BooleanOperator.any):add{ - ConditionNode(ColumnName('pagename'), Comparator.eq, Page.pageifyLink(name)), - ConditionNode(ColumnName('id'), Comparator.eq, name), - } - local row = mw.ext.LiquipediaDB.lpdb('player', { - query = 'team', - conditions = conditions:toString(), - limit = 1, - })[1] or {} - return Logic.nilIfEmpty(row.team) or Logic.nilIfEmpty(PlayerExt.syncTeam(Page.pageifyLink(name))) -end - ---@param updated string? ---@return Renderable local function buildTitle(updated) @@ -111,7 +85,7 @@ function PowerRankings.main(frame) pageName = Logic.nilIfEmpty(entry.link) or entry.name, } PlayerExt.syncPlayer(player) - local teamTemplate = resolvePrimaryTeam(entry.link or entry.name) + local teamTemplate = PlayerExt.syncTeam(player.pageName) return TableWidgets.Row{children = { TableWidgets.Cell{children = HtmlWidgets.B{children = entry.rank}}, From f4091bd735e5400eef531b0fafc27440beb8d97f Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Thu, 11 Jun 2026 21:21:38 +0100 Subject: [PATCH 20/28] Update Orgs.lua --- lua/wikis/fortnite/PowerRankings/Orgs.lua | 36 +++++++---------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/lua/wikis/fortnite/PowerRankings/Orgs.lua b/lua/wikis/fortnite/PowerRankings/Orgs.lua index 3a2aa2eb03e..78d1245749b 100644 --- a/lua/wikis/fortnite/PowerRankings/Orgs.lua +++ b/lua/wikis/fortnite/PowerRankings/Orgs.lua @@ -12,6 +12,7 @@ local Array = Lua.import('Module:Array') local DateExt = Lua.import('Module:Date/Ext') local Flags = Lua.import('Module:Flags') local Icon = Lua.import('Module:Icon') +local LeagueIcon = Lua.import('Module:LeagueIcon') local Logic = Lua.import('Module:Logic') local Lpdb = Lua.import('Module:Lpdb') local MathUtil = Lua.import('Module:MathUtil') @@ -56,19 +57,6 @@ local function resolvePrimaryTeam(name) if Logic.isEmpty(name) then return '' end - local conditions = ConditionTree(BooleanOperator.any):add{ - ConditionNode(ColumnName('pagename'), Comparator.eq, Page.pageifyLink(name)), - ConditionNode(ColumnName('id'), Comparator.eq, name), - } - local row = mw.ext.LiquipediaDB.lpdb('player', { - query = 'team', - conditions = conditions:toString(), - limit = 1, - })[1] or {} - local primary = Logic.nilIfEmpty(row.team) - if primary then - return primary - end return PlayerExt.syncTeam(Page.pageifyLink(name)) or '' end @@ -97,17 +85,6 @@ local function storeOrgRankings(list) end) end -local function tournamentIcon(icon, icondark, page, size) - if Logic.isEmpty(icon) then return '' end - icondark = Logic.nilIfEmpty(icondark) or icon - local function modeSpan(mode, image) - return string.format( - '[[File:%s|%s|link=%s]]', - mode, image, size, page) - end - return modeSpan('lightmode', icon) .. modeSpan('darkmode', icondark) -end - local function gatherPlacementData(year) local earnings = {} local winPages = {} @@ -225,7 +202,7 @@ local function buildTitle(updated) HtmlWidgets.B{children = 'Fortnite Organization Power Rankings'}, Logic.isNotEmpty(updated) and HtmlWidgets.Span{ css = {['font-weight'] = 'normal'}, - children = {HtmlWidgets.Br{}, 'Last Updated: ', updated}, + children = {HtmlWidgets.Br{}, 'Last updated: ', updated}, } or nil )} end @@ -273,7 +250,14 @@ local function buildRow(rank, o, wrapped) end) local membersText = table.concat(memberDisplays, ', ') .. ' (' .. o.count .. ')' local achievementsText = table.concat(Array.map(o.achievements or {}, function(a) - return tournamentIcon(a.icon, a.icondark, a.page, '30x30px') + return LeagueIcon.display{ + icon = a.icon, + iconDark = a.icondark, + link = a.page, + name = a.name, + size = 30, + options = {noTemplate = true}, + } end)) return TableWidgets.Row{children = WidgetUtil.collect( From ad3f9115681a449a3747c6cc2f7a5cecf3ca0bca Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Thu, 11 Jun 2026 23:20:09 +0100 Subject: [PATCH 21/28] Update Orgs.lua --- lua/wikis/fortnite/PowerRankings/Orgs.lua | 48 ++++++++++++++--------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/lua/wikis/fortnite/PowerRankings/Orgs.lua b/lua/wikis/fortnite/PowerRankings/Orgs.lua index 78d1245749b..fad1a069da1 100644 --- a/lua/wikis/fortnite/PowerRankings/Orgs.lua +++ b/lua/wikis/fortnite/PowerRankings/Orgs.lua @@ -18,7 +18,6 @@ local Lpdb = Lua.import('Module:Lpdb') local MathUtil = Lua.import('Module:MathUtil') local Page = Lua.import('Module:Page') local Table = Lua.import('Module:Table') -local Team = Lua.import('Module:Team') local TeamTemplate = Lua.import('Module:TeamTemplate') local Variables = Lua.import('Module:Variables') @@ -53,6 +52,22 @@ local function normalizeName(name) return string.lower((name or ''):gsub('[%s_]', '')) end +local function buildMatchKeys(team) + local histNames = TeamTemplate.queryHistoricalNames(team) + if Logic.isEmpty(histNames) then + histNames = {team} + end + local keys = {} + Array.forEach(histNames, function(nm) + keys[normalizeName(nm)] = true + local raw = TeamTemplate.getRawOrNil(nm) + if raw and Logic.isNotEmpty(raw.page) then + keys[normalizeName(raw.page)] = true + end + end) + return keys +end + local function resolvePrimaryTeam(name) if Logic.isEmpty(name) then return '' @@ -323,13 +338,11 @@ function PowerRankingsOrgs.main(frame) local teamEarnings, teamWinPages, tournamentDetails = gatherPlacementData(year) Array.forEach(list, function(o) - o.histNames = Team.queryHistoricalNames(o.team) - if Logic.isEmpty(o.histNames) then - o.histNames = {o.team} + o.matchKeys = buildMatchKeys(o.team) + o.cash = 0 + for key in pairs(o.matchKeys) do + o.cash = o.cash + (teamEarnings[key] or 0) end - o.cash = Array.reduce(o.histNames, function(total, nm) - return total + (teamEarnings[normalizeName(nm)] or 0) - end, 0) end) local n = #list @@ -373,19 +386,18 @@ function PowerRankingsOrgs.main(frame) Array.forEach(display, function(o) local achievements = {} local seen = {} - Array.forEach(o.histNames, function(nm) - local pagesForTeam = teamWinPages[normalizeName(nm)] - if not pagesForTeam then - return - end - for page in pairs(pagesForTeam) do - if not seen[page] then - seen[page] = true - local d = tournamentDetails[page] or {name = page:gsub('_', ' '), icon = '', icondark = ''} - table.insert(achievements, {page = page, name = d.name, icon = d.icon, icondark = d.icondark}) + for key in pairs(o.matchKeys) do + local pagesForTeam = teamWinPages[key] + if pagesForTeam then + for page in pairs(pagesForTeam) do + if not seen[page] then + seen[page] = true + local d = tournamentDetails[page] or {name = page:gsub('_', ' '), icon = '', icondark = ''} + table.insert(achievements, {page = page, name = d.name, icon = d.icon, icondark = d.icondark}) + end end end - end) + end o.achievements = achievements end) end From ca33e7a987ab444ec8add23688d507dcd5432165 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Wed, 17 Jun 2026 20:02:16 +0100 Subject: [PATCH 22/28] Update Orgs.lua Applying hjpalpha's code --- lua/wikis/fortnite/PowerRankings/Orgs.lua | 603 +++++++++++----------- 1 file changed, 291 insertions(+), 312 deletions(-) diff --git a/lua/wikis/fortnite/PowerRankings/Orgs.lua b/lua/wikis/fortnite/PowerRankings/Orgs.lua index fad1a069da1..d7d6cfeae2b 100644 --- a/lua/wikis/fortnite/PowerRankings/Orgs.lua +++ b/lua/wikis/fortnite/PowerRankings/Orgs.lua @@ -9,6 +9,7 @@ local Lua = require('Module:Lua') local Arguments = Lua.import('Module:Arguments') local Array = Lua.import('Module:Array') +local Currency = Lua.import('Module:Currency') local DateExt = Lua.import('Module:Date/Ext') local Flags = Lua.import('Module:Flags') local Icon = Lua.import('Module:Icon') @@ -16,10 +17,10 @@ local LeagueIcon = Lua.import('Module:LeagueIcon') local Logic = Lua.import('Module:Logic') local Lpdb = Lua.import('Module:Lpdb') local MathUtil = Lua.import('Module:MathUtil') +local Operator = Lua.import('Module:Operator') local Page = Lua.import('Module:Page') local Table = Lua.import('Module:Table') local TeamTemplate = Lua.import('Module:TeamTemplate') -local Variables = Lua.import('Module:Variables') local Opponent = Lua.import('Module:Opponent/Custom') local OpponentDisplay = Lua.import('Module:OpponentDisplay/Custom') @@ -43,176 +44,304 @@ local PowerRankingsData = Lua.import('Module:PowerRankings/Data', {loadData = tr local DISPLAY_PAGE = 'Fortnite Power Rankings/Organizations' local TOP_N = 200 local MAX_PLAYERS_PER_ORG = 4 -local MAX_PLAYERS_PER_PLACEMENT = 10 local DEFAULT_WEIGHTS = {count = 0.12, pr = 0.35, cash = 0.45} +---@class FortniteRankingsPlayer: standardPlayer +---@field rank integer +---@field points number + +---@class FortniteRankingsTeam +---@field team string +---@field pageName string +---@field count integer +---@field score number +---@field earnings number +---@field average number +---@field achievements string[] +---@field players FortniteRankingsPlayer[] +---@field rank integer +---@field flag string? +---@field countRank integer? +---@field averageRank integer? +---@field earningsRank integer? + + local PowerRankingsOrgs = {} -local function normalizeName(name) - return string.lower((name or ''):gsub('[%s_]', '')) -end +---@param frame Frame +---@return Renderable +function PowerRankingsOrgs.main(frame) + local args = Arguments.getArgs(frame) -local function buildMatchKeys(team) - local histNames = TeamTemplate.queryHistoricalNames(team) - if Logic.isEmpty(histNames) then - histNames = {team} - end - local keys = {} - Array.forEach(histNames, function(nm) - keys[normalizeName(nm)] = true - local raw = TeamTemplate.getRawOrNil(nm) - if raw and Logic.isNotEmpty(raw.page) then - keys[normalizeName(raw.page)] = true + local config = { + limit = tonumber(args.limit), + showMore = Logic.readBool(args.showMore), + wrapped = Logic.readBool(args.wrapped), + year = tonumber(args.year) or DateExt.getYearOf(), + weights = { + count = tonumber(args.wCount) or DEFAULT_WEIGHTS.count, + pr = tonumber(args.wPR) or DEFAULT_WEIGHTS.pr, + cash = tonumber(args.wCash) or DEFAULT_WEIGHTS.cash, + }, + updated = Logic.isNotEmpty(PowerRankingsData.updated) + and PowerRankingsData.updated .. ' ' .. DateExt.defaultTimezone + or nil, + } + config.weightSum = config.weights.count + config.weights.pr + config.weights.cash + + ---@type FortniteRankingsPlayer[] + local players = Array.map(PowerRankingsData.players or {}, function(player) + local rank = tonumber(player.rank) + if not rank or rank > TOP_N then + return end - end) - return keys -end -local function resolvePrimaryTeam(name) - if Logic.isEmpty(name) then - return '' - end - return PlayerExt.syncTeam(Page.pageifyLink(name)) or '' -end + local pageName = Page.pageifyLink(player.link or player.name) --[[@as string]] -local function formatNumber(value) - return mw.getContentLanguage():formatNum(MathUtil.round(tonumber(value) or 0)) -end + return PlayerExt.syncPlayer{ + pageName = pageName, + displayName = player.name, + --todo: test if "updated" works, else: team = PlayerExt.syncTeam(pageName), + team = PlayerExt.syncTeam(pageName, nil, config.updated), + rank = rank, + points = tonumber(player.points), + } + end) -local function orgPageKey(team) - local raw = TeamTemplate.getRawOrNil(team) - local page = raw and Logic.nilIfEmpty(raw.page) or team - return normalizeName(page) -end + local historicalToTeam = {} + local byTeam = {} + Array.forEach(players, function(player) + if not player.team then return end + local team = historicalToTeam[player.team] + if not team then + team = player.team --[[@as string]] + historicalToTeam[team] = team + local teamPage = Page.pageifyLink(TeamTemplate.getPageName(team)) --[[@as string]] + Array.forEach(TeamTemplate.queryHistoricalNames(teamPage), function(t) + historicalToTeam[t] = team + end) + byTeam[team] = {players = {}, team = team, earnings = 0, achievements = {}, pageName = teamPage} + end + table.insert(byTeam[team].players, player) + end) -local function storeOrgRankings(list) - if Logic.readBool(Variables.varDefault('disable_LPDB_storage')) then - return + for _, team in pairs(byTeam) do + Array.sortInPlaceBy(team.players, Operator.property('points')) + team.players = Array.reverse(team.players) + team.count = math.min(#team.players, MAX_PLAYERS_PER_ORG) + local points = Array.map(Array.sub(team.players, 1, team.count), Operator.property('points')) + team.average = MathUtil.sum(points) / team.count end - Array.forEach(list, function(o, rank) - local key = orgPageKey(o.team) - mw.ext.LiquipediaDB.lpdb_datapoint('FTN_ORG_PR_' .. key, { - type = 'FTN_ORG_PR', - name = key, - information = rank, - extradata = {score = MathUtil.formatRounded{value = o.score, precision = 1}}, - }) + + local achievementsInfo = PowerRankingsOrgs._addPlacementData(byTeam, historicalToTeam, config.year) + + local teams = Array.extractValues(byTeam) + Array.forEach(teams, function(team) + team.achievements = Array.unique(team.achievements) end) -end -local function gatherPlacementData(year) - local earnings = {} - local winPages = {} - local pageSet = {} - - local function process(item) - local indiv = tonumber(item.individualprizemoney) or 0 - local page = item.pagename - local isSTierWin = item.placement == '1' - and (item.liquipediatier == '1' or item.liquipediatier == 'S-Tier') - and item.liquipediatiertype ~= 'Qualifier' - and item.liquipediatiertype ~= 'Showmatch' - local opPlayers = item.opponentplayers or {} - local opType = item.opponenttype - local opName = item.opponentname - - local function processPlayer(i) - if Logic.isEmpty(opPlayers['p' .. i]) then - return - end - local teamRaw = opPlayers['p' .. i .. 'team'] - if Logic.isEmpty(teamRaw) and opType == 'team' then - teamRaw = opName - end - if Logic.isEmpty(teamRaw) then - return - end - local norm = normalizeName(teamRaw) - earnings[norm] = (earnings[norm] or 0) + indiv - if not (isSTierWin and Logic.isNotEmpty(page)) then - return - end - winPages[norm] = winPages[norm] or {} - if winPages[norm][page] then - return - end - winPages[norm][page] = true - pageSet[page] = page - end + PowerRankingsOrgs._determineScore(teams, config.weights, config.weightSum) - for i = 1, MAX_PLAYERS_PER_PLACEMENT do - processPlayer(i) + table.sort(teams, function(a, b) + if a.score ~= b.score then + return a.score > b.score end + return a.average > b.average + end) + + if not config.wrapped then + PowerRankingsOrgs._store(teams) + end + + if config.limit then + teams = Array.sub(teams, 1, config.limit) end + PowerRankingsOrgs._fetchTeamFlags(teams) + + return PowerRankingsOrgs._buildDisplay(teams, achievementsInfo, config) +end + +---@param teams FortniteRankingsTeam[] +function PowerRankingsOrgs._fetchTeamFlags(teams) + local teamByPageName = {} + Array.forEach(teams, function(team, teamIndex) + teamByPageName[team.pageName] = teamIndex + end) + + local pages = Array.extractKeys(teamByPageName) + local queryResults = mw.ext.LiquipediaDB.lpdb('team', { + conditions = tostring(ConditionUtil.anyOf(ColumnName('pagename'), pages)), + query = 'pagename, locations, location', + limit = 5000, + }) + Array.forEach(queryResults, function(teamRecord) + local index = teamByPageName[teamRecord.pagename] + -- `.location` (deprecated) is needed on fortnite since their input on team infoboxes is bad ... + teams[index].flag = Logic.emptyOr(teamRecord.locations['country1'], teamRecord.location) + end) +end + +---@param byTeam table +---@param historicalToTeam table +---@param year integer +---@return {pageName: string, displayName: string, icon: string?, iconDark: string?}[] +function PowerRankingsOrgs._addPlacementData(byTeam, historicalToTeam, year) local placementConditions = ConditionTree(BooleanOperator.all):add{ ConditionNode(ColumnName('date_year'), Comparator.eq, tostring(year)), ConditionNode(ColumnName('prizemoney'), Comparator.gt, '0'), } + ---@type table + local achievements = {} + Lpdb.executeMassQuery('placement', { - conditions = placementConditions:toString(), - query = 'pagename, opponentname, opponenttype, opponentplayers, ' + conditions = tostring(placementConditions), + query = 'pagename, opponentname, opponenttemplate, opponenttype, opponentplayers, ' .. 'individualprizemoney, placement, liquipediatier, liquipediatiertype', limit = 5000, - }, process) - - local pages = Array.extractValues(pageSet) - - local details = {} - if Logic.isNotEmpty(pages) then - local tournamentRows = mw.ext.LiquipediaDB.lpdb('tournament', { - conditions = ConditionUtil.anyOf(ColumnName('pagename'), pages):toString(), - query = 'pagename, name, icon, icondark', - limit = 100, - }) or {} - Array.forEach(tournamentRows, function(tournamentRow) - details[tournamentRow.pagename] = { - name = Logic.nilIfEmpty(tournamentRow.name) or tournamentRow.pagename:gsub('_', ' '), - icon = tournamentRow.icon, - icondark = tournamentRow.icondark, - } + }, function(placement) + local prize = tonumber(placement.individualprizemoney) or 0 + local isAchievement = tonumber(placement.placement) == 1 + and tonumber(placement.liquipediatier) == 1 + and not Table.includes({'Qualifier', 'Showmatch'}, placement.liquipediatiertype) + + local opponent = Opponent.fromLpdbStruct(placement) + Array.forEach(opponent.players, function(player) + local teamTemplate = historicalToTeam[player.team] + local team = byTeam[teamTemplate] + if not team then return end + team.earnings = team.earnings + prize + if isAchievement then + achievements[placement.pagename] = true + table.insert(team.achievements, placement.pagename) + end end) - end + end) - return earnings, winPages, details + return PowerRankingsOrgs._getTournamentInfo(Array.extractKeys(achievements)) end -local function resolveOrgFlags(list) - local pageByTeam = {} - local pages = {} - Array.forEach(list, function(o) - local raw = TeamTemplate.getRawOrNil(o.team) - if raw and Logic.isNotEmpty(raw.page) then - local page = raw.page:gsub(' ', '_') - pageByTeam[o.team] = page - table.insert(pages, page) - end +---@param pages string[] +---@return table +function PowerRankingsOrgs._getTournamentInfo(pages) + if Logic.isEmpty(pages) then return {} end + + local queryData = mw.ext.LiquipediaDB.lpdb('tournament', { + conditions = tostring(ConditionUtil.anyOf(ColumnName('pagename'), pages)), + query = 'pagename, name, icon, icondark', + limit = 5000, + }) + + return Table.map(queryData, function(key, tournament) + return tournament.pagename, { + pageName = tournament.pagename, + displayName = Logic.nilIfEmpty(tournament.name) or tournament.pagename:gsub('_', ' '), + icon = tournament.icon, + iconDark = tournament.icondark, + } end) +end + +---@param teams FortniteRankingsTeam[] +---@param weights {count: number, pr: number, cash: number} +---@param weightSum number +function PowerRankingsOrgs._determineScore(teams, weights, weightSum) + local numberOfTeams = #teams + + ---@param key string + local function getRankForKey(key) + if numberOfTeams <= 1 then + return + end + + local sorted = Array.sortBy(teams, Operator.property(key)) - local locByPage = {} - if Logic.isNotEmpty(pages) then - local teamRows = mw.ext.LiquipediaDB.lpdb('team', { - conditions = ConditionUtil.anyOf(ColumnName('pagename'), pages):toString(), - query = 'pagename, location', - limit = 1000, - }) or {} - Array.forEach(teamRows, function(teamRow) - if Logic.isNotEmpty(teamRow.pagename) then - locByPage[teamRow.pagename] = teamRow.location + local i = 1 + while i <= numberOfTeams do + local j = i + while j < numberOfTeams and sorted[j + 1][key] == sorted[i][key] do + j = j + 1 end - end) + local norm = ((i + j) / 2 - 1) / (numberOfTeams - 1) + for k = i, j do + sorted[k][key .. 'Rank'] = norm + end + i = j + 1 + end end - Array.forEach(list, function(o) - local page = pageByTeam[o.team] - o.flag = page and Logic.nilIfEmpty(locByPage[page]) or nil + getRankForKey('count') + getRankForKey('average') + getRankForKey('earnings') + + Array.forEach(teams, function(team, teamIndex) + team.score = 100 * ( + weights.count * (team.countRank or 1) + + weights.pr * (team.averageRank or 1) + + weights.cash * (team.earningsRank or 1) + ) / weightSum + end) +end + +---@param teams FortniteRankingsTeam +function PowerRankingsOrgs._store(teams) + if Lpdb.isStorageDisabled() then return end + Array.forEach(teams, function(team) + mw.ext.LiquipediaDB.lpdb_datapoint('FTN_ORG_PR_' .. team.pageName, { + type = 'FTN_ORG_PR', + name = team.pageName, + information = team.rank, + extradata = {score = MathUtil.formatRounded{value = team.score, precision = 1}}, + }) end) end +---@param teams FortniteRankingsTeam[] +---@param achievementsInfo table +---@param config {wrapped: boolean, updated: string?, year: integer, showMore: integer} +---@return Renderable +function PowerRankingsOrgs._buildDisplay(teams, achievementsInfo, config) + local columns = WidgetUtil.collect( + {align = 'center'}, + {align = 'center'}, + {align = 'left'}, + {align = 'left'}, + not config.wrapped and {align = 'center'} or nil, + {align = 'center'}, + not config.wrapped and {align = 'center'} or nil, + not config.wrapped and {align = 'center'} or nil + ) + + local rows = Array.map(teams, function(team, rank) + return PowerRankingsOrgs._buildRow(rank, team, config.wrapped, achievementsInfo) + end) + + return TableWidgets.Table{ + title = PowerRankingsOrgs._buildTitle(config.updated), + sortable = false, + columns = columns, + footer = config.showMore and Link{ + link = DISPLAY_PAGE, + linktype = 'internal', + children = { + HtmlWidgets.Div{ + children = {'See Rankings Page', Icon.makeIcon{iconName = 'goto'}}, + classes = {'ranking-table__footer-button'}, + }, + }, + } or nil, + css = {width = '100%'}, + children = { + TableWidgets.TableHeader{children = {PowerRankingsOrgs._buildHeader(config.wrapped, config.year)}}, + TableWidgets.TableBody{children = rows}, + }, + } +end + ---@param updated string? ----@return Widget -local function buildTitle(updated) +---@return Renderable +function PowerRankingsOrgs._buildTitle(updated) return HtmlWidgets.Div{children = WidgetUtil.collect( HtmlWidgets.B{children = 'Fortnite Organization Power Rankings'}, Logic.isNotEmpty(updated) and HtmlWidgets.Span{ @@ -222,24 +351,10 @@ local function buildTitle(updated) )} end ----@return Widget -local function buildFooter() - return Link{ - link = DISPLAY_PAGE, - linktype = 'internal', - children = { - HtmlWidgets.Div{ - children = {'See Rankings Page', Icon.makeIcon{iconName = 'goto'}}, - classes = {'ranking-table__footer-button'}, - }, - }, - } -end - ---@param wrapped boolean ---@param year integer ----@return Widget -local function buildHeader(wrapped, year) +---@return Renderable +function PowerRankingsOrgs._buildHeader(wrapped, year) return TableWidgets.Row{children = WidgetUtil.collect( TableWidgets.CellHeader{children = 'Rank'}, TableWidgets.CellHeader{children = ''}, @@ -253,183 +368,47 @@ local function buildHeader(wrapped, year) end ---@param rank integer ----@param o table +---@param team FortniteRankingsTeam ---@param wrapped boolean ----@return Widget -local function buildRow(rank, o, wrapped) - local flagCell = Logic.isNotEmpty(o.flag) and Flags.Icon{flag = o.flag, shouldLink = false} or '' - local memberDisplays = Array.map(Array.sub(o.members, 1, o.count), function(m) - local player = {displayName = m.name, pageName = Logic.nilIfEmpty(m.link) or m.name} - PlayerExt.syncPlayer(player) - return tostring(PlayerDisplay.InlinePlayer{player = player}) +---@param achievementsInfo table +---@return VNode +function PowerRankingsOrgs._buildRow(rank, team, wrapped, achievementsInfo) + local flagCell = Logic.isNotEmpty(team.flag) and Flags.Icon{flag = team.flag, shouldLink = false} or '' + local memberDisplays = Array.map(Array.sub(team.players, 1, team.count), function(player) + return PlayerDisplay.InlinePlayer{player = player} end) - local membersText = table.concat(memberDisplays, ', ') .. ' (' .. o.count .. ')' - local achievementsText = table.concat(Array.map(o.achievements or {}, function(a) + local membersText = Array.append( + Array.interleave(memberDisplays, ', '), + ' (' .. team.count .. ')' + ) + + local achievements = Array.map(team.achievements or {}, function(achievement) + local info = achievementsInfo[achievement] + if not info then + return + end return LeagueIcon.display{ - icon = a.icon, - iconDark = a.icondark, - link = a.page, - name = a.name, + icon = info.icon, + iconDark = info.iconDark, + link = info.pageName, + name = info.displayName, size = 30, options = {noTemplate = true}, } - end)) + end) return TableWidgets.Row{children = WidgetUtil.collect( TableWidgets.Cell{children = HtmlWidgets.B{children = rank}}, TableWidgets.Cell{children = flagCell}, TableWidgets.Cell{children = OpponentDisplay.BlockOpponent{ - opponent = {type = Opponent.team, template = o.team}, + opponent = {type = Opponent.team, template = team.team, extradata = {}}, }}, TableWidgets.Cell{children = membersText}, - not wrapped and TableWidgets.Cell{children = achievementsText} or nil, - TableWidgets.Cell{children = HtmlWidgets.B{children = MathUtil.formatRounded{value = o.score, precision = 1}}}, - not wrapped and TableWidgets.Cell{children = formatNumber(o.avgPR)} or nil, - not wrapped and TableWidgets.Cell{children = '$' .. formatNumber(o.cash)} or nil + not wrapped and TableWidgets.Cell{children = achievements} or nil, + TableWidgets.Cell{children = HtmlWidgets.B{children = MathUtil.formatRounded{value = team.score, precision = 1}}}, + not wrapped and TableWidgets.Cell{children = Currency.formatMoney(team.average, 0)} or nil, + not wrapped and TableWidgets.Cell{children = '$' .. Currency.formatMoney(team.earnings, 0)} or nil )} end ----@param frame Frame ----@return Widget -function PowerRankingsOrgs.main(frame) - local args = Arguments.getArgs(frame) - local limit = tonumber(args.limit) - local showMore = Logic.readBool(args.showMore) - local wrapped = Logic.readBool(args.wrapped) - local year = tonumber(args.year) or DateExt.getYearOf(DateExt.getContextualDateOrNow()) - local weights = { - count = tonumber(args.wCount) or DEFAULT_WEIGHTS.count, - pr = tonumber(args.wPR) or DEFAULT_WEIGHTS.pr, - cash = tonumber(args.wCash) or DEFAULT_WEIGHTS.cash, - } - local weightSum = weights.count + weights.pr + weights.cash - - local updated - if Logic.isNotEmpty(PowerRankingsData.updated) then - updated = PowerRankingsData.updated .. ' ' .. DateExt.defaultTimezone - end - - local players = Array.filter(PowerRankingsData.players or {}, function(pl) - return (tonumber(pl.rank) or 0) <= TOP_N - end) - - local byOrg = {} - Array.forEach(players, function(pl) - local team = Logic.nilIfEmpty(resolvePrimaryTeam(pl.link or pl.name)) - if not team then - return - end - byOrg[team] = byOrg[team] or {} - table.insert(byOrg[team], {name = pl.name, link = pl.link, points = tonumber(pl.points) or 0}) - end) - - local list = {} - for team, members in pairs(byOrg) do - table.sort(members, function(a, b) return a.points > b.points end) - local count = math.min(#members, MAX_PLAYERS_PER_ORG) - local topPoints = Array.map(Array.sub(members, 1, count), function(m) return m.points end) - table.insert(list, { - team = team, - members = members, - count = count, - avgPR = MathUtil.sum(topPoints) / count, - }) - end - - local teamEarnings, teamWinPages, tournamentDetails = gatherPlacementData(year) - Array.forEach(list, function(o) - o.matchKeys = buildMatchKeys(o.team) - o.cash = 0 - for key in pairs(o.matchKeys) do - o.cash = o.cash + (teamEarnings[key] or 0) - end - end) - - local n = #list - local function rankNormalize(getValue, field) - if n <= 1 then - Array.forEach(list, function(o) o[field] = 1 end) - return - end - local sorted = Table.copy(list) - table.sort(sorted, function(a, b) return getValue(a) < getValue(b) end) - local i = 1 - while i <= n do - local j = i - while j < n and getValue(sorted[j + 1]) == getValue(sorted[i]) do j = j + 1 end - local norm = ((i + j) / 2 - 1) / (n - 1) - for k = i, j do sorted[k][field] = norm end - i = j + 1 - end - end - - rankNormalize(function(o) return o.count end, 'nCount') - rankNormalize(function(o) return o.avgPR end, 'nPR') - rankNormalize(function(o) return o.cash end, 'nCash') - - Array.forEach(list, function(o) - o.score = 100 * (weights.count * o.nCount + weights.pr * o.nPR + weights.cash * o.nCash) / weightSum - end) - - table.sort(list, function(a, b) - if a.score ~= b.score then return a.score > b.score end - return a.avgPR > b.avgPR - end) - - if not wrapped then - storeOrgRankings(list) - end - - local display = limit and Array.sub(list, 1, limit) or list - - if not wrapped then - Array.forEach(display, function(o) - local achievements = {} - local seen = {} - for key in pairs(o.matchKeys) do - local pagesForTeam = teamWinPages[key] - if pagesForTeam then - for page in pairs(pagesForTeam) do - if not seen[page] then - seen[page] = true - local d = tournamentDetails[page] or {name = page:gsub('_', ' '), icon = '', icondark = ''} - table.insert(achievements, {page = page, name = d.name, icon = d.icon, icondark = d.icondark}) - end - end - end - end - o.achievements = achievements - end) - end - - resolveOrgFlags(display) - - local columns = WidgetUtil.collect( - {align = 'center'}, - {align = 'center'}, - {align = 'left'}, - {align = 'left'}, - not wrapped and {align = 'center'} or nil, - {align = 'center'}, - not wrapped and {align = 'center'} or nil, - not wrapped and {align = 'center'} or nil - ) - - local rows = Array.map(display, function(o, rank) - return buildRow(rank, o, wrapped) - end) - - return TableWidgets.Table{ - title = buildTitle(updated), - sortable = false, - columns = columns, - footer = showMore and buildFooter() or nil, - css = {width = '100%'}, - children = { - TableWidgets.TableHeader{children = {buildHeader(wrapped, year)}}, - TableWidgets.TableBody{children = rows}, - }, - } -end - return PowerRankingsOrgs From f0bb9e5136210426d910533428e545e28098a02c Mon Sep 17 00:00:00 2001 From: hjpalpha <75081997+hjpalpha@users.noreply.github.com> Date: Thu, 18 Jun 2026 04:37:10 +0200 Subject: [PATCH 23/28] Update lua/wikis/fortnite/PowerRankings/Orgs.lua --- lua/wikis/fortnite/PowerRankings/Orgs.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/wikis/fortnite/PowerRankings/Orgs.lua b/lua/wikis/fortnite/PowerRankings/Orgs.lua index d7d6cfeae2b..07352b0608b 100644 --- a/lua/wikis/fortnite/PowerRankings/Orgs.lua +++ b/lua/wikis/fortnite/PowerRankings/Orgs.lua @@ -182,7 +182,7 @@ function PowerRankingsOrgs._fetchTeamFlags(teams) end) end ----@param byTeam table +---@param byTeam table ---@param historicalToTeam table ---@param year integer ---@return {pageName: string, displayName: string, icon: string?, iconDark: string?}[] From 6cacbbd5ad042cdefb09331c5cb3af53260ae120 Mon Sep 17 00:00:00 2001 From: hjpalpha <75081997+hjpalpha@users.noreply.github.com> Date: Thu, 18 Jun 2026 08:16:36 +0200 Subject: [PATCH 24/28] Apply suggestion from @hjpalpha --- lua/wikis/fortnite/PowerRankings/Orgs.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lua/wikis/fortnite/PowerRankings/Orgs.lua b/lua/wikis/fortnite/PowerRankings/Orgs.lua index 07352b0608b..4fd4415a3fe 100644 --- a/lua/wikis/fortnite/PowerRankings/Orgs.lua +++ b/lua/wikis/fortnite/PowerRankings/Orgs.lua @@ -18,14 +18,13 @@ local Logic = Lua.import('Module:Logic') local Lpdb = Lua.import('Module:Lpdb') local MathUtil = Lua.import('Module:MathUtil') local Operator = Lua.import('Module:Operator') -local Page = Lua.import('Module:Page') -local Table = Lua.import('Module:Table') -local TeamTemplate = Lua.import('Module:TeamTemplate') - local Opponent = Lua.import('Module:Opponent/Custom') local OpponentDisplay = Lua.import('Module:OpponentDisplay/Custom') +local Page = Lua.import('Module:Page') local PlayerDisplay = Lua.import('Module:Player/Display/Custom') local PlayerExt = Lua.import('Module:Player/Ext/Custom') +local Table = Lua.import('Module:Table') +local TeamTemplate = Lua.import('Module:TeamTemplate') local HtmlWidgets = Lua.import('Module:Widget/Html') local Link = Lua.import('Module:Widget/Basic/Link') From 1a7d1a43f2e4fa9b2605cca8fd7f6a2c5c22e0e2 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Thu, 18 Jun 2026 16:45:46 +0100 Subject: [PATCH 25/28] Update Custom.lua --- .../fortnite/Infobox/Person/Player/Custom.lua | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua b/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua index a3d4e15d0ff..ba4ac578b1a 100644 --- a/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua +++ b/lua/wikis/fortnite/Infobox/Person/Player/Custom.lua @@ -9,7 +9,6 @@ local Lua = require('Module:Lua') local Abbreviation = Lua.import('Module:Abbreviation') local ActiveYears = Lua.import('Module:YearsActive') -local Array = Lua.import('Module:Array') local Class = Lua.import('Module:Class') local Region = Lua.import('Module:Region') local Math = Lua.import('Module:MathUtil') @@ -25,25 +24,11 @@ local Cell = Widgets.Cell local Link = Lua.import('Module:Widget/Basic/Link') local CURRENT_YEAR = tonumber(os.date('%Y')) -local POWER_RANKINGS_DATA = Lua.import('Module:PowerRankings/Data', {loadData = true}) +local PowerRankings = Lua.import('Module:PowerRankings') local CustomPlayer = Class.new(Player) local CustomInjector = Class.new(Injector) ----@param pageName string ----@return integer? points ----@return integer? rank -local function fetchPowerRanking(pageName) - local entry = Array.find(POWER_RANKINGS_DATA.players or {}, function(player) - return ((player.link or player.name):gsub(' ', '_')) == (pageName:gsub(' ', '_')) - end) - if not entry then - return - end - - return entry.points, entry.rank -end - ---@param frame Frame ---@return VNode function CustomPlayer.run(frame) @@ -71,8 +56,6 @@ function CustomInjector:parse(id, widgets) currentYearEarnings = '$' .. mw.getContentLanguage():formatNum(currentYearEarnings) end - local prPoints, prRank = fetchPowerRanking(caller.pagename) - return { Cell{name = 'Approx. Winnings ' .. CURRENT_YEAR, children = {currentYearEarnings}}, Cell{name = 'Years active', children = {yearsActive}}, @@ -85,7 +68,7 @@ function CustomInjector:parse(id, widgets) }, Cell{ name = Link{link = 'Fortnite Power Rankings', children = 'Fortnite PR'}, - children = prPoints and prRank and (prPoints .. ' (Rank #' .. prRank .. ')') or nil + children = PowerRankings.queryForInfobox(caller.pagename, 'FTN_PR'), }, } elseif id == 'region' then return {} From 83454ec2b4ccab2563f79e3258e73683e94bf363 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Thu, 18 Jun 2026 16:46:31 +0100 Subject: [PATCH 26/28] Update Custom.lua --- lua/wikis/fortnite/Infobox/Team/Custom.lua | 26 +++------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/lua/wikis/fortnite/Infobox/Team/Custom.lua b/lua/wikis/fortnite/Infobox/Team/Custom.lua index f1960cbeb00..fc23c550885 100644 --- a/lua/wikis/fortnite/Infobox/Team/Custom.lua +++ b/lua/wikis/fortnite/Infobox/Team/Custom.lua @@ -13,12 +13,14 @@ local Info = Lua.import('Module:Info', {loadData = true}) local Lpdb = Lua.import('Module:Lpdb') local Math = Lua.import('Module:MathUtil') local Namespace = Lua.import('Module:Namespace') +local Page = Lua.import('Module:Page') local Table = Lua.import('Module:Table') local Injector = Lua.import('Module:Widget/Injector') local Team = Lua.import('Module:Infobox/Team') local Opponent = Lua.import('Module:Opponent/Custom') +local PowerRankings = Lua.import('Module:PowerRankings') local Widgets = Lua.import('Module:Widget/All') local Cell = Widgets.Cell @@ -41,27 +43,6 @@ local MAXIMUM_NUMBER_OF_PLAYERS_IN_PLACEMENTS = Info.config.defaultMaxPlayersPer local CustomInjector = Class.new(Injector) ----@param team string ----@return string? score ----@return string? rank -local function fetchPowerRanking(team) - local key = string.lower((team or ''):gsub('[%s_]', '')) - local conditions = ConditionTree(BooleanOperator.all):add{ - ConditionNode(ColumnName('type'), Comparator.eq, 'FTN_ORG_PR'), - ConditionNode(ColumnName('name'), Comparator.eq, key), - } - local data = mw.ext.LiquipediaDB.lpdb('datapoint', { - limit = 1, - order = 'date DESC', - conditions = conditions:toString(), - query = 'information, extradata', - })[1] - if not data then - return - end - return (data.extradata or {}).score, data.information -end - ---@param frame Frame ---@return VNode function CustomTeam.run(frame) @@ -84,10 +65,9 @@ function CustomInjector:parse(id, widgets) children = {playerEarnings ~= 0 and ('$' .. mw.getContentLanguage():formatNum(Math.round(playerEarnings))) or nil} }) elseif id == 'custom' then - local prScore, prRank = fetchPowerRanking(self.caller.pagename) table.insert(widgets, Cell{ name = Link{link = 'Fortnite Power Rankings/Organizations', children = 'Fortnite Org PR'}, - children = prScore and prRank and (prScore .. ' (Rank #' .. prRank .. ')') or nil + children = PowerRankings.queryForInfobox(Page.pageifyLink(self.caller.pagename), 'FTN_ORG_PR'), }) end From 4c7f8b8447803ee21effcb776b4dd9b40be2a4d4 Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Thu, 18 Jun 2026 16:47:28 +0100 Subject: [PATCH 27/28] Update PowerRankings.lua --- lua/wikis/fortnite/PowerRankings.lua | 62 +++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/lua/wikis/fortnite/PowerRankings.lua b/lua/wikis/fortnite/PowerRankings.lua index ea825291b2d..f4cff2fda19 100644 --- a/lua/wikis/fortnite/PowerRankings.lua +++ b/lua/wikis/fortnite/PowerRankings.lua @@ -9,21 +9,30 @@ local Lua = require('Module:Lua') local Arguments = Lua.import('Module:Arguments') local Array = Lua.import('Module:Array') +local Currency = Lua.import('Module:Currency') local DateExt = Lua.import('Module:Date/Ext') local Icon = Lua.import('Module:Icon') local Logic = Lua.import('Module:Logic') - +local Lpdb = Lua.import('Module:Lpdb') local Opponent = Lua.import('Module:Opponent/Custom') local OpponentDisplay = Lua.import('Module:OpponentDisplay/Custom') local PlayerDisplay = Lua.import('Module:Player/Display/Custom') local PlayerExt = Lua.import('Module:Player/Ext/Custom') +local PowerRankingsData = Lua.import('Module:PowerRankings/Data', {loadData = true}) + +local Condition = Lua.import('Module:Condition') +local ConditionTree = Condition.Tree +local ConditionNode = Condition.Node +local Comparator = Condition.Comparator +local BooleanOperator = Condition.BooleanOperator +local ColumnName = Condition.ColumnName local HtmlWidgets = Lua.import('Module:Widget/Html') local Link = Lua.import('Module:Widget/Basic/Link') local TableWidgets = Lua.import('Module:Widget/Table2/All') local WidgetUtil = Lua.import('Module:Widget/Util') -local PowerRankingsData = Lua.import('Module:PowerRankings/Data', {loadData = true}) +local PLAYER_DATAPOINT_TYPE = 'FTN_PR' local PowerRankings = {} @@ -74,9 +83,10 @@ function PowerRankings.main(frame) players = Array.sub(players, 1, limit) end - local updated + local updated, updatedIso if Logic.isNotEmpty(PowerRankingsData.updated) then updated = PowerRankingsData.updated .. ' ' .. DateExt.defaultTimezone + updatedIso = DateExt.toYmdInUtc(PowerRankingsData.updated) end local rows = Array.map(players, function(entry) @@ -85,15 +95,18 @@ function PowerRankings.main(frame) pageName = Logic.nilIfEmpty(entry.link) or entry.name, } PlayerExt.syncPlayer(player) - local teamTemplate = PlayerExt.syncTeam(player.pageName) + local teamTemplate = PlayerExt.syncTeam(player.pageName, nil, {date = updatedIso}) + + PowerRankings._store(player, entry) return TableWidgets.Row{children = { TableWidgets.Cell{children = HtmlWidgets.B{children = entry.rank}}, - TableWidgets.Cell{children = HtmlWidgets.B{children = entry.points}}, + TableWidgets.Cell{children = HtmlWidgets.B{children = Currency.formatMoney(entry.points, 0)}}, TableWidgets.Cell{children = PlayerDisplay.BlockPlayer{player = player}}, TableWidgets.Cell{children = teamTemplate and OpponentDisplay.BlockOpponent{opponent = { type = Opponent.team, template = teamTemplate, + extradata = {}, }} or nil}, }} end) @@ -123,4 +136,43 @@ function PowerRankings.main(frame) } end +---@param player standardPlayer +---@param entry {rank: integer, points: number} +function PowerRankings._store(player, entry) + if Lpdb.isStorageDisabled() then return end + mw.ext.LiquipediaDB.lpdb_datapoint(PLAYER_DATAPOINT_TYPE .. '_' .. player.pageName, { + type = PLAYER_DATAPOINT_TYPE, + name = player.pageName, + information = entry.rank, + extradata = {score = entry.points}, + }) +end + +---@param pageName string +---@param datapointType string +---@return string? +function PowerRankings.queryForInfobox(pageName, datapointType) + local conditions = ConditionTree(BooleanOperator.all):add{ + ConditionNode(ColumnName('type'), Comparator.eq, datapointType), + ConditionNode(ColumnName('name'), Comparator.eq, pageName), + } + + local data = mw.ext.LiquipediaDB.lpdb('datapoint', { + limit = 1, + order = 'date DESC', + conditions = tostring(conditions), + query = 'information, extradata', + })[1] + + if not data then return end + + local points = data.extradata.score + local rank = data.information + if not points or not rank then return end + + points = Currency.formatMoney(points, datapointType == PLAYER_DATAPOINT_TYPE and 0 or 1) + + return points .. ' (Rank #' .. rank .. ')' +end + return PowerRankings From 80e46da32be51b568034e8bfa0bc1326258a843a Mon Sep 17 00:00:00 2001 From: Mehdi9120310391 Date: Thu, 18 Jun 2026 16:48:04 +0100 Subject: [PATCH 28/28] Update Orgs.lua --- lua/wikis/fortnite/PowerRankings/Orgs.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/wikis/fortnite/PowerRankings/Orgs.lua b/lua/wikis/fortnite/PowerRankings/Orgs.lua index 4fd4415a3fe..f0cc0c7807f 100644 --- a/lua/wikis/fortnite/PowerRankings/Orgs.lua +++ b/lua/wikis/fortnite/PowerRankings/Orgs.lua @@ -148,6 +148,10 @@ function PowerRankingsOrgs.main(frame) return a.average > b.average end) + Array.forEach(teams, function(team, rank) + team.rank = rank + end) + if not config.wrapped then PowerRankingsOrgs._store(teams) end