diff --git a/components/auto_inline_icon/commons/auto_inline_icon.lua b/components/auto_inline_icon/commons/auto_inline_icon.lua new file mode 100644 index 00000000000..aa663d38aac --- /dev/null +++ b/components/auto_inline_icon/commons/auto_inline_icon.lua @@ -0,0 +1,111 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:AutoInlineIcon +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute + +local Class = require('Module:Class') +local Lua = require('Module:Lua') +local InlineIconAndText = require('Module:Widget/InlineIconAndText') +local ManualData = Lua.requireIfExists('Module:InlineIcon/ManualData', {loadData = true}) + +local AutoInlineIcon = {} + +---@param _ table +---@param category string +---@param lookup string +---@param extraInfo string? +---@return string +function AutoInlineIcon.display(_, category, lookup, extraInfo) + assert(category, 'Category parameter is required.') + assert(lookup, 'Lookup parameter is required.') + + local data = AutoInlineIcon._getDataRetrevalFunction(category)(lookup) + assert(data, 'Data not found.') + + local icon = AutoInlineIcon._iconCreator(data) + + if not data.text then + return tostring(icon) + end + + return tostring(InlineIconAndText{ + icon = icon, + text = data.text, + link = data.link, + }) +end + +---@param category string +---@return fun(name: string): table +function AutoInlineIcon._getDataRetrevalFunction(category) + local categoryMapper = { + H = AutoInlineIcon._queryHeroData, + A = function(name) + error('Abilities not yet implemented.') + end, + I = AutoInlineIcon._queryItemData, + M = function(name) + return ManualData[name] + end, + } + assert(categoryMapper[category], 'Invalid category parameter.') + return categoryMapper[category] +end + +---@param data table +---@return IconWidget +function AutoInlineIcon._iconCreator(data) + if data.iconType == 'image' then + local IconImage = require('Module:Widget/Icon/Image') + return IconImage{ + imageLight = data.iconLight, + imageDark = data.iconDark, + link = data.link, + } + elseif data.iconType == 'fa' then + local IconFa = require('Module:Widget/Icon/Fontawesome') + return IconFa{ + iconName = data.icon, + link = data.link, + } + end + error('Invalid iconType.') +end + +---@param name string +---@return table +function AutoInlineIcon._queryItemData(name) + local data = mw.ext.LiquipediaDB.lpdb('datapoint', { + conditions = '[[type::item]] AND [[name::'.. name ..']]', + })[1] + assert(data, 'Item not found.') + + return { + iconType = 'image', + link = data.pagename, + text = data.name, + iconLight = data.image, + iconDark = data.imagedark, + } +end + +---@param name string +---@return table +function AutoInlineIcon._queryHeroData(name) + local data = mw.ext.LiquipediaDB.lpdb('datapoint', { + conditions = '[[type::character]] AND [[name::'.. name ..']]', + })[1] + assert(data, 'Hero not found.') + + return { + iconType = 'image', + link = data.pagename, + text = data.name, + iconLight = data.extradata.icon or data.image, + iconDark = data.extradata.icon or data.imagedark, + } +end + +return Class.export(AutoInlineIcon) diff --git a/components/character_icon/commons/character_icon.lua b/components/character_icon/commons/character_icon.lua index bec131423ad..84ac56b83cd 100644 --- a/components/character_icon/commons/character_icon.lua +++ b/components/character_icon/commons/character_icon.lua @@ -79,7 +79,7 @@ function CharacterIcon.Icon(args) return CharacterIcon._makeImage(iconInfo, args.size, args.class) .. (Logic.readBool(args.addTextLink) - and (' ' .. Page.makeInternalLink(args.character, iconInfo.link or args.character)) + and (' ' .. Page.makeInternalLink(iconInfo.display or args.character, iconInfo.link or args.character)) or '') end diff --git a/components/external_media_links/external_media_link.lua b/components/external_media_links/external_media_link.lua index 4a779af2d67..5045ff2fd85 100644 --- a/components/external_media_links/external_media_link.lua +++ b/components/external_media_links/external_media_link.lua @@ -251,6 +251,7 @@ function ExternalMediaLink.wrapper(args) language = args.language, translation = args.translation, translator = args.translator, + trans_title = args.trans_title, } if args.authors then diff --git a/components/external_media_links/external_media_list.lua b/components/external_media_links/external_media_list.lua index 6ad8d781bec..a6b7e3cebe3 100644 --- a/components/external_media_links/external_media_list.lua +++ b/components/external_media_links/external_media_list.lua @@ -220,7 +220,7 @@ function MediaList._row(item, args) row:node(MediaList._displayTitle(item)) if String.isNotEmpty(item.translatedtitle) then - row:wikitext(mw.text.nowiki('[') .. '[' .. item.translatedtitle .. ']' .. mw.text.nowiki(']')) + row:wikitext(' ' .. mw.text.nowiki('[') .. item.translatedtitle .. mw.text.nowiki(']')) end local authors = {} diff --git a/components/faction/wikis/ageofempires/faction_data.lua b/components/faction/wikis/ageofempires/faction_data.lua index 3a3dfd4406e..6f68e28a481 100644 --- a/components/faction/wikis/ageofempires/faction_data.lua +++ b/components/faction/wikis/ageofempires/faction_data.lua @@ -621,108 +621,113 @@ local factionPropsAoM = { pageName = 'Egyptians' .. AOM_SUFFIX, faction = 'egyptians', }, - norse = { + freyr = { index = 5, + name = 'Freyr', + faction = 'freyr', + }, + norse = { + index = 6, name = 'Norse', pageName = 'Norse' .. AOM_SUFFIX, faction = 'norse', }, fuxi = { - index = 6, + index = 7, name = 'Fu Xi', faction = 'fuxi', }, gaia = { - index = 7, + index = 8, name = 'Gaia', pageName = 'Gaia', faction = 'gaia', }, hades = { - index = 8, + index = 9, name = 'Hades', faction = 'hades', }, isis = { - index = 9, + index = 10, name = 'Isis', faction = 'isis', }, kronos = { - index = 10, + index = 11, name = 'Kronos', faction = 'kronos', }, loki = { - index = 11, + index = 12, name = 'Loki', faction = 'loki', }, nuwa = { - index = 12, + index = 13, name = 'Nü Wa', faction = 'nuwa', }, odin = { - index = 13, + index = 14, name = 'Odin', faction = 'odin', }, oranos = { - index = 14, + index = 15, name = 'Oranos', faction = 'oranos', }, poseidon = { - index = 15, + index = 16, name = 'Poseidon', pageName = 'Poseidon (god)', faction = 'poseidon', }, ra = { - index = 16, + index = 17, name = 'Ra', pageName = 'Ra (god)', faction = 'ra', }, set = { - index = 17, + index = 18, name = 'Set', faction = 'set', }, shennong = { - index = 18, + index = 19, name = 'Shennong', faction = 'shennong', }, thor = { - index = 19, + index = 20, name = 'Thor', faction = 'thor', }, zeus = { - index = 20, + index = 21, name = 'Zeus', faction = 'zeus', }, quetzalcoatl = { - index = 21, + index = 22, name = 'Quetzalcoatl', faction = 'quetzalcoatl', }, huitzilopochtli = { - index = 22, + index = 23, name = 'Huitzilopochtli', faction = 'huitzilopochtli', }, tezcatlipoca = { - index = 23, + index = 24, name = 'Tezcatlipoca', faction = 'tezcatlipoca', }, unknown = { - index = 24, + index = 25, name = 'Unknown', faction = 'unknown', }, @@ -935,9 +940,23 @@ return { atl = 'atlanteans', chi = 'chinese', egy = 'egyptians', + fre = 'freyr', + fux = 'fuxi', + gai = 'gaia', gre = 'greeks', + had = 'hades', + isi = 'isis', + kro = 'kronos', + lok = 'loki', nor = 'norse', ['nu wa'] = 'nuwa', + nuw = 'nuwa', + odi = 'odin', + ora = 'oranos', + pos = 'poseidon', + she = 'shennong', + tho = 'thor', + zeu = 'zeus', que = 'quetzalcoatl', hui = 'huitzilopochtli', tez = 'tezcatlipoca', diff --git a/components/faction/wikis/ageofempires/faction_icon_data.lua b/components/faction/wikis/ageofempires/faction_icon_data.lua index aa6095ce7b0..f85fa1f0666 100644 --- a/components/faction/wikis/ageofempires/faction_icon_data.lua +++ b/components/faction/wikis/ageofempires/faction_icon_data.lua @@ -345,61 +345,64 @@ local byFactionAoM = { icon = 'File:Chinese AoM icon.png', }, greeks = { - icon = 'File:Greek AoM icon.png', + icon = 'File:Greek AoM R icon.png', }, atlanteans = { - icon = 'File:Atlantean AoM icon.png', + icon = 'File:Atlantean AoM R icon.png', }, egyptians = { - icon = 'File:Egyptian AoM icon.png', + icon = 'File:Egyptian AoM R icon.png', }, norse = { - icon = 'File:Norse AoM icon.png', + icon = 'File:Norse AoM R icon.png', }, fuxi = { icon = 'File:Fu Xi AoM icon.png', }, + freyr = { + icon = 'File:Freyr AoM R icon.png', + }, gaia = { - icon = 'File:Gaia AoM icon.png', + icon = 'File:Gaia AoM R icon.png', }, hades = { - icon = 'File:Hades AoM icon.png', + icon = 'File:Hades AoM R icon.png', }, isis = { - icon = 'File:Isis AoM icon.png', + icon = 'File:Isis AoM R icon.png', }, kronos = { - icon = 'File:Kronos AoM icon.png', + icon = 'File:Kronos AoM R icon.png', }, loki = { - icon = 'File:Loki AoM icon.png', + icon = 'File:Loki AoM R icon.png', }, nuwa = { icon = 'File:Nu Wa AoM icon.png', }, odin = { - icon = 'File:Odin AoM icon.png', + icon = 'File:Odin AoM R icon.png', }, oranos = { - icon = 'File:Oranos AoM icon.png', + icon = 'File:Oranos AoM R icon.png', }, poseidon = { - icon = 'File:Poseidon AoM icon.png', + icon = 'File:Poseidon AoM R icon.png', }, ra = { - icon = 'File:Ra AoM icon.png', + icon = 'File:Ra AoM R icon.png', }, set = { - icon = 'File:Set AoM icon.png', + icon = 'File:Set AoM R icon.png', }, shennong = { icon = 'File:Shennong AoM icon.png', }, thor = { - icon = 'File:Thor AoM icon.png', + icon = 'File:Thor AoM R icon.png', }, zeus = { - icon = 'File:Zeus AoM icon.png', + icon = 'File:Zeus AoM R icon.png', }, quetzalcoatl = { icon = 'File:Quetzalcoatl AoM icon.png', diff --git a/components/game_table/commons/game_table.lua b/components/game_table/commons/game_table.lua index 82446e9741a..d627990b15f 100644 --- a/components/game_table/commons/game_table.lua +++ b/components/game_table/commons/game_table.lua @@ -11,12 +11,11 @@ local Class = require('Module:Class') local Game = require('Module:Game') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local Table = require('Module:Table') local VodLink = require('Module:VodLink') local MatchTable = Lua.import('Module:MatchTable') -local NP_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} +local NOT_PLAYED = 'np' local SCORE_CONCAT = ' : ' ---@class GameTableMatch: MatchTableMatch @@ -32,7 +31,7 @@ end) ---@return match2game? function GameTable:gameFromRecord(game) if self.countGames == self.config.limit then return nil end - if Table.includes(NP_STATUSES, game.resulttype) then + if game.resulttype == NOT_PLAYED or Logic.isEmpty(game.winner) then return nil end @@ -103,7 +102,7 @@ end ---@param match GameTableMatch ---@param game match2game ---@return Html? -function GameTable:_displayGame(match, game) +function GameTable:displayGame(match, game) if not self.config.showResult then return elseif Logic.isEmpty(match.result.vs) then @@ -130,7 +129,7 @@ function GameTable:gameRow(match, game) :node(self:_displayGameIconForGame(game)) :node(self:_displayIcon(match)) :node(self:_displayTournament(match)) - :node(self:_displayGame(match, game)) + :node(self:displayGame(match, game)) :node(self:_displayGameVod(game.vod)) end diff --git a/components/game_table/commons/game_table_character.lua b/components/game_table/commons/game_table_character.lua index d5798c7163d..500d35d89ca 100644 --- a/components/game_table/commons/game_table_character.lua +++ b/components/game_table/commons/game_table_character.lua @@ -41,13 +41,16 @@ local SCORE_CONCAT = ' : ' ---@field picks string[][] ---@field bans string[][]? ---@field pickedBy number? +---@field pickedByplayer number? ---@class CharacterGameTable: GameTable ---@field character string ---@field isCharacterTable boolean +---@field isPickedByRequired boolean ---@field config CharacterGameTableConfig local CharacterGameTable = Class.new(GameTable, function (self) self.isCharacterTable = self.args.tableMode == CHARACTER_MODE + self.isPickedByRequired = self.isCharacterTable if not self.isCharacterTable then self.resultFromRecord = GameTable.resultFromRecord @@ -193,17 +196,23 @@ function CharacterGameTable:getCharacters(game, maxNumber, keyMaker) end ----@param picks string[][] +---@param game CharacterGameTableGame ---@return number? -function CharacterGameTable:_getCharacterPick(picks) +function CharacterGameTable:getCharacterPick(game) ---@param opponentIndex number ---@return number? local findCharacter = function (opponentIndex) - local found = Array.find(picks[opponentIndex], function (character) + local found = Array.indexOf(game.picks[opponentIndex], function (character) return character == self.character end) + game.pickedByplayer = found > 0 and found or nil - return found and opponentIndex or nil + return found > 0 and opponentIndex or nil + end + + local winner = tonumber(game.winner) + if winner ~= 0 then + return findCharacter(winner) or findCharacter(winner == 1 and 2 or 1) end return findCharacter(1) or findCharacter(2) end @@ -220,9 +229,9 @@ function CharacterGameTable:gameFromRecord(game) gameRecord.picks = self:getCharacters(gameRecord, self.config.numPicks, self.getCharacterKey) gameRecord.bans = self.config.showBans and self:getCharacters(gameRecord, self.config.numBans,self.getCharacterBanKey) or nil - gameRecord.pickedBy = self.isCharacterTable and self:_getCharacterPick(gameRecord.picks) or nil + gameRecord.pickedBy = self.isPickedByRequired and self:getCharacterPick(gameRecord) or nil - if self.isCharacterTable then + if self.isPickedByRequired then return Logic.isNotEmpty(gameRecord.pickedBy) and gameRecord or nil end @@ -330,7 +339,7 @@ end ---@param match GameTableMatch ---@param game CharacterGameTableGame ---@return Html? -function CharacterGameTable:_displayGame(match, game) +function CharacterGameTable:displayGame(match, game) if not self.config.showResult then return end @@ -415,7 +424,7 @@ function CharacterGameTable:gameRow(match, game) :node(self:_displayGameIconForGame(game)) :node(self:_displayIcon(match)) :node(self:_displayTournament(match)) - :node(self:_displayGame(match, game)) + :node(self:displayGame(match, game)) :node(self:_displayLength(game)) :node(self:_displayGameVod(game.vod)) end diff --git a/components/game_table/dota2/game_table_character_custom.lua b/components/game_table/dota2/game_table_character_custom.lua index c7c6281af64..b47e8563320 100644 --- a/components/game_table/dota2/game_table_character_custom.lua +++ b/components/game_table/dota2/game_table_character_custom.lua @@ -12,26 +12,27 @@ local Lua = require('Module:Lua') local GameTableCharacter = Lua.import('Module:GameTable/Character') -local CustomGameTableCharacter = Class.new(GameTableCharacter) +---@class Dota2CharacterGameTable: CharacterGameTable +local CustomCharacterGameTable = Class.new(GameTableCharacter) ---@return integer -function CustomGameTableCharacter:getNumberOfBans() +function CustomCharacterGameTable:getNumberOfBans() return 7 end ---@param opponentIndex number ---@param playerIndex number ---@return string -function CustomGameTableCharacter:getCharacterKey(opponentIndex, playerIndex) +function CustomCharacterGameTable:getCharacterKey(opponentIndex, playerIndex) return 'team' .. opponentIndex .. 'hero' .. playerIndex end ---@param frame Frame ---@return Html -function CustomGameTableCharacter.results(frame) +function CustomCharacterGameTable.results(frame) local args = Arguments.getArgs(frame) - return CustomGameTableCharacter(args):readConfig():query():build() + return CustomCharacterGameTable(args):readConfig():query():build() end -return CustomGameTableCharacter +return CustomCharacterGameTable diff --git a/components/game_table/valorant/game_table_character_custom.lua b/components/game_table/valorant/game_table_character_custom.lua new file mode 100644 index 00000000000..19f0b79dee6 --- /dev/null +++ b/components/game_table/valorant/game_table_character_custom.lua @@ -0,0 +1,174 @@ +--- +-- @Liquipedia +-- wiki=valorant +-- page=Module:GameTable/Character/Custom +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Arguments = require('Module:Arguments') +local Array = require('Module:Array') +local CharacterIcon = require('Module:CharacterIcon') +local Class = require('Module:Class') +local Lua = require('Module:Lua') +local MathUtil = require('Module:MathUtil') +local Page = require('Module:Page') +local Table = require('Module:Table') + +local OpponentLibraries = require('Module:OpponentLibraries') +local Opponent = OpponentLibraries.Opponent + +local CharacterGameTable = Lua.import('Module:GameTable/Character') + +---@class ValorantCharacterGameTable: CharacterGameTable +local CustomCharacterGameTable = Class.new(CharacterGameTable, function (self) + self.args.showBans = false + + if self.args.tableMode == Opponent.solo then + self.isPickedByRequired = true + self.args.showOnlyGameStats = true + self.statsFromMatches = CharacterGameTable.statsFromMatches + end +end) + +---@param game CharacterGameTableGame +---@return number? +function CustomCharacterGameTable:getCharacterPick(game) + if self.config.mode ~= Opponent.solo then + return CharacterGameTable.getCharacterPick(self, game) + end + local aliases = self.config.aliases + local found + Table.iter.forEachPair(game.participants, function (participantId, participant) + if found then return end + if aliases[participant.player] then + local pKey = Array.parseCommaSeparatedString(participantId, '_') + game.pickedByplayer = tonumber(pKey[2]) + found = tonumber(pKey[1]) + return + end + end) + + return found +end + +---@return integer +function CustomCharacterGameTable:getNumberOfPicks() + return 10 +end + +---@param opponentIndex number +---@param playerIndex number +---@return string +function CustomCharacterGameTable:getCharacterKey(opponentIndex, playerIndex) + return 't' .. opponentIndex .. 'p' .. playerIndex .. 'agent' +end + +---@return Html +function CustomCharacterGameTable:headerRow() + local makeHeaderCell = function(text, width) + return mw.html.create('th'):css('max-width', width):node(text) + end + + local config = self.config + + local nodes = Array.append({}, + makeHeaderCell('Date', '100px'), + config.showTier and makeHeaderCell('Tier', '70px') or nil, + config.showType and makeHeaderCell('Type', '70px') or nil, + config.showIcon and makeHeaderCell(nil, '25px'):addClass('unsortable') or nil, + makeHeaderCell('Tournament'), + makeHeaderCell('Map'), + (config.showResult and config.mode == Opponent.solo) and makeHeaderCell('') or nil, + (config.showResult and config.mode ~= Opponent.team) and makeHeaderCell('K') or nil, + (config.showResult and config.mode ~= Opponent.team) and makeHeaderCell('D') or nil, + (config.showResult and config.mode ~= Opponent.team) and makeHeaderCell('A') or nil, + (config.showResult and config.mode ~= Opponent.team) and makeHeaderCell('Ratio') or nil, + config.showResult and makeHeaderCell('Picks'):addClass('unsortable') or nil, + config.showResult and makeHeaderCell(nil, '80px') or nil, + config.showResult and makeHeaderCell('Score') or nil, + config.showResult and makeHeaderCell(nil, '80px') or nil, + config.showResult and makeHeaderCell('vs. Picks'):addClass('unsortable') or nil, + config.showLength and makeHeaderCell('Length') or nil, + config.showVod and makeHeaderCell('VOD', '60px') or nil + ) + + local header = mw.html.create('tr') + Array.forEach(nodes, function (node) + header:node(node) + end) + + return header +end + +---@param participant table +---@return number? +function CustomCharacterGameTable:_getRatio(participant) + local kills = tonumber(participant.kills) or 0 + local deaths = tonumber(participant.deaths) or 0 + if deaths == 0 then + return nil + end + + return MathUtil.round(kills / deaths, 1) +end + +---@param match GameTableMatch +---@param game CharacterGameTableGame +---@return Html? +function CustomCharacterGameTable:displayGame(match, game) + local makeCell = function (text) + return mw.html.create('td'):node(text) + end + + local makeIcon = function (character) + if not character then return nil end + return mw.html.create('td') + :node(CharacterIcon.Icon{character = character, size = self.config.iconSize, date = game.date}) + end + + local opponent = match.result.opponent + local opponentVs = match.result.vs + if self.isCharacterTable then + local pickedBy = game.pickedBy + ---@cast pickedBy -nil + if pickedBy == 2 then + opponent, opponentVs = opponentVs, opponent + end + end + + local node = mw.html.create() + :node(makeCell(Page.makeInternalLink(game.map))) + + if self.config.mode ~= Opponent.team then + local participant = game.participants[game.pickedBy .. '_' .. game.pickedByplayer] + if self.config.mode == Opponent.solo then + local index = Array.indexOf(game.picks[game.pickedBy], function (pick) + return participant.agent == pick + end) + node:node(index > 0 and makeIcon(table.remove(game.picks[game.pickedBy], index)) or makeCell()) + end + node + :node(makeCell(participant and participant.kills or nil)) + :node(makeCell(participant and participant.deaths or nil)) + :node(makeCell(participant and participant.assists or nil)) + :node(makeCell(participant and self:_getRatio(participant) or nil)) + end + + return node + :node(self:_displayCharacters(game, opponent.id, 'picks')) + :node(self:_displayOpponent(opponent, false)) + :node(self:_displayScore(game, opponent.id, opponentVs.id)) + :node(self:_displayOpponent(opponentVs, true)) + :node(self:_displayCharacters(game, opponentVs.id, 'picks')) +end + +---@param frame Frame +---@return Html +function CustomCharacterGameTable.results(frame) + local args = Arguments.getArgs(frame) + + return CustomCharacterGameTable(args):readConfig():query():build() +end + +return CustomCharacterGameTable diff --git a/components/hidden_data_box/wikis/arenaofvalor/hidden_data_box_custom.lua b/components/hidden_data_box/wikis/honorofkings/hidden_data_box_custom.lua similarity index 98% rename from components/hidden_data_box/wikis/arenaofvalor/hidden_data_box_custom.lua rename to components/hidden_data_box/wikis/honorofkings/hidden_data_box_custom.lua index fe4828a0695..6b3124b6085 100644 --- a/components/hidden_data_box/wikis/arenaofvalor/hidden_data_box_custom.lua +++ b/components/hidden_data_box/wikis/honorofkings/hidden_data_box_custom.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- wiki=arenaofvalor +-- wiki=honorofkings -- page=Module:HiddenDataBox/Custom -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute diff --git a/components/infobox/commons/custom/infobox_person_user_custom.lua b/components/infobox/commons/custom/infobox_person_user_custom.lua index bb439f1192d..ef06792d6ab 100644 --- a/components/infobox/commons/custom/infobox_person_user_custom.lua +++ b/components/infobox/commons/custom/infobox_person_user_custom.lua @@ -14,7 +14,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local User = Lua.import('Module:Infobox/Person/User') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -51,8 +51,8 @@ function CustomInjector:parse(id, widgets) ) if not String.isEmpty(args['fav-team-1']) then Array.appendWith(widgets, - Title{name = 'Favorite Teams'}, - Center{content = {self.caller:_getFavouriteTeams()}} + Title{children = 'Favorite Teams'}, + Center{children = {self.caller:_getFavouriteTeams()}} ) end elseif @@ -60,9 +60,9 @@ function CustomInjector:parse(id, widgets) not (String.isEmpty(args.team_history) and String.isEmpty(args.clan_history)) then return { - Title{ name = 'History' }, - Center{content = {args.team_history}}, - Center{content = {args.clan_history}}, + Title{children = 'History' }, + Center{children = {args.team_history}}, + Center{children = {args.clan_history}}, } end diff --git a/components/infobox/commons/infobox.lua b/components/infobox/commons/infobox.lua deleted file mode 100644 index dfe0da3fb6d..00000000000 --- a/components/infobox/commons/infobox.lua +++ /dev/null @@ -1,110 +0,0 @@ ---- --- @Liquipedia --- wiki=commons --- page=Module:Infobox --- --- Please see https://github.com/Liquipedia/Lua-Modules to contribute --- - -local Array = require('Module:Array') -local Class = require('Module:Class') -local Logic = require('Module:Logic') -local Lua = require('Module:Lua') -local Variables = require('Module:Variables') -local WarningBox = require('Module:WarningBox') - -local WidgetFactory = Lua.import('Module:Widget/Factory') - ----@class Infobox ----@field frame Frame? ----@field root Html? ----@field adbox Html? ----@field content Html? ----@field warnings string[] ----@field injector WidgetInjector? -local Infobox = Class.new() - ---- Inits the Infobox instance ----@param frame Frame ----@param gameName string ----@param forceDarkMode boolean? ----@return self -function Infobox:create(frame, gameName, forceDarkMode) - self.frame = frame - self.root = mw.html.create('div') - :addClass('fo-nttax-infobox-wrapper') - :addClass('infobox-' .. gameName:lower()) - self.adbox = mw.html.create('div') - :addClass('fo-nttax-infobox-adbox') - :node(mw.getCurrentFrame():preprocess('')) - self.content = mw.html.create('div') - :addClass('fo-nttax-infobox') - if forceDarkMode then - self.root:addClass('infobox-darkmodeforced') - end - - self.injector = nil - self.warnings = {} - - return self -end - ----Adds categories ----@param ... string? ----@return self -function Infobox:categories(...) - Array.forEach({...}, function(cat) return mw.ext.TeamLiquidIntegration.add_category(cat) end) - return self -end - ----Sets the widgetInjector ----@param injector WidgetInjector? ----@return self -function Infobox:widgetInjector(injector) - self.injector = injector - return self -end - ----Adds custom components after the end the infobox ----@param wikitext string|number|Html|nil ----@return self -function Infobox:bottom(wikitext) - if Logic.isEmpty(wikitext) then - return self - end - - self.bottomContent = (self.bottomContent or mw.html.create()):node(wikitext) - return self -end - ---- Returns completed infobox ----@param widgets Widget[] ----@return Html -function Infobox:build(widgets) - for _, widget in ipairs(widgets) do - if widget == nil or widget['is_a'] == nil then - error('Infobox:build can only accept Widgets') - end - - local contentItems = WidgetFactory.work(widget, self.injector) - - for _, node in ipairs(contentItems or {}) do - self.content:node(node) - end - end - - self.root:node(self.content) - - local isFirstInfobox = Variables.varDefault('is_first_infobox', true) - if isFirstInfobox == true then - self.root:node(self.adbox) - Variables.varDefine('is_first_infobox', 'false') - end - self.root:node(self.bottomContent) - - return mw.html.create() - :node(self.root) - :node(WarningBox.displayAll(self.warnings)) -end - -return Infobox diff --git a/components/infobox/commons/infobox_basic.lua b/components/infobox/commons/infobox_basic.lua index 4f74bf0af66..64fd42a6974 100644 --- a/components/infobox/commons/infobox_basic.lua +++ b/components/infobox/commons/infobox_basic.lua @@ -7,13 +7,14 @@ -- local Arguments = require('Module:Arguments') +local Array = require('Module:Array') local Class = require('Module:Class') local Lua = require('Module:Lua') local Logic = require('Module:Logic') local Table = require('Module:Table') local Info = Lua.import('Module:Info') -local Infobox = Lua.import('Module:Infobox') +local Infobox = Lua.import('Module:Widget/Infobox/Core') ---@class BasicInfobox ---@operator call(Frame): BasicInfobox @@ -21,22 +22,41 @@ local Infobox = Lua.import('Module:Infobox') ---@field pagename string ---@field name string ---@field wiki string ----@field infobox Infobox +---@field injector WidgetInjector? +---@field warnings string[] +---@field bottomContent string[] local BasicInfobox = Class.new( function(self, frame) self.args = Arguments.getArgs(frame) self.pagename = mw.title.getCurrentTitle().text self.name = self.args.name or self.pagename self.wiki = self.args.wiki or Info.wikiName - - self.infobox = Infobox:create(frame, self.wiki, Logic.readBool(self.args.darkmodeforced)) + self.bottomContent = {} + self.warnings = {} + self.injector = nil end ) +---Adds categories +---@param ... string? +---@return self +function BasicInfobox:categories(...) + Array.forEach({...}, function(cat) return mw.ext.TeamLiquidIntegration.add_category(cat) end) + return self +end + +---Adds bottom content +---@param content string|number|Html|nil +---@return self +function BasicInfobox:bottom(content) + table.insert(self.bottomContent, content) + return self +end + ---@param injector WidgetInjector? ---@return self function BasicInfobox:setWidgetInjector(injector) - self.infobox:widgetInjector(injector) + self.injector = injector return self end @@ -79,4 +99,16 @@ function BasicInfobox:getAllArgsForBase(args, base, options) return foundArgs end +---@param widgets Widget[] +---@return string +function BasicInfobox:build(widgets) + return Infobox{ + gameName = self.wiki, + forceDarkMode = Logic.readBool(self.args.darkmodeforced), + bottomContent = self.bottomContent, + warnings = self.warnings, + children = widgets, + }:tryMake(self.injector) or '' +end + return BasicInfobox diff --git a/components/infobox/commons/infobox_building.lua b/components/infobox/commons/infobox_building.lua index 7fb6f342550..3a50d63960c 100644 --- a/components/infobox/commons/infobox_building.lua +++ b/components/infobox/commons/infobox_building.lua @@ -14,7 +14,7 @@ local String = require('Module:StringUtils') local BasicInfobox = Lua.import('Module:Infobox/Basic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -33,9 +33,8 @@ function Building.run(frame) end ---creates the infobox ----@return Html +---@return string function Building:createInfobox() - local infobox = self.infobox local args = self.args local widgets = { @@ -45,10 +44,11 @@ function Building:createInfobox() imageDefault = args.default, imageDark = args.imagedark or args.imagedarkmode, imageDefaultDark = args.defaultdark or args.defaultdarkmode, + subHeader = self:subHeaderDisplay(args), size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = (args.informationType or 'Building') .. ' Information'}, + Center{children = {args.caption}}, + Title{children = (args.informationType or 'Building') .. ' Information'}, Cell{name = 'Built by', content = {args.builtby}}, Customizable{ id = 'cost', @@ -93,17 +93,17 @@ function Building:createInfobox() } }, Customizable{id = 'custom', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } - infobox:categories('Buildings') - infobox:categories(unpack(self:getWikiCategories(args))) + self:categories('Buildings') + self:categories(unpack(self:getWikiCategories(args))) if Namespace.isMain() then self:setLpdbData(args) end - return infobox:build(widgets) + return self:build(widgets) end ---@param args table @@ -137,4 +137,11 @@ end function Building:setLpdbData(args) end +--- Allows for overriding this functionality +---@param args table +---@return string? +function Building:subHeaderDisplay(args) + return args.title +end + return Building diff --git a/components/infobox/commons/infobox_campaign.lua b/components/infobox/commons/infobox_campaign.lua index c26eb0d846e..80787ae095c 100644 --- a/components/infobox/commons/infobox_campaign.lua +++ b/components/infobox/commons/infobox_campaign.lua @@ -11,7 +11,7 @@ local Lua = require('Module:Lua') local BasicInfobox = Lua.import('Module:Infobox/Basic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Header = Widgets.Header local Center = Widgets.Center @@ -25,9 +25,8 @@ function Campaign.run(frame) return campaign:createInfobox() end ----@return Html +---@return string function Campaign:createInfobox() - local infobox = self.infobox local args = self.args local widgets = { @@ -37,12 +36,12 @@ function Campaign:createInfobox() imageDark = args.imagedark or args.imagedarkmode, size = args.imagesize, }, - Center{content = {args.caption}}, + Center{children = {args.caption}}, } - infobox:categories('Campaign') + self:categories('Campaign') - return infobox:build(widgets) + return self:build(widgets) end diff --git a/components/infobox/commons/infobox_campaign_mission.lua b/components/infobox/commons/infobox_campaign_mission.lua index eb416dfea5d..49347193b3e 100644 --- a/components/infobox/commons/infobox_campaign_mission.lua +++ b/components/infobox/commons/infobox_campaign_mission.lua @@ -12,7 +12,7 @@ local Namespace = require('Module:Namespace') local BasicInfobox = Lua.import('Module:Infobox/Basic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Header = Widgets.Header local Title = Widgets.Title local Center = Widgets.Center @@ -30,9 +30,8 @@ function Mission.run(frame) return mission:createInfobox() end ----@return Html +---@return string function Mission:createInfobox() - local infobox = self.infobox local args = self.args local widgets = { @@ -45,23 +44,23 @@ function Mission:createInfobox() }, } }, - Center{content = {args.caption}}, - Title{name = 'Mission Information'}, + Center{children = {args.caption}}, + Title{children = 'Mission Information'}, Breakdown{ - content = {'Mission Objective'}, + children = {'Mission Objective'}, classes = {'infobox-header', 'wiki-backgroundcolor-light', 'infobox-header-3'} }, - Breakdown{content = { args.objective }}, + Breakdown{children = { args.objective }}, Customizable{id = 'custom', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } if Namespace.isMain() then - infobox:categories('Missions', 'Campaign') - infobox:categories(unpack(self:getWikiCategories(args))) + self:categories('Missions', 'Campaign') + self:categories(unpack(self:getWikiCategories(args))) end - return infobox:build(widgets) + return self:build(widgets) end return Mission diff --git a/components/infobox/commons/infobox_character.lua b/components/infobox/commons/infobox_character.lua index 44e60717505..aa469269854 100644 --- a/components/infobox/commons/infobox_character.lua +++ b/components/infobox/commons/infobox_character.lua @@ -14,7 +14,7 @@ local Namespace = require('Module:Namespace') local BasicInfobox = Lua.import('Module:Infobox/Basic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -31,9 +31,8 @@ function Character.run(frame) return character:createInfobox() end ----@return Html +---@return string function Character:createInfobox() - local infobox = self.infobox local args = self.args local widgets = { @@ -46,8 +45,8 @@ function Character:createInfobox() imageDefaultDark = args.defaultdark or args.defaultdarkmode, size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = (args.informationType or 'Character') .. ' Information'}, + Center{children = {args.caption}}, + Title{children = (args.informationType or 'Character') .. ' Information'}, Cell{name = 'Real Name', content = {args.realname}}, Customizable{ id = 'country', @@ -88,16 +87,16 @@ function Character:createInfobox() } }, Customizable{id = 'custom', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } if Namespace.isMain() then - infobox:categories(args.informationType or 'Character') - infobox:categories(unpack(self:getWikiCategories(args))) + self:categories(args.informationType or 'Character') + self:categories(unpack(self:getWikiCategories(args))) self:setLpdbData(args) end - return infobox:build(widgets) + return self:build(widgets) end ---@param location string? diff --git a/components/infobox/commons/infobox_company.lua b/components/infobox/commons/infobox_company.lua index 1d993ff91bf..54e0c49e9be 100644 --- a/components/infobox/commons/infobox_company.lua +++ b/components/infobox/commons/infobox_company.lua @@ -17,7 +17,7 @@ local Table = require('Module:Table') local BasicInfobox = Lua.import('Module:Infobox/Basic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -39,9 +39,8 @@ function Company.run(frame) return company:createInfobox() end ----@return Html +---@return string function Company:createInfobox() - local infobox = self.infobox local args = self.args local widgets = { @@ -51,8 +50,8 @@ function Company:createInfobox() imageDark = args.imagedark or args.imagedarkmode, size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = 'Company Information'}, + Center{children = {args.caption}}, + Title{children = 'Company Information'}, Customizable{id = 'parent', children = { Cell{ name = 'Parent Company', @@ -76,7 +75,7 @@ function Company:createInfobox() Builder{ builder = function() if args.companytype == COMPANY_TYPE_ORGANIZER then - infobox:categories('Tournament organizers') + self:categories('Tournament organizers') return { Cell{ name = 'Awarded Prize Pools', @@ -86,14 +85,14 @@ function Company:createInfobox() end end }, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, Builder{ builder = function() local links = Links.transform(args) if not Table.isEmpty(links) then return { - Title{name = 'Links'}, - Widgets.Links{content = links} + Title{children = 'Links'}, + Widgets.Links{links = links} } end end @@ -124,9 +123,9 @@ function Company:createInfobox() }) }) - infobox:categories('Companies') + self:categories('Companies') - return infobox:build(widgets) + return self:build(widgets) end ---@param location string? diff --git a/components/infobox/commons/infobox_cosmetic.lua b/components/infobox/commons/infobox_cosmetic.lua index 20712c384a8..e492cfc0f12 100644 --- a/components/infobox/commons/infobox_cosmetic.lua +++ b/components/infobox/commons/infobox_cosmetic.lua @@ -13,7 +13,7 @@ local Namespace = require('Module:Namespace') local BasicInfobox = Lua.import('Module:Infobox/Basic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Header = Widgets.Header local Center = Widgets.Center local Customizable = Widgets.Customizable @@ -21,7 +21,7 @@ local Customizable = Widgets.Customizable ---@class CosmeticInfobox: BasicInfobox local Cosmetic = Class.new(BasicInfobox) ----@return Html +---@return string function Cosmetic:createInfobox() local args = self.args self:customParseArguments(args) @@ -45,21 +45,21 @@ function Cosmetic:createInfobox() Customizable{ id = 'caption', children = { - Center{content = {args.caption}}, + Center{children = {args.caption}}, } }, Customizable{id = 'custom', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } - self.infobox:categories('Cosmetics') - self.infobox:categories(unpack(self:getWikiCategories(args))) + self:categories('Cosmetics') + self:categories(unpack(self:getWikiCategories(args))) if Namespace.isMain() then self:setLpdbData(args) end - return self.infobox:build(widgets) + return self:build(widgets) end ---@param args table diff --git a/components/infobox/commons/infobox_game.lua b/components/infobox/commons/infobox_game.lua index 7f165d1b8fd..ac5fed038f5 100644 --- a/components/infobox/commons/infobox_game.lua +++ b/components/infobox/commons/infobox_game.lua @@ -15,7 +15,7 @@ local Json = require('Module:Json') local BasicInfobox = Lua.import('Module:Infobox/Basic') local Links = Lua.import('Module:Links') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -33,9 +33,8 @@ function Game.run(frame) return game:createInfobox() end ----@return Html +---@return string function Game:createInfobox() - local infobox = self.infobox local args = self.args local links = Links.transform(args) @@ -46,8 +45,8 @@ function Game:createInfobox() imageDark = args.imagedark or args.imagedarkmode, size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = 'Game Information'}, + Center{children = {args.caption}}, + Title{children = 'Game Information'}, Customizable{ id = 'developer', children = { @@ -102,21 +101,21 @@ function Game:createInfobox() builder = function() if not Table.isEmpty(links) then return { - Title{name = 'Links'}, - Widgets.Links{content = links} + Title{children = 'Links'}, + Widgets.Links{links = links} } end end }, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } if Namespace.isMain() then - infobox:categories('Games') + self:categories('Games') self:_setLpdbData(args) end - return infobox:build(widgets) + return self:build(widgets) end ---@param args table diff --git a/components/infobox/commons/infobox_item.lua b/components/infobox/commons/infobox_item.lua index f8c16a7c01f..39beba69201 100644 --- a/components/infobox/commons/infobox_item.lua +++ b/components/infobox/commons/infobox_item.lua @@ -12,7 +12,7 @@ local Namespace = require('Module:Namespace') local BasicInfobox = Lua.import('Module:Infobox/Basic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -29,9 +29,8 @@ function Item.run(frame) return item:createInfobox() end ----@return Html +---@return string function Item:createInfobox() - local infobox = self.infobox local args = self.args local widgets = { @@ -51,13 +50,13 @@ function Item:createInfobox() Customizable{ id = 'caption', children = { - Center{content = {args.caption}}, + Center{children = {args.caption}}, } }, Customizable{ id = 'info', children = { - Title{name = 'Item Information'}, + Title{children = 'Item Information'}, Cell{name = 'Type', content = {args.type}}, Cell{name = 'Rarity', content = {args.rarity}}, Cell{name = 'Level', content = {args.level}}, @@ -69,45 +68,45 @@ function Item:createInfobox() Customizable{ id = 'attributes', children = { - Title{name = 'Attributes'}, + Title{children = 'Attributes'}, } }, Customizable{ id = 'ability', children = { - Title{name = 'Ability'}, + Title{children = 'Ability'}, } }, Customizable{ id = 'availability', children = { - Title{name = 'Availability'}, + Title{children = 'Availability'}, } }, Customizable{ id = 'maps', children = { - Title{name = 'Maps'}, + Title{children = 'Maps'}, } }, Customizable{ id = 'recipe', children = { - Title{name = 'Recipe'}, + Title{children = 'Recipe'}, } }, Customizable{id = 'custom', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } - infobox:categories('Items') - infobox:categories(unpack(self:getWikiCategories(args))) + self:categories('Items') + self:categories(unpack(self:getWikiCategories(args))) if Namespace.isMain() then self:setLpdbData(args) end - return infobox:build(widgets) + return self:build(widgets) end ---@param args table diff --git a/components/infobox/commons/infobox_league.lua b/components/infobox/commons/infobox_league.lua index 0068a57015d..ef35e748329 100644 --- a/components/infobox/commons/infobox_league.lua +++ b/components/infobox/commons/infobox_league.lua @@ -34,7 +34,7 @@ local TextSanitizer = Lua.import('Module:TextSanitizer') local INVALID_TIER_WARNING = '${tierString} is not a known Liquipedia ${tierMode}' local VENUE_DESCRIPTION = '(${desc})' -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -67,8 +67,8 @@ function League:createInfobox() imageDark = args.imagedark or args.imagedarkmode, size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = 'League Information'}, + Center{children = {args.caption}}, + Title{children = 'League Information'}, Cell{ name = 'Series', content = { @@ -121,13 +121,13 @@ function League:createInfobox() local value = tostring(args.type):lower() if self:shouldStore(args) then if value == 'offline' then - self.infobox:categories('Offline Tournaments') + self:categories('Offline Tournaments') elseif value == 'online' then - self.infobox:categories('Online Tournaments') + self:categories('Online Tournaments') elseif value:match('online') and value:match('offline') then - self.infobox:categories('Online/Offline Tournaments') + self:categories('Online/Offline Tournaments') else - self.infobox:categories('Unknown Type Tournaments') + self:categories('Unknown Type Tournaments') end end @@ -196,22 +196,22 @@ function League:createInfobox() builder = function() if Table.isNotEmpty(self.links) then return { - Title{name = 'Links'}, - Widgets.Links{content = self.links} + Title{children = 'Links'}, + Widgets.Links{links = self.links} } end end }, Customizable{id = 'customcontent', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, Customizable{id = 'chronology', children = { Builder{ builder = function() if self:_isChronologySet(args.previous, args.next) then return { - Title{name = 'Chronology'}, + Title{children = 'Chronology'}, Chronology{ - content = Table.filterByKey(args, function(key) + links = Table.filterByKey(args, function(key) return type(key) == 'string' and (key:match('^previous%d?$') ~= nil or key:match('^next%d?$') ~= nil) end) } @@ -225,16 +225,16 @@ function League:createInfobox() self.name = TextSanitizer.stripHTML(self.name) - self.infobox:bottom(self:createBottomContent()) + self:bottom(self:createBottomContent()) if self:shouldStore(args) then - self.infobox:categories(unpack(self:_getCategories(args))) + self:categories(unpack(self:_getCategories(args))) self:_setLpdbData(args, self.links) self:_setSeoTags(args) end return mw.html.create() - :node(self.infobox:build(widgets)) + :node(self:build(widgets)) :node(Logic.readBool(args.autointro) and ('' .. self:seoText(args)) or nil) end @@ -372,11 +372,11 @@ function League:addTierCategories(args) table.insert(categories, tierTypeCategory) if not isValidTierTuple and not tierCategory and Logic.isNotEmpty(tier) then - table.insert(self.infobox.warnings, String.interpolate(INVALID_TIER_WARNING, {tierString = tier, tierMode = 'Tier'})) + table.insert(self.warnings, String.interpolate(INVALID_TIER_WARNING, {tierString = tier, tierMode = 'Tier'})) table.insert(categories, 'Pages with invalid Tier') end if not isValidTierTuple and not tierTypeCategory and String.isNotEmpty(tierType) then - table.insert(self.infobox.warnings, + table.insert(self.warnings, String.interpolate(INVALID_TIER_WARNING, {tierString = tierType, tierMode = 'Tiertype'})) table.insert(categories, 'Pages with invalid Tiertype') end @@ -581,7 +581,7 @@ function League:_createLocation(args) local nationality = Flags.getLocalisation(country) if String.isEmpty(nationality) then - self.infobox:categories('Unrecognised Country') + self:categories('Unrecognised Country') else local location = args['city' .. index] or args['location' .. index] @@ -592,7 +592,7 @@ function League:_createLocation(args) end if self:shouldStore(args) then - self.infobox:categories(nationality .. ' Tournaments') + self:categories(nationality .. ' Tournaments') end table.insert(display, Flags.Icon{flag = country, shouldLink = true} .. ' ' .. displayText .. '') end @@ -639,7 +639,7 @@ function League:getIcons(iconArgs) } if String.isNotEmpty(trackingCategory) then - table.insert(self.infobox.warnings, 'Missing icon while icondark is set.') + table.insert(self.warnings, 'Missing icon while icondark is set.') end return icon, iconDark, display diff --git a/components/infobox/commons/infobox_lore.lua b/components/infobox/commons/infobox_lore.lua index 591e6d6419d..6fc694c4d27 100644 --- a/components/infobox/commons/infobox_lore.lua +++ b/components/infobox/commons/infobox_lore.lua @@ -13,7 +13,7 @@ local Namespace = require('Module:Namespace') local BasicInfobox = Lua.import('Module:Infobox/Basic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Header = Widgets.Header local Center = Widgets.Center local Customizable = Widgets.Customizable @@ -21,7 +21,7 @@ local Customizable = Widgets.Customizable ---@class LoreInfobox: BasicInfobox local Cosmetic = Class.new(BasicInfobox) ----@return Html +---@return string function Cosmetic:createInfobox() local args = self.args self:customParseArguments(args) @@ -44,21 +44,21 @@ function Cosmetic:createInfobox() Customizable{ id = 'caption', children = { - Center{content = {args.caption}}, + Center{children = {args.caption}}, } }, Customizable{id = 'custom', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } - self.infobox:categories('Lore') - self.infobox:categories(unpack(self:getWikiCategories(args))) + self:categories('Lore') + self:categories(unpack(self:getWikiCategories(args))) if Namespace.isMain() then self:setLpdbData(args) end - return self.infobox:build(widgets) + return self:build(widgets) end ---@param args table diff --git a/components/infobox/commons/infobox_manufacturer.lua b/components/infobox/commons/infobox_manufacturer.lua index e9399f15e04..61a62e53517 100644 --- a/components/infobox/commons/infobox_manufacturer.lua +++ b/components/infobox/commons/infobox_manufacturer.lua @@ -14,7 +14,7 @@ local Namespace = require('Module:Namespace') local BasicInfobox = Lua.import('Module:Infobox/Basic') local Locale = Lua.import('Module:Locale') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -25,9 +25,8 @@ local Builder = Widgets.Builder ---@class ManufacturerInfobox: BasicInfobox local Manufacturer = Class.new(BasicInfobox) ----@return Html +---@return string function Manufacturer:createInfobox() - local infobox = self.infobox local args = self.args local widgets = { @@ -46,10 +45,10 @@ function Manufacturer:createInfobox() Customizable{ id = 'caption', children = { - Center{content = {args.caption}}, + Center{children = {args.caption}}, } }, - Title{name = (args.informationType or 'Manufacturer') .. ' Information'}, + Title{children = (args.informationType or 'Manufacturer') .. ' Information'}, Cell{name = 'Former Name(s)', content = {args.formernames}}, Cell{name = 'Description', content = {args.description}}, Cell{name = 'Season(s)', content = {args.seasons}}, @@ -62,7 +61,7 @@ function Manufacturer:createInfobox() builder = function() if args.founded or args.dissolved then return { - Title{name = 'History'}, + Title{children = 'History'}, Cell{name = 'Founded', content = {args.founded}}, Cell{name = 'Dissolved', content = {args.dissolved}} } @@ -75,11 +74,11 @@ function Manufacturer:createInfobox() if Namespace.isMain() then self:setLpdbData(args) - infobox:categories('Manufacturers') - infobox:categories(unpack(self:getWikiCategories(args))) + self:categories('Manufacturers') + self:categories(unpack(self:getWikiCategories(args))) end - return infobox:build(widgets) + return self:build(widgets) end ---@param args table diff --git a/components/infobox/commons/infobox_map.lua b/components/infobox/commons/infobox_map.lua index 786a43bcd4d..6f1e7f03b14 100644 --- a/components/infobox/commons/infobox_map.lua +++ b/components/infobox/commons/infobox_map.lua @@ -15,7 +15,7 @@ local Table = require('Module:Table') local BasicInfobox = Lua.import('Module:Infobox/Basic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -33,9 +33,8 @@ function Map.run(frame) return map:createInfobox() end ----@return Html +---@return string function Map:createInfobox() - local infobox = self.infobox local args = self.args self:_readCreators() @@ -47,23 +46,23 @@ function Map:createInfobox() imageDark = args.imagedark or args.imagedarkmode, size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = (args.informationType or 'Map') .. ' Information'}, + Center{children = {args.caption}}, + Title{children = (args.informationType or 'Map') .. ' Information'}, Cell{name = 'Creator', content = self.creators, options = {makeLink = true}}, Customizable{id = 'location', children = { Cell{name = 'Location', content = {args.location}} }}, Cell{name = 'Release Date', content = {args.releasedate}}, Customizable{id = 'custom', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } if Namespace.isMain() then - infobox:categories('Maps', unpack(self:getWikiCategories(args))) + self:categories('Maps', unpack(self:getWikiCategories(args))) self:_setLpdbData(args) end - return infobox:build(widgets) + return self:build(widgets) end --- Allows for overriding this functionality diff --git a/components/infobox/commons/infobox_patch.lua b/components/infobox/commons/infobox_patch.lua index eefb4b04b19..00b5e18fd68 100644 --- a/components/infobox/commons/infobox_patch.lua +++ b/components/infobox/commons/infobox_patch.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local BasicInfobox = Lua.import('Module:Infobox/Basic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -36,9 +36,8 @@ function Patch.run(frame) return patch:createInfobox() end ----@return Html +---@return string function Patch:createInfobox() - local infobox = self.infobox local args = self.args local widgets = { @@ -48,8 +47,8 @@ function Patch:createInfobox() imageDark = args.imagedark or args.imagedarkmode, size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = (self:getInformationType(args)) .. ' Information'}, + Center{children = {args.caption}}, + Title{children = (self:getInformationType(args)) .. ' Information'}, Cell{name = 'Version', content = {args.version}}, Customizable{id = 'release', children = { Cell{name = 'Release', content = {args.release}}, @@ -61,8 +60,8 @@ function Patch:createInfobox() local highlights = self:getAllArgsForBase(args, 'highlight') if not Table.isEmpty(highlights) then return { - Title{name = 'Highlights'}, - Highlights{content = highlights} + Title{children = 'Highlights'}, + Highlights{children = highlights} } end end @@ -72,24 +71,24 @@ function Patch:createInfobox() local chronologyData = self:getChronologyData(args) if not Table.isEmpty(chronologyData) then return { - Title{name = 'Chronology'}, + Title{children = 'Chronology'}, Chronology{ - content = chronologyData + links = chronologyData } } end end }, Customizable{id = 'customcontent', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } if Namespace.isMain() and not Logic.readBool(Variables.varDefault('disable_LPDB_storage')) then - infobox:categories(self:getInformationType(args)) + self:categories(self:getInformationType(args)) self:setLpdbData(args) end - return infobox:build(widgets) + return self:build(widgets) end --- Allows for overriding this functionality diff --git a/components/infobox/commons/infobox_person.lua b/components/infobox/commons/infobox_person.lua index 82a2808619d..85da4f35840 100644 --- a/components/infobox/commons/infobox_person.lua +++ b/components/infobox/commons/infobox_person.lua @@ -25,7 +25,7 @@ local Links = Lua.import('Module:Links') local PlayerIntroduction = Lua.import('Module:PlayerIntroduction/Custom') local Region = Lua.import('Module:Region') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Header = Widgets.Header local Title = Widgets.Title local Cell = Widgets.Cell @@ -45,7 +45,9 @@ local COUNTRIES_EASTERN_NAME_ORDER = { 'Hong Kong', 'Vietnam', 'South Korea', - 'Cambodia' + 'Cambodia', + 'Macau', + 'Singapore', } ---@enum PlayerStatus @@ -74,9 +76,8 @@ function Person.run(frame) return person:createInfobox() end ----@return Html +---@return string function Person:createInfobox() - local infobox = self.infobox local args = self.args self.locations = self:getLocations() @@ -135,8 +136,8 @@ function Person:createInfobox() subHeader = self:subHeaderDisplay(args), size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = (args.informationType or 'Player') .. ' Information'}, + Center{children = {args.caption}}, + Title{children = (args.informationType or 'Player') .. ' Information'}, Customizable{id = 'names', children = { Cell{name = 'Name', content = {args.name}}, Cell{name = 'Romanized Name', content = {args.romanized_name}}, @@ -191,8 +192,8 @@ function Person:createInfobox() builder = function() if Table.isNotEmpty(links) then return { - Title{name = 'Links'}, - Widgets.Links{content = links, variant = LINK_VARIANT} + Title{children = 'Links'}, + Widgets.Links{links = links, variant = LINK_VARIANT} } end end @@ -202,8 +203,8 @@ function Person:createInfobox() builder = function() if String.isNotEmpty(args.achievements) then return { - Title{name = 'Achievements'}, - Center{content = {args.achievements}} + Title{children = 'Achievements'}, + Center{children = {args.achievements}} } end end @@ -214,21 +215,21 @@ function Person:createInfobox() builder = function() if String.isNotEmpty(args.history) then return { - Title{name = 'History'}, - Center{content = {args.history}} + Title{children = 'History'}, + Center{children = {args.history}} } end end }, }}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, Customizable{id = 'customcontent', children = {}}, } - infobox:bottom(self:createBottomContent()) + self:bottom(self:createBottomContent()) local statusToStore = self:getStatusToStore(args) - infobox:categories(unpack(self:getCategories( + self:categories(unpack(self:getCategories( args, age.birth, personType.category, @@ -245,7 +246,7 @@ function Person:createInfobox() ) end - return infobox:build(widgets) + return self:build(widgets) end ---@param args table @@ -328,7 +329,7 @@ function Person:getStandardNationalityValue(nationality) if String.isEmpty(nationalityToStore) then table.insert( - self.infobox.warnings, + self.warnings, '"' .. nationality .. '" is not supported as a value for nationalities' ) return nil diff --git a/components/infobox/commons/infobox_scene.lua b/components/infobox/commons/infobox_scene.lua index 214d0c61f41..3dc25993a61 100644 --- a/components/infobox/commons/infobox_scene.lua +++ b/components/infobox/commons/infobox_scene.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local BasicInfobox = Lua.import('Module:Infobox/Basic') local Flags = Lua.import('Module:Flags') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -34,9 +34,8 @@ function Scene.run(frame) return scene:createInfobox() end ----@return Html +---@return string function Scene:createInfobox() - local infobox = self.infobox local args = self.args local widgets = { @@ -46,21 +45,21 @@ function Scene:createInfobox() imageDark = args.imagedark or args.imagedarkmode, size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = 'Scene Information'}, + Center{children = {args.caption}}, + Title{children = 'Scene Information'}, Cell{name = 'Region', content = {args.region}}, Cell{name = 'National Team', content = {args.nationalteam}, options = {makeLink = true}}, Cell{name = 'Events', content = self:getAllArgsForBase(args, 'event', {makeLink = true})}, Cell{name = 'Size', content = {args.size}}, Customizable{id = 'custom', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, Builder{ builder = function() local links = Links.transform(args) if not Table.isEmpty(links) then return { - Title{name = 'Links'}, - Widgets.Links{content = links} + Title{children = 'Links'}, + Widgets.Links{links = links} } end end @@ -69,17 +68,17 @@ function Scene:createInfobox() builder = function() if not String.isEmpty(args.achievements) then return { - Title{name ='Achievements'}, - Center{content = {args.achievements}} + Title{children ='Achievements'}, + Center{children = {args.achievements}} } end end } } - infobox:categories('Scene') + self:categories('Scene') - return infobox:build(widgets) + return self:build(widgets) end --- Allows for overriding this functionality diff --git a/components/infobox/commons/infobox_series.lua b/components/infobox/commons/infobox_series.lua index 254a34c2bda..cc6f66661f5 100644 --- a/components/infobox/commons/infobox_series.lua +++ b/components/infobox/commons/infobox_series.lua @@ -27,7 +27,7 @@ local ReferenceCleaner = Lua.import('Module:ReferenceCleaner') local INVALID_TIER_WARNING = '${tierString} is not a known Liquipedia ${tierMode}' -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -45,9 +45,8 @@ function Series.run(frame) return series:createInfobox() end ----@return Html +---@return string function Series:createInfobox() - local infobox = self.infobox local args = self.args -- define this here so we can use it in lpdb data and the display @@ -74,8 +73,8 @@ function Series:createInfobox() imageDark = args.imagedark or args.imagedarkmode, size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = 'Series Information'}, + Center{children = {args.caption}}, + Title{children = 'Series Information'}, Builder{ builder = function() local organizers = self:_createOrganizers(args) @@ -166,8 +165,8 @@ function Series:createInfobox() builder = function() if not Table.isEmpty(links) then return { - Title{name = 'Links'}, - Widgets.Links{content = links} + Title{children = 'Links'}, + Widgets.Links{links = links} } end end @@ -177,10 +176,10 @@ function Series:createInfobox() if self:shouldStore(args) then self:_setLpdbData(args, links) - infobox:categories(unpack(self:_getCategories(args))) + self:categories(unpack(self:_getCategories(args))) end - return infobox:build(widgets) + return self:build(widgets) end ---@param args table @@ -294,7 +293,7 @@ function Series:_getIconFromLeagueIconSmall(lpdbData) if String.isNotEmpty(trackingCategory) then table.insert( - self.infobox.warnings, + self.warnings, 'Missing icon while icondark is set.' .. trackingCategory ) end @@ -396,11 +395,11 @@ function Series:addTierCategories(args) table.insert(categories, tierTypeCategory) if not isValidTierTuple and not tierCategory and String.isNotEmpty(tier) then - table.insert(self.infobox.warnings, String.interpolate(INVALID_TIER_WARNING, {tierString = tier, tierMode = 'Tier'})) + table.insert(self.warnings, String.interpolate(INVALID_TIER_WARNING, {tierString = tier, tierMode = 'Tier'})) table.insert(categories, 'Pages with invalid Tier') end if not isValidTierTuple and not tierTypeCategory and String.isNotEmpty(tierType) then - table.insert(self.infobox.warnings, + table.insert(self.warnings, String.interpolate(INVALID_TIER_WARNING, {tierString = tierType, tierMode = 'Tiertype'})) table.insert(categories, 'Pages with invalid Tiertype') end diff --git a/components/infobox/commons/infobox_show.lua b/components/infobox/commons/infobox_show.lua index 6a662512939..66fa2ee7fc5 100644 --- a/components/infobox/commons/infobox_show.lua +++ b/components/infobox/commons/infobox_show.lua @@ -15,7 +15,7 @@ local Table = require('Module:Table') local BasicInfobox = Lua.import('Module:Infobox/Basic') local Flags = Lua.import('Module:Flags') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -34,9 +34,8 @@ function Show.run(frame) return show:createInfobox() end ----@return Html +---@return string function Show:createInfobox() - local infobox = self.infobox local args = self.args local widgets = { @@ -46,8 +45,8 @@ function Show:createInfobox() imageDark = args.imagedark or args.imagedarkmode, size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = 'Show Information'}, + Center{children = {args.caption}}, + Title{children = 'Show Information'}, Cell{name = 'Host(s)', content = self:getAllArgsForBase(args, 'host', {makeLink = true})}, Cell{name = 'Format', content = {args.format}}, Cell{name = 'Airs', content = {args.airs}}, @@ -62,26 +61,26 @@ function Show:createInfobox() local secondaryLinks = Show:_addSecondaryLinkDisplay(args) local returnWidgets = {} if (not Table.isEmpty(links)) or (secondaryLinks ~= '') then - table.insert(returnWidgets, Title{name = 'Links'}) + table.insert(returnWidgets, Title{children = 'Links'}) end if not Table.isEmpty(links) then - table.insert(returnWidgets, Widgets.Links{content = links}) + table.insert(returnWidgets, Widgets.Links{links = links}) end if secondaryLinks ~= '' then - table.insert(returnWidgets, Center{content = {secondaryLinks}}) + table.insert(returnWidgets, Center{children = {secondaryLinks}}) end return returnWidgets end }, Customizable{id = 'customcontent', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } if Namespace.isMain() then - infobox:categories('Shows') + self:categories('Shows') end - return infobox:build(widgets) + return self:build(widgets) end ---@param country string? diff --git a/components/infobox/commons/infobox_skill.lua b/components/infobox/commons/infobox_skill.lua index 07cd5e17714..70357105962 100644 --- a/components/infobox/commons/infobox_skill.lua +++ b/components/infobox/commons/infobox_skill.lua @@ -17,7 +17,7 @@ local Variables = require('Module:Variables') local BasicInfobox = Lua.import('Module:Infobox/Basic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -34,9 +34,8 @@ function Skill.run(frame) return skill:createInfobox() end ----@return Html +---@return string function Skill:createInfobox() - local infobox = self.infobox local args = self.args if String.isEmpty(args.informationType) then @@ -50,8 +49,8 @@ function Skill:createInfobox() imageDark = args.imagedark or args.imagedarkmode, size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = args.informationType .. ' Information'}, + Center{children = {args.caption}}, + Title{children = args.informationType .. ' Information'}, Cell{name = 'Caster(s)', content = self:getAllArgsForBase(args, 'caster', {makeLink = true})}, Customizable{ id = 'cost', @@ -80,16 +79,16 @@ function Skill:createInfobox() } }, Customizable{id = 'custom', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } if Namespace.isMain() then local categories = self:getCategories(args) - infobox:categories(unpack(categories)) + self:categories(unpack(categories)) self:_setLpdbData(args) end - return infobox:build(widgets) + return self:build(widgets) end ---@param args table diff --git a/components/infobox/commons/infobox_strategy.lua b/components/infobox/commons/infobox_strategy.lua index 742449065fd..445221a4477 100644 --- a/components/infobox/commons/infobox_strategy.lua +++ b/components/infobox/commons/infobox_strategy.lua @@ -13,7 +13,7 @@ local String = require('Module:StringUtils') local BasicInfobox = Lua.import('Module:Infobox/Basic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -30,9 +30,8 @@ function Strategy.run(frame) return strategy:createInfobox() end ----@return Html +---@return string function Strategy:createInfobox() - local infobox = self.infobox local args = self.args if String.isEmpty(args.informationType) then @@ -49,22 +48,22 @@ function Strategy:createInfobox() }, } }, - Center{content = {args.caption}}, - Title{name = args.informationType .. ' Information'}, + Center{children = {args.caption}}, + Title{children = args.informationType .. ' Information'}, Cell{ name = 'Creator(s)', content = {args.creator or args['created-by']}, options = {makeLink = true} }, Customizable{id = 'custom', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } if Namespace.isMain() then - infobox:categories('Strategies') + self:categories('Strategies') end - return infobox:build(widgets) + return self:build(widgets) end return Strategy diff --git a/components/infobox/commons/infobox_team.lua b/components/infobox/commons/infobox_team.lua index f42b4b93f7e..f13a6ecb843 100644 --- a/components/infobox/commons/infobox_team.lua +++ b/components/infobox/commons/infobox_team.lua @@ -30,7 +30,7 @@ local Locale = Lua.import('Module:Locale') local ReferenceCleaner = Lua.import('Module:ReferenceCleaner') local Region = Lua.import('Module:Region') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -60,9 +60,8 @@ function Team.run(frame) return team:createInfobox() end ----@return Html +---@return string function Team:createInfobox() - local infobox = self.infobox local args = self.args --- Transform data @@ -96,8 +95,8 @@ function Team:createInfobox() imageDefaultDark = args.defaultdark or args.defaultdarkmode, size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = 'Team Information'}, + Center{children = {args.caption}}, + Title{children = 'Team Information'}, Customizable{id = 'topcustomcontent', children = {}}, Cell{ name = 'Location', @@ -137,8 +136,8 @@ function Team:createInfobox() builder = function() if not Table.isEmpty(links) then return { - Title{name = 'Links'}, - Widgets.Links{content = links, variant = LINK_VARIANT} + Title{children = 'Links'}, + Widgets.Links{links = links, variant = LINK_VARIANT} } end end @@ -150,8 +149,8 @@ function Team:createInfobox() builder = function() if String.isNotEmpty(args.achievements) then return { - Title{name = 'Achievements'}, - Center{content = {args.achievements}} + Title{children = 'Achievements'}, + Center{children = {args.achievements}} } end end @@ -165,7 +164,7 @@ function Team:createInfobox() builder = function() if Table.isNotEmpty(created) or args.disbanded then return { - Title{name = 'History'}, + Title{children = 'History'}, Cell{name = 'Created', content = created}, Cell{name = 'Disbanded', content = {args.disbanded}} } @@ -178,21 +177,21 @@ function Team:createInfobox() builder = function() if args.trades then return { - Center{content = {args.trades}} + Center{children = {args.trades}} } end end }, Customizable{id = 'customcontent', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } - infobox:bottom(self:_createUpcomingMatches()) - infobox:bottom(self:createBottomContent()) + self:bottom(self:_createUpcomingMatches()) + self:bottom(self:createBottomContent()) -- Categories if self:shouldStore(args) then - infobox:categories('Teams') - infobox:categories(unpack(self:getWikiCategories(args))) + self:categories('Teams') + self:categories(unpack(self:getWikiCategories(args))) end -- Store LPDB data and Wiki-variables @@ -201,7 +200,7 @@ function Team:createInfobox() self:_definePageVariables(args) end - return infobox:build(widgets) + return self:build(widgets) end ---@return string|number|nil # storage date @@ -297,7 +296,7 @@ function Team:_createLocation(location) end if String.isNotEmpty(demonym) and self:shouldStore(self.args) then - self.infobox:categories(demonym .. ' Teams') + self:categories(demonym .. ' Teams') end return Flags.Icon({flag = location, shouldLink = true}) .. @@ -324,7 +323,7 @@ function Team:getStandardLocationValue(location) if String.isEmpty(locationToStore) then table.insert( - self.infobox.warnings, + self.warnings, '"' .. location .. '" is not supported as a value for locations' ) return diff --git a/components/infobox/commons/infobox_tool.lua b/components/infobox/commons/infobox_tool.lua index b3768b31ba9..68a6be9e170 100644 --- a/components/infobox/commons/infobox_tool.lua +++ b/components/infobox/commons/infobox_tool.lua @@ -12,7 +12,7 @@ local Namespace = require('Module:Namespace') local BasicInfobox = Lua.import('Module:Infobox/Basic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -31,9 +31,8 @@ function Tool.run(frame) return tool:createInfobox() end ----@return Html +---@return string function Tool:createInfobox() - local infobox = self.infobox local args = self.args local widgets = { @@ -42,8 +41,8 @@ function Tool:createInfobox() image = args.image, imageDark = args.imagedark or args.imagedarkmode, }, - Center{content = {args.caption}}, - Title{name = 'Tool Information'}, + Center{children = {args.caption}}, + Title{children = 'Tool Information'}, Cell{name = 'Game', content = { (args.game or args.defaultGame) .. (args.gameversion and (' ' .. args.gameversion) or '') }}, @@ -51,14 +50,14 @@ function Tool:createInfobox() Cell{name = 'Current Version', content = {args.version or UNKNOWN}}, Cell{name = 'Thread', content = {args.thread and ('[' .. args.thread .. ' Thread]') or nil}}, Cell{name = 'Download', content = {args.download}}, - Center{content = {args.footnotes and ('' .. args.footnotes .. '') or nil}}, + Center{children = {args.footnotes and ('' .. args.footnotes .. '') or nil}}, } if Namespace.isMain() then - infobox:categories('Tools', unpack(self:getWikiCategories(args))) + self:categories('Tools', unpack(self:getWikiCategories(args))) end - return infobox:build(widgets) + return self:build(widgets) end --- Allows for overriding this functionality diff --git a/components/infobox/commons/infobox_unit.lua b/components/infobox/commons/infobox_unit.lua index 253308e7934..d906663ef65 100644 --- a/components/infobox/commons/infobox_unit.lua +++ b/components/infobox/commons/infobox_unit.lua @@ -14,7 +14,7 @@ local String = require('Module:StringUtils') local BasicInfobox = Lua.import('Module:Infobox/Basic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -31,9 +31,8 @@ function Unit.run(frame) return unit:createInfobox() end ----@return Html +---@return string function Unit:createInfobox() - local infobox = self.infobox local args = self.args local widgets = { @@ -54,10 +53,10 @@ function Unit:createInfobox() Customizable{ id = 'caption', children = { - Center{content = {args.caption}}, + Center{children = {args.caption}}, } }, - Title{name = (args.informationType or 'Unit') .. ' Information'}, + Title{children = (args.informationType or 'Unit') .. ' Information'}, Customizable{ id = 'type', children = { @@ -102,18 +101,18 @@ function Unit:createInfobox() } }, Customizable{id = 'custom', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, Customizable{id = 'customcontent', children = {}}, } - infobox:categories('Units') - infobox:categories(unpack(self:getWikiCategories(args))) + self:categories('Units') + self:categories(unpack(self:getWikiCategories(args))) if Namespace.isMain() then self:setLpdbData(args) end - return infobox:build(widgets) + return self:build(widgets) end ---@param args table diff --git a/components/infobox/commons/infobox_unofficial_world_champion.lua b/components/infobox/commons/infobox_unofficial_world_champion.lua index 4af629d1c87..b4a42d74cc1 100644 --- a/components/infobox/commons/infobox_unofficial_world_champion.lua +++ b/components/infobox/commons/infobox_unofficial_world_champion.lua @@ -13,7 +13,7 @@ local String = require('Module:StringUtils') local BasicInfobox = Lua.import('Module:Infobox/Basic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -32,9 +32,8 @@ function UnofficialWorldChampion.run(frame) return unofficialWorldChampion:createInfobox() end ----@return Html +---@return string function UnofficialWorldChampion:createInfobox() - local infobox = self.infobox local args = self.args local widgets = { @@ -44,9 +43,9 @@ function UnofficialWorldChampion:createInfobox() imageDark = args.imagedark or args.imagedarkmode, size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = 'Current Champion'}, - Center{content = { args['current champion'] }, classes = { 'infobox-size-20', 'infobox-bold' }}, + Center{children = {args.caption}}, + Title{children = 'Current Champion'}, + Center{children = { args['current champion'] }, classes = { 'infobox-size-20', 'infobox-bold' }}, Builder{ builder = function() if not String.isEmpty(args['gained date']) then @@ -59,13 +58,13 @@ function UnofficialWorldChampion:createInfobox() contentCell = ' vs ' .. args['gained against'] end return { - Title{name = 'Title Gained'}, + Title{children = 'Title Gained'}, Cell{name = args['gained date'], content = { contentCell }}, } end end }, - Title{name = 'Most Defences'}, + Title{children = 'Most Defences'}, Cell{ name = (args['most defences no'] or '?') .. ' Matches', content = { args['most defences'] }, @@ -75,32 +74,32 @@ function UnofficialWorldChampion:createInfobox() builder = function() return Array.map( self:getAllArgsForBase(args, 'most defences against '), - function (value) return Breakdown{content = {value}} end + function (value) return Breakdown{children = {value}} end ) end }, } }, - Title{name = 'Longest Consecutive Time as Champion'}, + Title{children = 'Longest Consecutive Time as Champion'}, Cell{ name = (args['longest consecutive no'] or '?') .. ' days', content = { args['longest consecutive'] }, }, - Title{name = 'Longest Total Time as Champion'}, + Title{children = 'Longest Total Time as Champion'}, Cell{ name = (args['longest total no'] or '?') .. ' days', content = { args['longest total'] }, }, - Title{name = 'Most Times Held'}, + Title{children = 'Most Times Held'}, Cell{ name = (args['most times held no'] or '?') .. ' times', content = { args['most times held'] }, }, Customizable{id = 'custom', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } - return infobox:build(widgets) + return self:build(widgets) end return UnofficialWorldChampion diff --git a/components/infobox/commons/infobox_weapon.lua b/components/infobox/commons/infobox_weapon.lua index cecdd60ebf4..6a80ce6b1b0 100644 --- a/components/infobox/commons/infobox_weapon.lua +++ b/components/infobox/commons/infobox_weapon.lua @@ -14,7 +14,7 @@ local Namespace = require('Module:Namespace') local BasicInfobox = Lua.import('Module:Infobox/Basic') local Flags = Lua.import('Module:Flags') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header local Title = Widgets.Title @@ -32,9 +32,8 @@ function Weapon.run(frame) return weapon:createInfobox() end ----@return Html +---@return string function Weapon:createInfobox() - local infobox = self.infobox local args = self.args local widgets = { @@ -47,8 +46,8 @@ function Weapon:createInfobox() imageDefaultDark = args.defaultdark or args.defaultdarkmode, size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = (args.informationType or 'Weapon') .. ' Information'}, + Center{children = {args.caption}}, + Title{children = (args.informationType or 'Weapon') .. ' Information'}, Cell{ name = 'Class', content = self:getAllArgsForBase(args, 'class', {makeLink = true}), @@ -104,17 +103,17 @@ function Weapon:createInfobox() } }, Customizable{id = 'custom', children = {}}, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } - infobox:categories('Weapons') - infobox:categories(unpack(self:getWikiCategories(args))) + self:categories('Weapons') + self:categories(unpack(self:getWikiCategories(args))) if Namespace.isMain() then self:setLpdbData(args) end - return infobox:build(widgets) + return self:build(widgets) end ---@param location string? diff --git a/components/infobox/commons/infobox_website.lua b/components/infobox/commons/infobox_website.lua index dc29e3c9831..bce6793988b 100644 --- a/components/infobox/commons/infobox_website.lua +++ b/components/infobox/commons/infobox_website.lua @@ -13,7 +13,7 @@ local Table = require('Module:Table') local BasicInfobox = Lua.import('Module:Infobox/Basic') local Links = Lua.import('Module:Links') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Header = Widgets.Header local Title = Widgets.Title local Cell = Widgets.Cell @@ -31,9 +31,8 @@ function Website.run(frame) return website:createInfobox() end ----@return Html +---@return string function Website:createInfobox() - local infobox = self.infobox local args = self.args local widgets = { @@ -43,8 +42,8 @@ function Website:createInfobox() imageDark = args.imagedark or args.imagedarkmode, size = args.imagesize, }, - Center{content = {args.caption}}, - Title{name = 'Website Information'}, + Center{children = {args.caption}}, + Title{children = 'Website Information'}, Cell{name = 'Type', content = {args.type}}, Cell{name = 'Available Language(s)', content = self:getAllArgsForBase(args, 'language')}, Cell{name = 'Content License', content = {args.content_license}}, @@ -56,16 +55,16 @@ function Website:createInfobox() local links = Links.transform(args) if not Table.isEmpty(links) then return { - Title{name = 'Links'}, - Widgets.Links{content = links} + Title{children = 'Links'}, + Widgets.Links{links = links} } end end }, - Center{content = {args.footnotes}}, + Center{children = {args.footnotes}}, } - return infobox:build(widgets) + return self:build(widgets) end return Website diff --git a/components/infobox/extensions/wikis/stormgate/infobox_extension_attack.lua b/components/infobox/extensions/wikis/stormgate/infobox_extension_attack.lua index 521f839be03..ad3b5999d4e 100644 --- a/components/infobox/extensions/wikis/stormgate/infobox_extension_attack.lua +++ b/components/infobox/extensions/wikis/stormgate/infobox_extension_attack.lua @@ -13,7 +13,7 @@ local Page = require('Module:Page') local String = require('Module:StringUtils') local Table = require('Module:Table') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -47,7 +47,7 @@ function Attack.run(argsJson, attackIndex, faction) Attack._store(data, args, faction, attackIndex) return { - Title{name = 'Attack' .. attackIndex .. ': ' .. args.name}, + Title{children = 'Attack' .. attackIndex .. ': ' .. args.name}, Cell{name = 'Target', content = {Attack._displayArray(data.targets)}}, Cell{name = 'Damage', content = {Attack._displayDamage(data)}}, Cell{name = 'Effect', content = {Attack._displayArray(data.effect)}}, diff --git a/components/infobox/extensions/wikis/warcraft/infobox_extension_building_unit_shared.lua b/components/infobox/extensions/wikis/warcraft/infobox_extension_building_unit_shared.lua index befbaea46b4..31be7ab70fe 100644 --- a/components/infobox/extensions/wikis/warcraft/infobox_extension_building_unit_shared.lua +++ b/components/infobox/extensions/wikis/warcraft/infobox_extension_building_unit_shared.lua @@ -18,7 +18,7 @@ local Math = require('Module:MathUtil') local String = require('Module:StringUtils') local Table = require('Module:Table') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local BreakDown = Widgets.Breakdown local Cell = Widgets.Cell local Title = Widgets.Title @@ -144,10 +144,10 @@ function CustomBuildingUnit.attackDisplay(args) if Array.all(data, Logic.isEmpty) then return nil end - return BreakDown{contentClasses = {content1 = {'infobox-description'}}, content = {desc, unpack(data)}} + return BreakDown{contentClasses = {content1 = {'infobox-description'}}, children = {desc, unpack(data)}} end - return Array.append({Title{name = 'Combat'}}, + return Array.append({Title{children = 'Combat'}}, toWidget('Requirement:', 'requirement'), toWidget('[[Attack Damage|Damage]]:', 'attackTypeAndDamage'), toWidget(CustomBuildingUnit.getAreaHeader(args) or '', 'area'), @@ -340,7 +340,7 @@ function CustomBuildingUnit.mercenaryStats(args) end return { - Title{name = 'Mercenary Stats'}, + Title{children = 'Mercenary Stats'}, Cell{name = 'Stock Maximum', content = {args.stock}}, Cell{name = 'Stock Start Delay', content = { Abbreviation.make(args.stockstart .. 's', 'First available at ' .. GameClock.run(args.stockstart))}}, @@ -360,7 +360,7 @@ function CustomBuildingUnit.movement(args, title) end return { - Title{name = title}, + Title{children = title}, Cell{name = '[[Movement Speed|Speed]]', content = {speed}}, Cell{name = 'Turn Rate', content = {args.turnrate}}, Cell{name = 'Move Type', content = {args.movetype}}, diff --git a/components/infobox/wikis/ageofempires/infobox_league_custom.lua b/components/infobox/wikis/ageofempires/infobox_league_custom.lua index 7b9320230c8..a4ef14d75ff 100644 --- a/components/infobox/wikis/ageofempires/infobox_league_custom.lua +++ b/components/infobox/wikis/ageofempires/infobox_league_custom.lua @@ -26,7 +26,7 @@ local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') local ReferenceCleaner = Lua.import('Module:ReferenceCleaner') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -55,7 +55,6 @@ function CustomLeague:customParseArguments(args) String.isNotEmpty(args.team_number) and 'team' or '1v1' ) - self.categories = {} self.data.maps = self:_getMaps() self.data.gameModes = self:_getGameModes(args) end @@ -78,7 +77,7 @@ function CustomInjector:parse(id, widgets) local playertitle = (not String.isEmpty(args.team_number)) and 'Teams' or 'Players' Array.appendWith(widgets, - Title{name = playertitle}, + Title{children = playertitle}, Cell{name = 'Number of Teams', content = {args.team_number}}, Cell{name = 'Number of Players', content = {args.player_number}} ) @@ -96,13 +95,13 @@ function CustomInjector:parse(id, widgets) index = index + 1 end - table.insert(widgets, Center{content = teams}) + table.insert(widgets, Center{children = teams}) end if not String.isEmpty(args.map1) then Array.appendWith(widgets, - Title{name = 'Maps'}, - Center{content = caller:_displayMaps(caller.data.maps)} + Title{children = 'Maps'}, + Center{children = caller:_displayMaps(caller.data.maps)} ) end elseif id == 'sponsors' then @@ -134,7 +133,7 @@ end ---@param args table ---@return string[] function CustomLeague:getWikiCategories(args) - return Array.append(self.categories, + return Array.append({}, String.isEmpty(args.game) and 'Tournaments without game version' or (GameLookup.getName({args.game}) .. (args.beta and ' Beta' or '') .. ' Competitions') ) @@ -288,7 +287,7 @@ end function CustomLeague:_getGameModes(args) if String.isEmpty(args.gamemode) then local default = GameModeLookup.getDefault(args.game or '') - table.insert(self.categories, default .. ' Tournaments') + self:categories(default .. ' Tournaments') return {default} end @@ -297,7 +296,7 @@ function CustomLeague:_getGameModes(args) function(mode, index) gameModes[index] = GameModeLookup.getName(mode) or '' - table.insert(self.categories, not String.isEmpty(gameModes[index]) + self:categories(not String.isEmpty(gameModes[index]) and gameModes[index] .. ' Tournaments' or 'Pages with unknown game mode' ) @@ -350,7 +349,7 @@ function CustomLeague:_checkMapInformation(name, link, game) local extradata = data[1].extradata or {} if extradata.game ~= game then mw.logObject('Map ' .. name .. ' is linking to ' .. link .. ', an ' .. extradata.game .. ' page.') - table.insert(self.categories, 'Tournaments linking to maps for a different game') + self:categories('Tournaments linking to maps for a different game') end end end diff --git a/components/infobox/wikis/ageofempires/infobox_map_custom.lua b/components/infobox/wikis/ageofempires/infobox_map_custom.lua index 426fc294bee..6e5105b8462 100644 --- a/components/infobox/wikis/ageofempires/infobox_map_custom.lua +++ b/components/infobox/wikis/ageofempires/infobox_map_custom.lua @@ -16,7 +16,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Map = Lua.import('Module:Infobox/Map') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class AgeofEmpiresMapInfobox: MapInfobox diff --git a/components/infobox/wikis/ageofempires/infobox_person_player_custom.lua b/components/infobox/wikis/ageofempires/infobox_person_player_custom.lua index e43e50f556a..904fcf232fd 100644 --- a/components/infobox/wikis/ageofempires/infobox_person_player_custom.lua +++ b/components/infobox/wikis/ageofempires/infobox_person_player_custom.lua @@ -26,7 +26,7 @@ local Achievements = Lua.import('Module:Infobox/Extension/Achievements') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -180,7 +180,7 @@ function CustomInjector:parse(id, widgets) end) end if Logic.isNotEmpty(ratingCells) then - table.insert(widgets, Title{name = 'Ratings'}) + table.insert(widgets, Title{children = 'Ratings'}) Array.extendWith(widgets, ratingCells) end elseif id == 'status' then diff --git a/components/infobox/wikis/ageofempires/infobox_team_custom.lua b/components/infobox/wikis/ageofempires/infobox_team_custom.lua index ed74a94ed08..6972c54fb13 100644 --- a/components/infobox/wikis/ageofempires/infobox_team_custom.lua +++ b/components/infobox/wikis/ageofempires/infobox_team_custom.lua @@ -18,7 +18,7 @@ local Achievements = Lua.import('Module:Infobox/Extension/Achievements') local Injector = Lua.import('Module:Widget/Injector') local Team = Lua.import('Module:Infobox/Team') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Condition = require('Module:Condition') diff --git a/components/infobox/wikis/apexlegends/infobox_character_custom.lua b/components/infobox/wikis/apexlegends/infobox_character_custom.lua index f4952b2facb..d1b1e959808 100644 --- a/components/infobox/wikis/apexlegends/infobox_character_custom.lua +++ b/components/infobox/wikis/apexlegends/infobox_character_custom.lua @@ -13,7 +13,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Character = Lua.import('Module:Infobox/Character') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -38,7 +38,7 @@ function CustomInjector:parse(id, widgets) Array.appendWith(widgets, Cell{name = 'Age', content = {args.age}}, Cell{name = 'Home World', content = {args.homeworld}}, - Title{name = 'Abilities'}, + Title{children = 'Abilities'}, Cell{name = 'Legend Type', content = {args.legendtype}}, Cell{name = 'Passive', content = {'[[File:' .. args.name .. ' - Passive.png|20px]] ' .. args.passive}}, Cell{name = 'Tactical', content = {'[[File:' .. args.name .. ' - Active.png|20px]] ' .. args.active}}, diff --git a/components/infobox/wikis/apexlegends/infobox_league_custom.lua b/components/infobox/wikis/apexlegends/infobox_league_custom.lua index 10b1bc18dce..a55681373ff 100644 --- a/components/infobox/wikis/apexlegends/infobox_league_custom.lua +++ b/components/infobox/wikis/apexlegends/infobox_league_custom.lua @@ -13,14 +13,13 @@ local Logic = require('Module:Logic') local Page = require('Module:Page') local String = require('Module:StringUtils') local Table = require('Module:Table') -local Tier = require('Module:Tier') local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') local Locale = Lua.import('Module:Locale') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -29,6 +28,13 @@ local GAME_MODE = mw.loadData('Module:GameMode') local EA_ICON = ' [[File:EA icon.png|x15px|middle|link=Electronic Arts|' .. 'Tournament sponsored by Electronirc Arts & Respawn.]]' +local EA_TIERS = { + major = '[[Major]]', + premier = '[[Premier]]', + challenger = '[[Challenger]]', + online = '[[Online]]', +} + ---@class ApexlegendsLeagueInfobox: InfoboxLeague local CustomLeague = Class.new(League) local CustomInjector = Class.new(Injector) @@ -71,17 +77,17 @@ function CustomInjector:parse(id, widgets) end table.insert(widgets, Cell{ name = 'EA tier', - content = {Tier['ea'][string.lower(args.eatier or '')]}, + content = {EA_TIERS[string.lower(args.eatier or '')]}, classes = {'tournament-highlighted-bg'} }) elseif id == 'customcontent' then --maps if String.isNotEmpty(args.map1) then - table.insert(widgets, Title{name = args.maptitle or 'Maps'}) - table.insert(widgets, Center{content = self.caller:_makeBasedListFromArgs('map')}) + table.insert(widgets, Title{children = args.maptitle or 'Maps'}) + table.insert(widgets, Center{children = self.caller:_makeBasedListFromArgs('map')}) elseif String.isNotEmpty(args['2map1']) then - table.insert(widgets, Title{name = args['2maptitle'] or '2v2 Maps'}) - table.insert(widgets, Center{content = self.caller:_makeBasedListFromArgs('2map')}) + table.insert(widgets, Title{children = args['2maptitle'] or '2v2 Maps'}) + table.insert(widgets, Center{children = self.caller:_makeBasedListFromArgs('2map')}) end end return widgets diff --git a/components/infobox/wikis/apexlegends/infobox_map_custom.lua b/components/infobox/wikis/apexlegends/infobox_map_custom.lua index d2444b71bbd..ca0b379577b 100644 --- a/components/infobox/wikis/apexlegends/infobox_map_custom.lua +++ b/components/infobox/wikis/apexlegends/infobox_map_custom.lua @@ -15,7 +15,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Map = Lua.import('Module:Infobox/Map') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local TableCell = Widgets.TableCell @@ -67,7 +67,7 @@ function CustomInjector:parse(id, widgets) end) Array.appendWith(widgets, - Title{name = 'Ring Information'}, + Title{children = 'Ring Information'}, ringTable ) end @@ -79,11 +79,11 @@ end function CustomMap:_createRingTableHeader() local headerRow = TableRow{css = {['font-weight'] = 'bold'}} -- bg needed return headerRow - :addCell(TableCell{content = {'Ring'}}) - :addCell(TableCell{content = {'Wait (s)'}}) - :addCell(TableCell{content = {'CloseTime (s)'}}) - :addCell(TableCell{content = {'Damageper tick'}}) - :addCell(TableCell{content = {'End Diameter (m)'}}) + :addCell(TableCell{children = {'Ring'}}) + :addCell(TableCell{children = {'Wait (s)'}}) + :addCell(TableCell{children = {'CloseTime (s)'}}) + :addCell(TableCell{children = {'Damageper tick'}}) + :addCell(TableCell{children = {'End Diameter (m)'}}) end ---@param ringData string @@ -91,7 +91,7 @@ end function CustomMap:_createRingTableRow(ringData) local row = TableRow{} for _, item in ipairs(mw.text.split(ringData, ',')) do - row:addCell(TableCell{content = {item}}) + row:addCell(TableCell{children = {item}}) end return row end diff --git a/components/infobox/wikis/apexlegends/infobox_person_player_custom.lua b/components/infobox/wikis/apexlegends/infobox_person_player_custom.lua index 706ccf7ec84..8d2ae1ddfce 100644 --- a/components/infobox/wikis/apexlegends/infobox_person_player_custom.lua +++ b/components/infobox/wikis/apexlegends/infobox_person_player_custom.lua @@ -21,7 +21,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local INPUTS = { diff --git a/components/infobox/wikis/apexlegends/infobox_team_custom.lua b/components/infobox/wikis/apexlegends/infobox_team_custom.lua index 27a0225d75d..a1cc29f01ae 100644 --- a/components/infobox/wikis/apexlegends/infobox_team_custom.lua +++ b/components/infobox/wikis/apexlegends/infobox_team_custom.lua @@ -13,7 +13,7 @@ local Template = require('Module:Template') local Injector = Lua.import('Module:Widget/Injector') local Team = Lua.import('Module:Infobox/Team') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class ApexlegendsInfoboxTeam: InfoboxTeam diff --git a/components/infobox/wikis/apexlegends/infobox_weapon_custom.lua b/components/infobox/wikis/apexlegends/infobox_weapon_custom.lua index 9ce094ec71f..06388cbef86 100644 --- a/components/infobox/wikis/apexlegends/infobox_weapon_custom.lua +++ b/components/infobox/wikis/apexlegends/infobox_weapon_custom.lua @@ -11,7 +11,7 @@ local Class = require('Module:Class') local Lua = require('Module:Lua') local String = require('Module:StringUtils') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -94,8 +94,8 @@ function CustomInjector:parse(id, widgets) for index, attachment in ipairs(self.caller:getAllArgsForBase(args, 'attachment')) do table.insert(attachments, self.caller:_createContextualNoWrappingSpan(attachment, index)) end - table.insert(widgets, Title{name = 'Attachment Slots'}) - table.insert(widgets, Center{content = {table.concat(attachments, ' ')}}) + table.insert(widgets, Title{children = 'Attachment Slots'}) + table.insert(widgets, Center{children = {table.concat(attachments, ' ')}}) end if String.isNotEmpty(args.hopup) then @@ -105,8 +105,8 @@ function CustomInjector:parse(id, widgets) table.insert(hopups, self.caller:_createContextualNoWrappingSpan(hopup, index)) table.insert(hopups, args['hopupdesc' .. index]) end - table.insert(widgets, Title{name = 'Hop-Ups'}) - table.insert(widgets, Center{content = {table.concat(hopups, '')}}) + table.insert(widgets, Title{children = 'Hop-Ups'}) + table.insert(widgets, Center{children = {table.concat(hopups, '')}}) end Array.appendWith( diff --git a/components/infobox/wikis/arenafps/infobox_league_custom.lua b/components/infobox/wikis/arenafps/infobox_league_custom.lua index 587295bb87d..b271939afd5 100644 --- a/components/infobox/wikis/arenafps/infobox_league_custom.lua +++ b/components/infobox/wikis/arenafps/infobox_league_custom.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -81,8 +81,8 @@ function CustomInjector:parse(id, widgets) )) end) - table.insert(widgets, Title{name = 'Maps'}) - table.insert(widgets, Center{content = {table.concat(maps, ' • ')}}) + table.insert(widgets, Title{children = 'Maps'}) + table.insert(widgets, Center{children = {table.concat(maps, ' • ')}}) end end return widgets diff --git a/components/infobox/wikis/arenafps/infobox_person_player_custom.lua b/components/infobox/wikis/arenafps/infobox_person_player_custom.lua index 5d12f9554f0..3e1df7f759a 100644 --- a/components/infobox/wikis/arenafps/infobox_person_player_custom.lua +++ b/components/infobox/wikis/arenafps/infobox_person_player_custom.lua @@ -16,7 +16,7 @@ local Table = require('Module:Table') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local ROLES = { diff --git a/components/infobox/wikis/brawlhalla/infobox_league_custom.lua b/components/infobox/wikis/brawlhalla/infobox_league_custom.lua index 64653477da8..a3fff3faf0e 100644 --- a/components/infobox/wikis/brawlhalla/infobox_league_custom.lua +++ b/components/infobox/wikis/brawlhalla/infobox_league_custom.lua @@ -14,7 +14,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -41,7 +41,7 @@ function CustomInjector:parse(id, widgets) if id == 'customcontent' then if not String.isEmpty(args.player_number) or not String.isEmpty(args.doubles_number) then - table.insert(widgets, Title{name = 'Player Breakdown'}) + table.insert(widgets, Title{children = 'Player Breakdown'}) table.insert(widgets, Cell{ name = 'Number of Players', content = {args.player_number} diff --git a/components/infobox/wikis/brawlhalla/infobox_person_player_custom.lua b/components/infobox/wikis/brawlhalla/infobox_person_player_custom.lua index 34b4ecf33ea..fbd92f59276 100644 --- a/components/infobox/wikis/brawlhalla/infobox_person_player_custom.lua +++ b/components/infobox/wikis/brawlhalla/infobox_person_player_custom.lua @@ -31,7 +31,7 @@ local CLEAN_OTHER_ROLES = { local CURRENT_YEAR = tonumber(os.date('%Y')) -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Injector = Lua.import('Module:Widget/Injector') local Cell = Widgets.Cell diff --git a/components/infobox/wikis/brawlstars/infobox_league_custom.lua b/components/infobox/wikis/brawlstars/infobox_league_custom.lua index 945027f9f1f..a091d053e76 100644 --- a/components/infobox/wikis/brawlstars/infobox_league_custom.lua +++ b/components/infobox/wikis/brawlstars/infobox_league_custom.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local SUPERCELL_SPONSORED_ICON = '[[File:Supercell lightmode.png|x18px|link=Supercell' diff --git a/components/infobox/wikis/brawlstars/infobox_map_custom.lua b/components/infobox/wikis/brawlstars/infobox_map_custom.lua index cc466352902..fdbc4ee20ae 100644 --- a/components/infobox/wikis/brawlstars/infobox_map_custom.lua +++ b/components/infobox/wikis/brawlstars/infobox_map_custom.lua @@ -15,7 +15,7 @@ local Table = require('Module:Table') local Injector = Lua.import('Module:Widget/Injector') local Map = Lua.import('Module:Infobox/Map') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Center = Widgets.Center local Title = Widgets.Title @@ -42,8 +42,8 @@ function CustomInjector:parse(id, widgets) local modes = self.caller:_getModes(args) Array.appendWith(widgets, Cell{name = 'Environment', content = {args.environment}}, - Title{name = modes and 'Mode' or nil}, - Center{content = modes and {table.concat(modes, '')} or nil} + Title{children = modes and 'Mode' or nil}, + Center{children = modes and {table.concat(modes, '')} or nil} ) end return widgets diff --git a/components/infobox/wikis/brawlstars/infobox_person_player_custom.lua b/components/infobox/wikis/brawlstars/infobox_person_player_custom.lua index 052f23097a3..19014cfc4f8 100644 --- a/components/infobox/wikis/brawlstars/infobox_person_player_custom.lua +++ b/components/infobox/wikis/brawlstars/infobox_person_player_custom.lua @@ -20,7 +20,7 @@ local Template = require('Module:Template') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local ROLES = { diff --git a/components/infobox/wikis/brawlstars/infobox_unit_brawler_custom.lua b/components/infobox/wikis/brawlstars/infobox_unit_brawler_custom.lua index fd60e363af6..f1345372870 100644 --- a/components/infobox/wikis/brawlstars/infobox_unit_brawler_custom.lua +++ b/components/infobox/wikis/brawlstars/infobox_unit_brawler_custom.lua @@ -15,7 +15,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Unit = Lua.import('Module:Infobox/Unit') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -39,7 +39,7 @@ end function CustomInjector:parse(id, widgets) local args = self.caller.args if id == 'caption' and not String.isEmpty(args.min) then - table.insert(widgets, Center{content = {args.quote}}) + table.insert(widgets, Center{children = {args.quote}}) elseif id == 'type' then return { Cell{name = 'Real Name', content = {args.realname}}, @@ -58,10 +58,10 @@ function CustomInjector:parse(id, widgets) Cell{name = 'Release Date', content = {args.releasedate}}, Cell{name = 'Health', content = {args.hp}}, Cell{name = 'Movespeed', content = {args.movespeed}}, - Title{name = 'Weapon & Super'}, + Title{children = 'Weapon & Super'}, Cell{name = 'Primary Weapon', content = {args.attack}}, Cell{name = 'Super Ability', content = {args.super}}, - Title{name = 'Gadgets & Star Powers'}, + Title{children = 'Gadgets & Star Powers'}, Cell{name = 'Gadgets', content = {args.gadget}}, Cell{name = 'Star Powers', content = {args.star}} ) diff --git a/components/infobox/wikis/callofduty/infobox_league_custom.lua b/components/infobox/wikis/callofduty/infobox_league_custom.lua index 219c62f80e1..463e1a96904 100644 --- a/components/infobox/wikis/callofduty/infobox_league_custom.lua +++ b/components/infobox/wikis/callofduty/infobox_league_custom.lua @@ -18,7 +18,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -54,8 +54,8 @@ function CustomInjector:parse(id, widgets) local maps = Array.map(self.caller:getAllArgsForBase(args, 'map'), function(map) return tostring(self.caller:_createNoWrappingSpan(PageLink.makeInternalLink(map))) end) - table.insert(widgets, Title{name = 'Maps'}) - table.insert(widgets, Center{content = {table.concat(maps, ' • ')}}) + table.insert(widgets, Title{children = 'Maps'}) + table.insert(widgets, Center{children = {table.concat(maps, ' • ')}}) end end return widgets diff --git a/components/infobox/wikis/callofduty/infobox_person_player_custom.lua b/components/infobox/wikis/callofduty/infobox_person_player_custom.lua index d7f56551cc4..47373c8a28a 100644 --- a/components/infobox/wikis/callofduty/infobox_person_player_custom.lua +++ b/components/infobox/wikis/callofduty/infobox_person_player_custom.lua @@ -16,7 +16,7 @@ local TeamHistoryAuto = require('Module:TeamHistoryAuto') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -60,9 +60,9 @@ function CustomInjector:parse(id, widgets) if String.isNotEmpty(manualHistory) or automatedHistory then return { - Title{name = 'History'}, - Center{content = {manualHistory}}, - Center{content = {automatedHistory}}, + Title{children = 'History'}, + Center{children = {manualHistory}}, + Center{children = {automatedHistory}}, } end elseif id == 'role' then diff --git a/components/infobox/wikis/clashofclans/infobox_building_custom.lua b/components/infobox/wikis/clashofclans/infobox_building_custom.lua index 73d0a4ad926..7be703b0eaf 100644 --- a/components/infobox/wikis/clashofclans/infobox_building_custom.lua +++ b/components/infobox/wikis/clashofclans/infobox_building_custom.lua @@ -14,7 +14,7 @@ local Table = require('Module:Table') local Injector = Lua.import('Module:Widget/Injector') local Building = Lua.import('Module:Infobox/Building') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -54,7 +54,7 @@ function CustomInjector:parse(id, widgets) ) if Table.any(args, function(key) return MODE_AVAILABILITY[key] end) then - table.insert(widgets, Title{name = 'Mode Availability'}) + table.insert(widgets, Title{children = 'Mode Availability'}) local modeAvailabilityOrder = function(tbl, a, b) return tbl[a].order < tbl[b].order end for key, item in Table.iter.spairs(MODE_AVAILABILITY, modeAvailabilityOrder) do table.insert(widgets, Cell{name = item.name, content = {args[key]}}) diff --git a/components/infobox/wikis/clashofclans/infobox_league_custom.lua b/components/infobox/wikis/clashofclans/infobox_league_custom.lua index 199a40e647d..aa09e06f0b8 100644 --- a/components/infobox/wikis/clashofclans/infobox_league_custom.lua +++ b/components/infobox/wikis/clashofclans/infobox_league_custom.lua @@ -13,7 +13,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local SUPERCELL_SPONSORED_ICON = '[[File:Supercell lightmode.png|x18px|link=Supercell' diff --git a/components/infobox/wikis/clashofclans/infobox_team_custom.lua b/components/infobox/wikis/clashofclans/infobox_team_custom.lua index 5dbb3a30c8e..2fad8d84ca0 100644 --- a/components/infobox/wikis/clashofclans/infobox_team_custom.lua +++ b/components/infobox/wikis/clashofclans/infobox_team_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Team = Lua.import('Module:Infobox/Team') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class ClashofClansInfoboxTeam: InfoboxTeam diff --git a/components/infobox/wikis/clashroyale/infobox_league_custom.lua b/components/infobox/wikis/clashroyale/infobox_league_custom.lua index c52dad7eca7..67e1f9122db 100644 --- a/components/infobox/wikis/clashroyale/infobox_league_custom.lua +++ b/components/infobox/wikis/clashroyale/infobox_league_custom.lua @@ -14,7 +14,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class ClashroyaleLeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/clashroyale/infobox_person_player_custom.lua b/components/infobox/wikis/clashroyale/infobox_person_player_custom.lua index affd1b0fd96..7051913ee21 100644 --- a/components/infobox/wikis/clashroyale/infobox_person_player_custom.lua +++ b/components/infobox/wikis/clashroyale/infobox_person_player_custom.lua @@ -13,7 +13,7 @@ local Role = require('Module:Role') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class ClashroyaleInfoboxPlayer: Person diff --git a/components/infobox/wikis/counterstrike/infobox_company_custom.lua b/components/infobox/wikis/counterstrike/infobox_company_custom.lua index fc6a6a4c324..078a4133d41 100644 --- a/components/infobox/wikis/counterstrike/infobox_company_custom.lua +++ b/components/infobox/wikis/counterstrike/infobox_company_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Company = Lua.import('Module:Infobox/Company') local Injector = Lua.import('Module:Widget/Injector') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class CounterstrikeCompanyInfobox: CompanyInfobox diff --git a/components/infobox/wikis/counterstrike/infobox_league_custom.lua b/components/infobox/wikis/counterstrike/infobox_league_custom.lua index 9f74d364e05..133ae146f24 100644 --- a/components/infobox/wikis/counterstrike/infobox_league_custom.lua +++ b/components/infobox/wikis/counterstrike/infobox_league_custom.lua @@ -25,7 +25,7 @@ local League = Lua.import('Module:Infobox/League') local ReferenceCleaner = Lua.import('Module:ReferenceCleaner') local Tier = Lua.import('Module:Tier/Custom') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -159,8 +159,8 @@ function CustomInjector:parse(id, widgets) Page.makeInternalLink({}, map, map .. game) ))) end - table.insert(widgets, Title{name = 'Maps'}) - table.insert(widgets, Center{content = {table.concat(maps, ' • ')}}) + table.insert(widgets, Title{children = 'Maps'}) + table.insert(widgets, Center{children = {table.concat(maps, ' • ')}}) end elseif id == 'liquipediatier' then table.insert( diff --git a/components/infobox/wikis/counterstrike/infobox_person_player_custom.lua b/components/infobox/wikis/counterstrike/infobox_person_player_custom.lua index f01b1d4e2d1..c1256d51fb5 100644 --- a/components/infobox/wikis/counterstrike/infobox_person_player_custom.lua +++ b/components/infobox/wikis/counterstrike/infobox_person_player_custom.lua @@ -17,7 +17,7 @@ local Game = Lua.import('Module:Game') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local BANNED = mw.loadData('Module:Banned') diff --git a/components/infobox/wikis/counterstrike/infobox_series_custom.lua b/components/infobox/wikis/counterstrike/infobox_series_custom.lua index 7f51106f4fb..240a1a6cb1e 100644 --- a/components/infobox/wikis/counterstrike/infobox_series_custom.lua +++ b/components/infobox/wikis/counterstrike/infobox_series_custom.lua @@ -16,7 +16,7 @@ local Game = Lua.import('Module:Game') local Injector = Lua.import('Module:Widget/Injector') local Series = Lua.import('Module:Infobox/Series') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class CounterstrikeSeriesInfobox: SeriesInfobox diff --git a/components/infobox/wikis/counterstrike/infobox_team_custom.lua b/components/infobox/wikis/counterstrike/infobox_team_custom.lua index f61898e65e4..c87d401978f 100644 --- a/components/infobox/wikis/counterstrike/infobox_team_custom.lua +++ b/components/infobox/wikis/counterstrike/infobox_team_custom.lua @@ -17,7 +17,7 @@ local Game = Lua.import('Module:Game') local Injector = Lua.import('Module:Widget/Injector') local Team = Lua.import('Module:Infobox/Team') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class CounterstrikeInfoboxTeam: InfoboxTeam diff --git a/components/infobox/wikis/criticalops/infobox_league_custom.lua b/components/infobox/wikis/criticalops/infobox_league_custom.lua index abb4689071b..2dba8d5906a 100644 --- a/components/infobox/wikis/criticalops/infobox_league_custom.lua +++ b/components/infobox/wikis/criticalops/infobox_league_custom.lua @@ -15,7 +15,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -53,8 +53,8 @@ function CustomInjector:parse(id, widgets) Page.makeInternalLink(map) ))) end - table.insert(widgets, Title{name = 'Maps'}) - table.insert(widgets, Center{content = {table.concat(maps, ' • ')}}) + table.insert(widgets, Title{children = 'Maps'}) + table.insert(widgets, Center{children = {table.concat(maps, ' • ')}}) end end diff --git a/components/infobox/wikis/criticalops/infobox_person_player_custom.lua b/components/infobox/wikis/criticalops/infobox_person_player_custom.lua index 94c783a8e6a..fde11e7203b 100644 --- a/components/infobox/wikis/criticalops/infobox_person_player_custom.lua +++ b/components/infobox/wikis/criticalops/infobox_person_player_custom.lua @@ -14,7 +14,7 @@ local TeamHistoryAuto = require('Module:TeamHistoryAuto') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local ROLES = { diff --git a/components/infobox/wikis/crossfire/infobox_league_custom.lua b/components/infobox/wikis/crossfire/infobox_league_custom.lua index 33df2e3b298..ecf28d1c9c0 100644 --- a/components/infobox/wikis/crossfire/infobox_league_custom.lua +++ b/components/infobox/wikis/crossfire/infobox_league_custom.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -47,13 +47,13 @@ function CustomInjector:parse(id, widgets) } elseif id == 'customcontent' then if args.player_number then - table.insert(widgets, Title{name = 'Players'}) + table.insert(widgets, Title{children = 'Players'}) table.insert(widgets, Cell{name = 'Number of players', content = {args.player_number}}) end --teams section if args.team_number then - table.insert(widgets, Title{name = 'Teams'}) + table.insert(widgets, Title{children = 'Teams'}) table.insert(widgets, Cell{name = 'Number of teams', content = {args.team_number}}) end end diff --git a/components/infobox/wikis/crossfire/infobox_person_player_custom.lua b/components/infobox/wikis/crossfire/infobox_person_player_custom.lua index 3d7cad43637..c17e0d5c005 100644 --- a/components/infobox/wikis/crossfire/infobox_person_player_custom.lua +++ b/components/infobox/wikis/crossfire/infobox_person_player_custom.lua @@ -12,7 +12,7 @@ local Role = require('Module:Role') local String = require('Module:StringUtils') local TeamHistoryAuto = require('Module:TeamHistoryAuto') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Title = Widgets.Title local Center = Widgets.Center @@ -47,9 +47,9 @@ function CustomInjector:parse(id, widgets) if String.isEmpty(manualHistory) and not automatedHistory then return {} end return { - Title{name = 'History'}, - Center{content = {manualHistory}}, - Center{content = {automatedHistory}}, + Title{children = 'History'}, + Center{children = {manualHistory}}, + Center{children = {automatedHistory}}, } elseif id == 'region' then return {} end diff --git a/components/infobox/wikis/deadlock/infobox_character_custom.lua b/components/infobox/wikis/deadlock/infobox_character_custom.lua index ccbd0346364..350f40afd3d 100644 --- a/components/infobox/wikis/deadlock/infobox_character_custom.lua +++ b/components/infobox/wikis/deadlock/infobox_character_custom.lua @@ -14,7 +14,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Character = Lua.import('Module:Infobox/Character') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -43,7 +43,7 @@ function CustomInjector:parse(id, widgets) if id == 'custom' then Array.appendWith( widgets, - Title{name = 'Vitality'}, + Title{children = 'Vitality'}, Cell{name = 'Health', content = {args.basehealth}}, Cell{name = 'Health Regeneration', content = {args.basehealthregen}}, Cell{name = 'Bullet Resistance', content = {args.resistancebullet .. '%'}}, @@ -55,7 +55,7 @@ function CustomInjector:parse(id, widgets) Array.appendWith( widgets, - Title{name = 'Weapon'}, + Title{children = 'Weapon'}, Cell{name = 'DPS', content = {args.dps}}, Cell{name = 'Bullet Damage', content = {args.damagebullet}}, Cell{name = 'Bullets per Seconds', content = {args.bps}}, diff --git a/components/infobox/wikis/dota2/infobox_company_custom.lua b/components/infobox/wikis/dota2/infobox_company_custom.lua index d783d13425f..512b49ad801 100644 --- a/components/infobox/wikis/dota2/infobox_company_custom.lua +++ b/components/infobox/wikis/dota2/infobox_company_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Company = Lua.import('Module:Infobox/Company') local Injector = Lua.import('Module:Widget/Injector') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class Dota2CompanyInfobox: CompanyInfobox diff --git a/components/infobox/wikis/dota2/infobox_cosmetic_custom.lua b/components/infobox/wikis/dota2/infobox_cosmetic_custom.lua index ca46e61e20d..d8379c26308 100644 --- a/components/infobox/wikis/dota2/infobox_cosmetic_custom.lua +++ b/components/infobox/wikis/dota2/infobox_cosmetic_custom.lua @@ -19,7 +19,7 @@ local Template = require('Module:Template') local Injector = Lua.import('Module:Widget/Injector') local Cosmetic = Lua.import('Module:Infobox/Cosmetic') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Builder = Widgets.Builder local Cell = Widgets.Cell local Center = Widgets.Center @@ -69,10 +69,10 @@ function CustomInjector:parse(id, widgets) args.slot and ('Slot: ' .. slotText) or nil, } }, - Center{content = { + Center{children = { CustomCosmetic._buyNow(Logic.readBool(args.marketable or true), args.defindex) }}, - Title{name = 'Extra Information'}, + Title{children = 'Extra Information'}, Cell{name = 'Created By', content = (args.creator == 'Valve' and {Template.safeExpand(mw.getCurrentFrame(), 'Valve icon')}) or self.caller:getAllArgsForBase(args, 'creator') @@ -99,7 +99,7 @@ function CustomInjector:parse(id, widgets) Cell{name = #orgins == 1 and 'Origin' or 'Origins', content = orgins} } end}, - Center{content = {args.description}}, + Center{children = {args.description}}, Center{name = '[[Trading|Tradable]]', classes = {'infobox-cosmetic-tradeable'}, content = { CustomCosmetic._ableText('TRADEABLE', args.tradeable, args.marketlock) }}, @@ -126,8 +126,8 @@ function CustomCosmetic._displaySet(setName, manualItems) end return { - Title{name = setName}, - Widgets.Highlights{content = Array.map(setItems, function(element) + Title{children = setName}, + Widgets.Highlights{children = Array.map(setItems, function(element) return '[['.. element ..']]' end)} } diff --git a/components/infobox/wikis/dota2/infobox_league_custom.lua b/components/infobox/wikis/dota2/infobox_league_custom.lua index 7d1a5e34588..85df211566a 100644 --- a/components/infobox/wikis/dota2/infobox_league_custom.lua +++ b/components/infobox/wikis/dota2/infobox_league_custom.lua @@ -18,7 +18,7 @@ local Template = require('Module:Template') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class Dota2LeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/dota2/infobox_lore_custom.lua b/components/infobox/wikis/dota2/infobox_lore_custom.lua index 9eda1922ac6..e107e3c357e 100644 --- a/components/infobox/wikis/dota2/infobox_lore_custom.lua +++ b/components/infobox/wikis/dota2/infobox_lore_custom.lua @@ -13,7 +13,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Lore = Lua.import('Module:Infobox/Lore') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -39,7 +39,7 @@ function CustomInjector:parse(id, widgets) if id == 'custom' then Array.appendWith(widgets, - Title{name = 'About'}, + Title{children = 'About'}, Cell{name = 'Names', content = {args.names}}, Cell{name = 'Examples', content = {args.examples}}, Cell{name = 'Members', content = {args.members}}, @@ -50,7 +50,7 @@ function CustomInjector:parse(id, widgets) Cell{name = 'Age', content = {args.age}}, Cell{name = 'Race/Species', content = {args['race/species']}}, Cell{name = 'Faction/s', content = {args['faction/s']}}, - Title{name = 'Associated With'}, + Title{children = 'Associated With'}, Cell{name = 'Heroes', content = {args.heroes}}, Cell{name = 'Races', content = {args.race}}, Cell{name = 'Factions', content = {args.factions}}, diff --git a/components/infobox/wikis/dota2/infobox_patch_custom.lua b/components/infobox/wikis/dota2/infobox_patch_custom.lua index ae1ee982e17..80cc6776b4d 100644 --- a/components/infobox/wikis/dota2/infobox_patch_custom.lua +++ b/components/infobox/wikis/dota2/infobox_patch_custom.lua @@ -11,7 +11,7 @@ local Lua = require('Module:Lua') local Patch = Lua.import('Module:Infobox/Patch') local Injector = Lua.import('Module:Widget/Injector') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') ---@class Dota2PatchInfobox: PatchInfobox local CustomPatch = Class.new(Patch) diff --git a/components/infobox/wikis/dota2/infobox_person_player_custom.lua b/components/infobox/wikis/dota2/infobox_person_player_custom.lua index 2f26c393050..779abc12056 100644 --- a/components/infobox/wikis/dota2/infobox_person_player_custom.lua +++ b/components/infobox/wikis/dota2/infobox_person_player_custom.lua @@ -23,7 +23,7 @@ local Flags = Lua.import('Module:Flags') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -130,16 +130,16 @@ function CustomInjector:parse(id, widgets) } elseif id == 'history' then if not String.isEmpty(args.history_iwo) then - table.insert(widgets, Title{name = '[[Intel World Open|Intel World Open]] History'}) - table.insert(widgets, Center{content = {args.history_iwo}}) + table.insert(widgets, Title{children = '[[Intel World Open|Intel World Open]] History'}) + table.insert(widgets, Center{children = {args.history_iwo}}) end if not String.isEmpty(args.history_gfinity) then - table.insert(widgets, Title{name = '[[Gfinity/Elite_Series|Gfinity Elite Series]] History'}) - table.insert(widgets, Center{content = {args.history_gfinity}}) + table.insert(widgets, Title{children = '[[Gfinity/Elite_Series|Gfinity Elite Series]] History'}) + table.insert(widgets, Center{children = {args.history_gfinity}}) end if not String.isEmpty(args.history_odl) then - table.insert(widgets, Title{name = '[[Oceania Draft League|Oceania Draft League]] History'}) - table.insert(widgets, Center{content = {args.history_odl}}) + table.insert(widgets, Title{children = '[[Oceania Draft League|Oceania Draft League]] History'}) + table.insert(widgets, Center{children = {args.history_odl}}) end elseif id == 'role' then return { diff --git a/components/infobox/wikis/dota2/infobox_person_user_custom.lua b/components/infobox/wikis/dota2/infobox_person_user_custom.lua index 56886256feb..2b38d7e5ccd 100644 --- a/components/infobox/wikis/dota2/infobox_person_user_custom.lua +++ b/components/infobox/wikis/dota2/infobox_person_user_custom.lua @@ -16,7 +16,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local User = Lua.import('Module:Infobox/Person/User') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -56,8 +56,8 @@ function CustomInjector:parse(id, widgets) ) if not String.isEmpty(args['fav-team-1']) then Array.appendWith(widgets, - Title{name = 'Favorite teams'}, - Center{content = {self.caller:_getFavouriteTeams()}} + Title{children = 'Favorite teams'}, + Center{children = {self.caller:_getFavouriteTeams()}} ) end elseif @@ -65,9 +65,9 @@ function CustomInjector:parse(id, widgets) not (String.isEmpty(args.team_history) and String.isEmpty(args.clan_history)) then return { - Title{ name = 'History' }, - Center{content = {args.team_history}}, - Center{content = {args.clan_history}}, + Title{children = 'History' }, + Center{children = {args.team_history}}, + Center{children = {args.clan_history}}, } end diff --git a/components/infobox/wikis/dota2/infobox_series_custom.lua b/components/infobox/wikis/dota2/infobox_series_custom.lua index a24d53f9920..2078633f22b 100644 --- a/components/infobox/wikis/dota2/infobox_series_custom.lua +++ b/components/infobox/wikis/dota2/infobox_series_custom.lua @@ -14,7 +14,7 @@ local Injector = Lua.import('Module:Widget/Injector') local Series = Lua.import('Module:Infobox/Series') local Flags = Lua.import('Module:Flags') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class DotaSeriesInfobox: SeriesInfobox diff --git a/components/infobox/wikis/easportsfc/infobox_company_custom.lua b/components/infobox/wikis/easportsfc/infobox_company_custom.lua index ecf3a4681bf..e26a7c485a7 100644 --- a/components/infobox/wikis/easportsfc/infobox_company_custom.lua +++ b/components/infobox/wikis/easportsfc/infobox_company_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Company = Lua.import('Module:Infobox/Company') local Injector = Lua.import('Module:Widget/Injector') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class EafcCompanyInfobox: CompanyInfobox diff --git a/components/infobox/wikis/easportsfc/infobox_league_custom.lua b/components/infobox/wikis/easportsfc/infobox_league_custom.lua index 89fd94731dc..fffdd969806 100644 --- a/components/infobox/wikis/easportsfc/infobox_league_custom.lua +++ b/components/infobox/wikis/easportsfc/infobox_league_custom.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class EafcLeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/easportsfc/infobox_person_player_custom.lua b/components/infobox/wikis/easportsfc/infobox_person_player_custom.lua index e4a92fcc1f2..e218016f6b9 100644 --- a/components/infobox/wikis/easportsfc/infobox_person_player_custom.lua +++ b/components/infobox/wikis/easportsfc/infobox_person_player_custom.lua @@ -13,7 +13,7 @@ local Page = require('Module:Page') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class EafcInfoboxPlayer: Person diff --git a/components/infobox/wikis/esports/infobox_game_custom.lua b/components/infobox/wikis/esports/infobox_game_custom.lua index 7e2c2b814c7..0035447b367 100644 --- a/components/infobox/wikis/esports/infobox_game_custom.lua +++ b/components/infobox/wikis/esports/infobox_game_custom.lua @@ -15,7 +15,7 @@ local Game = Lua.import('Module:Infobox/Game') local Injector = Lua.import('Module:Widget/Injector') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class EsportsGameInfobox: GameInfobox diff --git a/components/infobox/wikis/fighters/infobox_league_custom.lua b/components/infobox/wikis/fighters/infobox_league_custom.lua index 4bcb6c8d5c1..f845ea67f1c 100644 --- a/components/infobox/wikis/fighters/infobox_league_custom.lua +++ b/components/infobox/wikis/fighters/infobox_league_custom.lua @@ -20,7 +20,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Chronology = Widgets.Chronology @@ -151,7 +151,7 @@ function CustomInjector:parse(id, widgets) ) elseif id == 'customcontent' then if args.circuit or args.points or args.circuit_next or args.circuit_previous then - table.insert(widgets, Title{name = 'Circuit Information'}) + table.insert(widgets, Title{children = 'Circuit Information'}) self.caller:_createCircuitInformation(widgets) end if args.circuit2 or args.points2 or args.circuit2_next or args.circuit2_previous then @@ -326,7 +326,7 @@ function CustomLeague:_createCircuitInformation(widgets, circuitIndex) Cell{name = 'Circuit Tier', content = {circuitArgs.tier}}, Cell{name = 'Tournament Region', content = {circuitArgs.region}}, Cell{name = 'Points', content = {circuitArgs.points}}, - Chronology{content = {next = circuitArgs.next, previous = circuitArgs.previous}} + Chronology{links = {next = circuitArgs.next, previous = circuitArgs.previous}} ) end diff --git a/components/infobox/wikis/fighters/infobox_person_player_custom.lua b/components/infobox/wikis/fighters/infobox_person_player_custom.lua index fdd25ef182d..94ac4a699f1 100644 --- a/components/infobox/wikis/fighters/infobox_person_player_custom.lua +++ b/components/infobox/wikis/fighters/infobox_person_player_custom.lua @@ -15,7 +15,7 @@ local YearsActive = require('Module:YearsActive') -- TODO Convert to use the com local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class FightersInfoboxPlayer: Person diff --git a/components/infobox/wikis/formula1/infobox_company_custom.lua b/components/infobox/wikis/formula1/infobox_company_custom.lua index ff98d2e740d..65ea611fa50 100644 --- a/components/infobox/wikis/formula1/infobox_company_custom.lua +++ b/components/infobox/wikis/formula1/infobox_company_custom.lua @@ -13,7 +13,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Company = Lua.import('Module:Infobox/Company') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -51,7 +51,7 @@ function CustomInjector:parse(id, widgets) end return Array.extendWith(widgets, - {Title{name = 'Staff Information'}}, + {Title{children = 'Staff Information'}}, Array.map(staffInfoCells, function(cellData) return Cell{name = cellData.name, content = {args[cellData.key]}} end) diff --git a/components/infobox/wikis/formula1/infobox_league_custom.lua b/components/infobox/wikis/formula1/infobox_league_custom.lua index bc34fde4d4b..ae846994925 100644 --- a/components/infobox/wikis/formula1/infobox_league_custom.lua +++ b/components/infobox/wikis/formula1/infobox_league_custom.lua @@ -13,7 +13,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class Formula1LeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/formula1/infobox_map_custom.lua b/components/infobox/wikis/formula1/infobox_map_custom.lua index 5fa1c90b1cf..4b5af875116 100644 --- a/components/infobox/wikis/formula1/infobox_map_custom.lua +++ b/components/infobox/wikis/formula1/infobox_map_custom.lua @@ -13,7 +13,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Map = Lua.import('Module:Infobox/Map') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class Formula1MapInfobox: MapInfobox diff --git a/components/infobox/wikis/formula1/infobox_person_player_custom.lua b/components/infobox/wikis/formula1/infobox_person_player_custom.lua index 1740af253e7..e55db355603 100644 --- a/components/infobox/wikis/formula1/infobox_person_player_custom.lua +++ b/components/infobox/wikis/formula1/infobox_person_player_custom.lua @@ -15,7 +15,7 @@ local TeamHistoryAuto = require('Module:TeamHistoryAuto') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -48,8 +48,8 @@ function CustomInjector:parse(id, widgets) table.insert(widgets, Cell{name = 'Abbreviations', content = {args.abbreviations}}) elseif id == 'history' then return { - Title{name = 'History'}, - Center{content = {TeamHistoryAuto.results{ + Title{children = 'History'}, + Center{children = {TeamHistoryAuto.results{ convertrole = true, addlpdbdata = true }}}, @@ -90,7 +90,7 @@ function CustomPlayer:addCustomCells(widgets) end return Array.extendWith(widgets, - {Title{name = 'F1 Driver Statistics'}}, + {Title{children = 'F1 Driver Statistics'}}, Array.map(statisticsCells, function(cellData) return Cell{name = cellData.name, content = {args[cellData.key]}} end) diff --git a/components/infobox/wikis/formula1/infobox_series_custom.lua b/components/infobox/wikis/formula1/infobox_series_custom.lua index 84d0c48be57..b1aba5573bf 100644 --- a/components/infobox/wikis/formula1/infobox_series_custom.lua +++ b/components/infobox/wikis/formula1/infobox_series_custom.lua @@ -13,7 +13,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Series = Lua.import('Module:Infobox/Series') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class Formula1SeriesInfobox: SeriesInfobox diff --git a/components/infobox/wikis/formula1/infobox_team_custom.lua b/components/infobox/wikis/formula1/infobox_team_custom.lua index 1e69cb6eb68..aae2d55fe50 100644 --- a/components/infobox/wikis/formula1/infobox_team_custom.lua +++ b/components/infobox/wikis/formula1/infobox_team_custom.lua @@ -15,7 +15,7 @@ local TeamTemplates = require('Module:Team') local Injector = Lua.import('Module:Widget/Injector') local Team = Lua.import('Module:Infobox/Team') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -61,16 +61,16 @@ function CustomInjector:parse(id, widgets) return TeamTemplates.team(nil, team) end) Array.extendWith(widgets, - {Title{name = 'Academy Team' .. (Table.size(academyTeams) > 1 and 's' or '')}}, - Array.map(academyTeams, function(academyTeam) return Center{content = {academyTeam}} end) + {Title{children = 'Academy Team' .. (Table.size(academyTeams) > 1 and 's' or '')}}, + Array.map(academyTeams, function(academyTeam) return Center{children = {academyTeam}} end) ) end if args.previous or args.next then Array.appendWith( widgets, - Title{name = 'Chronology'}, - Chronology{content = { + Title{children = 'Chronology'}, + Chronology{links = { previous = args.previous, previous2 = args.previous2, next = args.next, @@ -89,7 +89,7 @@ function CustomTeam._statisticsCells(args) if Array.all(STATISTICS, function(statsData) return args[statsData.key] == nil end) then return {} end - local widgets = {Title{name = 'Team Statistics'}} + local widgets = {Title{children = 'Team Statistics'}} Array.forEach(STATISTICS, function(statsData) table.insert(widgets, Cell{name = statsData.name, content = {args[statsData.key]}}) end) diff --git a/components/infobox/wikis/formula1/infobox_unit_car_custom.lua b/components/infobox/wikis/formula1/infobox_unit_car_custom.lua index 34eac18654c..31d028d5c06 100644 --- a/components/infobox/wikis/formula1/infobox_unit_car_custom.lua +++ b/components/infobox/wikis/formula1/infobox_unit_car_custom.lua @@ -15,7 +15,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Unit = Lua.import('Module:Infobox/Unit') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Chronology = Widgets.Chronology local Title = Widgets.Title @@ -56,8 +56,8 @@ function CustomInjector:parse(id, widgets) elseif id == 'customcontent' then if String.isEmpty(args.previous) and String.isEmpty(args.next) then return widgets end return { - Title{name = 'Chronology'}, - Chronology{content = {previous = args.previous, next = args.next}} + Title{children = 'Chronology'}, + Chronology{links = {previous = args.previous, next = args.next}} } end return widgets diff --git a/components/infobox/wikis/formula1/infobox_unit_engine_custom.lua b/components/infobox/wikis/formula1/infobox_unit_engine_custom.lua index 4b16a932cc3..393b12b7203 100644 --- a/components/infobox/wikis/formula1/infobox_unit_engine_custom.lua +++ b/components/infobox/wikis/formula1/infobox_unit_engine_custom.lua @@ -15,7 +15,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Unit = Lua.import('Module:Infobox/Unit') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Chronology = Widgets.Chronology local Title = Widgets.Title @@ -44,12 +44,12 @@ function CustomInjector:parse(id, widgets) Cell{name = 'Manufacturer', content = {args.manufacturer}}, Cell{name = 'Production', content = {args.production}}, Cell{name = 'Weight', content = {args.weight}}, - Title{name = 'Engine Output'}, + Title{children = 'Engine Output'}, Cell{name = 'Power', content = {args.power}}, Cell{name = 'Torque', content = {args.torque}}, Cell{name = 'Idle RPM', content = {args.idlerpm}}, Cell{name = 'Peak RPM', content = {args.peakrpm}}, - Title{name = 'Engine Layout'}, + Title{children = 'Engine Layout'}, Cell{name = 'Configuration', content = {args.configuration}}, Cell{name = 'Displacement', content = {args.displacement}}, Cell{name = 'Compression', content = {args.compression}}, @@ -59,8 +59,8 @@ function CustomInjector:parse(id, widgets) elseif id == 'customcontent' then if String.isEmpty(args.previous) and String.isEmpty(args.next) then return widgets end return { - Title{name = 'Chronology'}, - Chronology{content = {previous = args.previous, next = args.next}} + Title{children = 'Chronology'}, + Chronology{links = {previous = args.previous, next = args.next}} } end return widgets diff --git a/components/infobox/wikis/fortnite/infobox_league_custom.lua b/components/infobox/wikis/fortnite/infobox_league_custom.lua index 83851f449a0..cce77203a22 100644 --- a/components/infobox/wikis/fortnite/infobox_league_custom.lua +++ b/components/infobox/wikis/fortnite/infobox_league_custom.lua @@ -15,7 +15,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class FortniteLeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/fortnite/infobox_person_player_custom.lua b/components/infobox/wikis/fortnite/infobox_person_player_custom.lua index f8d33e388ae..f051448539a 100644 --- a/components/infobox/wikis/fortnite/infobox_person_player_custom.lua +++ b/components/infobox/wikis/fortnite/infobox_person_player_custom.lua @@ -19,7 +19,7 @@ local TeamHistoryAuto = require('Module:TeamHistoryAuto') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -82,9 +82,9 @@ function CustomInjector:parse(id, widgets) if String.isNotEmpty(manualHistory) or automatedHistory then return { - Title{name = 'History'}, - Center{content = {manualHistory}}, - Center{content = {automatedHistory}}, + Title{children = 'History'}, + Center{children = {manualHistory}}, + Center{children = {automatedHistory}}, } end elseif id == 'region' then return {} diff --git a/components/infobox/wikis/fortnite/infobox_team_custom.lua b/components/infobox/wikis/fortnite/infobox_team_custom.lua index 7893d300960..27aab81fc56 100644 --- a/components/infobox/wikis/fortnite/infobox_team_custom.lua +++ b/components/infobox/wikis/fortnite/infobox_team_custom.lua @@ -20,7 +20,7 @@ local Team = Lua.import('Module:Infobox/Team') local OpponentLibraries = require('Module:OpponentLibraries') local Opponent = OpponentLibraries.Opponent -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Condition = require('Module:Condition') diff --git a/components/infobox/wikis/freefire/infobox_league_custom.lua b/components/infobox/wikis/freefire/infobox_league_custom.lua index f0a8e85fe31..7bce797784c 100644 --- a/components/infobox/wikis/freefire/infobox_league_custom.lua +++ b/components/infobox/wikis/freefire/infobox_league_custom.lua @@ -14,7 +14,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class FreefireLeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/freefire/infobox_person_player_custom.lua b/components/infobox/wikis/freefire/infobox_person_player_custom.lua index 9559a0a0b55..402507d8ee9 100644 --- a/components/infobox/wikis/freefire/infobox_person_player_custom.lua +++ b/components/infobox/wikis/freefire/infobox_person_player_custom.lua @@ -17,7 +17,7 @@ local Template = require('Module:Template') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -82,9 +82,9 @@ function CustomInjector:parse(id, widgets) if String.isNotEmpty(manualHistory) or automatedHistory then return { - Title{name = 'History'}, - Center{content = {manualHistory}}, - Center{content = {automatedHistory}}, + Title{children = 'History'}, + Center{children = {manualHistory}}, + Center{children = {automatedHistory}}, } end elseif id == 'role' then diff --git a/components/infobox/wikis/freefire/infobox_team_custom.lua b/components/infobox/wikis/freefire/infobox_team_custom.lua index 16aebe79c22..6c72397c9fb 100644 --- a/components/infobox/wikis/freefire/infobox_team_custom.lua +++ b/components/infobox/wikis/freefire/infobox_team_custom.lua @@ -14,7 +14,7 @@ local Template = require('Module:Template') local Injector = Lua.import('Module:Widget/Injector') local Team = Lua.import('Module:Infobox/Team') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class FreefireInfoboxTeam: InfoboxTeam diff --git a/components/infobox/wikis/halo/infobox_league_custom.lua b/components/infobox/wikis/halo/infobox_league_custom.lua index 5529f17d5a5..e8a60a58b14 100644 --- a/components/infobox/wikis/halo/infobox_league_custom.lua +++ b/components/infobox/wikis/halo/infobox_league_custom.lua @@ -20,7 +20,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -57,8 +57,8 @@ function CustomInjector:parse(id, widgets) } elseif id == 'customcontent' and String.isNotEmpty(args.map1) then Array.appendWith(widgets, - Title{name = 'Maps'}, - Center{content = self.caller:_makeMapList()} + Title{children = 'Maps'}, + Center{children = self.caller:_makeMapList()} ) end return widgets diff --git a/components/infobox/wikis/halo/infobox_map_custom.lua b/components/infobox/wikis/halo/infobox_map_custom.lua index f75ffe6a756..918e0ca1e70 100644 --- a/components/infobox/wikis/halo/infobox_map_custom.lua +++ b/components/infobox/wikis/halo/infobox_map_custom.lua @@ -16,7 +16,7 @@ local Game = Lua.import('Module:Game') local Injector = Lua.import('Module:Widget/Injector') local Map = Lua.import('Module:Infobox/Map') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class HaloMapInfobox: MapInfobox diff --git a/components/infobox/wikis/halo/infobox_patch_custom.lua b/components/infobox/wikis/halo/infobox_patch_custom.lua index cf119e2aba5..23e4e3738b9 100644 --- a/components/infobox/wikis/halo/infobox_patch_custom.lua +++ b/components/infobox/wikis/halo/infobox_patch_custom.lua @@ -13,7 +13,7 @@ local Game = Lua.import('Module:Game') local Injector = Lua.import('Module:Widget/Injector') local Patch = Lua.import('Module:Infobox/Patch') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@Class HaloPatchInfobox: PatchInfobox diff --git a/components/infobox/wikis/halo/infobox_person_player_custom.lua b/components/infobox/wikis/halo/infobox_person_player_custom.lua index 90b574da95d..58a59dbf62f 100644 --- a/components/infobox/wikis/halo/infobox_person_player_custom.lua +++ b/components/infobox/wikis/halo/infobox_person_player_custom.lua @@ -17,7 +17,7 @@ local TeamHistoryAuto = require('Module:TeamHistoryAuto') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -61,9 +61,9 @@ function CustomInjector:parse(id, widgets) if String.isNotEmpty(args.history) or automatedHistory then return { - Title{name = 'History'}, - Center{content = {args.history}}, - Center{content = {automatedHistory}}, + Title{children = 'History'}, + Center{children = {args.history}}, + Center{children = {automatedHistory}}, } end elseif id == 'region' then return {} diff --git a/components/infobox/wikis/hearthstone/infobox_company_custom.lua b/components/infobox/wikis/hearthstone/infobox_company_custom.lua index a9dc491f804..31849117ee9 100644 --- a/components/infobox/wikis/hearthstone/infobox_company_custom.lua +++ b/components/infobox/wikis/hearthstone/infobox_company_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Company = Lua.import('Module:Infobox/Company') local Injector = Lua.import('Module:Widget/Injector') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class HearthstoneCompanyInfobox: CompanyInfobox diff --git a/components/infobox/wikis/hearthstone/infobox_league_custom.lua b/components/infobox/wikis/hearthstone/infobox_league_custom.lua index 6f315f96eca..3fec0bfb1f3 100644 --- a/components/infobox/wikis/hearthstone/infobox_league_custom.lua +++ b/components/infobox/wikis/hearthstone/infobox_league_custom.lua @@ -16,7 +16,7 @@ local InfoboxPrizePool = Lua.import('Module:Infobox/Extensions/PrizePool') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class HearthstoneLeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/hearthstone/infobox_person_player_custom.lua b/components/infobox/wikis/hearthstone/infobox_person_player_custom.lua index 1f99bbfad01..b486a814aac 100644 --- a/components/infobox/wikis/hearthstone/infobox_person_player_custom.lua +++ b/components/infobox/wikis/hearthstone/infobox_person_player_custom.lua @@ -6,27 +6,41 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- +local ActiveYears = require('Module:YearsActive') local Class = require('Module:Class') local Lua = require('Module:Lua') +local Role = require('Module:Role') +local Region = require('Module:Region') +local Math = require('Module:MathUtil') +local String = require('Module:StringUtils') +local TeamHistoryAuto = require('Module:TeamHistoryAuto') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell +local Title = Widgets.Title +local Center = Widgets.Center + +local CURRENT_YEAR = tonumber(os.date('%Y')) ---@class HearthstoneInfoboxPlayer: Person +---@field role table +---@field role2 table local CustomPlayer = Class.new(Player) local CustomInjector = Class.new(Injector) -local GM_ICON = '[[File:HS grandmastersIconSmall.png|x15px|link=Grandmasters]] ' - ---@param frame Frame ---@return Html function CustomPlayer.run(frame) local player = CustomPlayer(frame) player:setWidgetInjector(CustomInjector(player)) + player.args.autoTeam = true + player.role = Role.run{role = player.args.role} + player.role2 = Role.run{role = player.args.role2} + return player:createInfobox() end @@ -34,14 +48,57 @@ end ---@param widgets Widget[] ---@return Widget[] function CustomInjector:parse(id, widgets) - local args = self.caller.args + local caller = self.caller + local args = caller.args if id == 'custom' then - local grandMaster = args.grandmasters and (GM_ICON .. args.grandmasters) or nil - table.insert(widgets, Cell{name = 'Grandmasters', content = {grandMaster}}) - end + local yearsActive = ActiveYears.display{player = caller.pagename} + + local currentYearEarnings = caller.earningsPerYear[CURRENT_YEAR] + if currentYearEarnings then + currentYearEarnings = Math.round(currentYearEarnings) + currentYearEarnings = '$' .. mw.getContentLanguage():formatNum(currentYearEarnings) + end + + return { + Cell{name = 'Approx. Winnings ' .. CURRENT_YEAR, content = {currentYearEarnings}}, + Cell{name = 'Years active', content = {yearsActive}}, + } + elseif id == 'history' then + local manualHistory = args.history + local automatedHistory = TeamHistoryAuto.results{ + addlpdbdata = true, + convertrole = true, + player = self.caller.pagename + } + if String.isNotEmpty(manualHistory) or automatedHistory then + return { + Title{children = 'History'}, + Center{children = {manualHistory}}, + Center{children = {automatedHistory}}, + } + end + elseif id == 'region' then return {} + elseif id == 'role' then + return { + Cell{name = 'Role(s)', content = {caller.role.display, caller.role2.display}} + } + end return widgets end +---@param lpdbData table +---@param args table +---@param personType string +---@return table +function CustomPlayer:adjustLPDB(lpdbData, args, personType) + lpdbData.extradata.role = (self.role or {}).variable + lpdbData.extradata.role2 = (self.role2 or {}).variable + + lpdbData.region = String.nilIfEmpty(Region.name({region = args.region, country = args.country})) + + return lpdbData +end + return CustomPlayer diff --git a/components/infobox/wikis/heroes/infobox_league_custom.lua b/components/infobox/wikis/heroes/infobox_league_custom.lua index 9dc3d945310..e3401af80b5 100644 --- a/components/infobox/wikis/heroes/infobox_league_custom.lua +++ b/components/infobox/wikis/heroes/infobox_league_custom.lua @@ -15,7 +15,7 @@ local PageLink = require('Module:Page') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Center = Widgets.Center local Title = Widgets.Title @@ -54,8 +54,8 @@ function CustomInjector:parse(id, widgets) end) if #maps > 0 then - table.insert(widgets, Title{name = 'Battlegrounds'}) - table.insert(widgets, Center{content = {table.concat(maps, ' • ')}}) + table.insert(widgets, Title{children = 'Battlegrounds'}) + table.insert(widgets, Center{children = {table.concat(maps, ' • ')}}) end end diff --git a/components/infobox/wikis/heroes/infobox_person_player_custom.lua b/components/infobox/wikis/heroes/infobox_person_player_custom.lua index 1009f944089..c619082f4b8 100644 --- a/components/infobox/wikis/heroes/infobox_person_player_custom.lua +++ b/components/infobox/wikis/heroes/infobox_person_player_custom.lua @@ -15,7 +15,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local SIZE_HERO = '28x28px' diff --git a/components/infobox/wikis/arenaofvalor/infobox_league_custom.lua b/components/infobox/wikis/honorofkings/infobox_league_custom.lua similarity index 90% rename from components/infobox/wikis/arenaofvalor/infobox_league_custom.lua rename to components/infobox/wikis/honorofkings/infobox_league_custom.lua index 69f0ee92cdb..75dd62e426e 100644 --- a/components/infobox/wikis/arenaofvalor/infobox_league_custom.lua +++ b/components/infobox/wikis/honorofkings/infobox_league_custom.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- wiki=arenaofvalor +-- wiki=honorofkings -- page=Module:Infobox/League/Custom -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute @@ -16,11 +16,11 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title ----@class ArenaofvalorLeagueInfobox: InfoboxLeague +---@class HonorofkingsLeagueInfobox: InfoboxLeague local CustomLeague = Class.new(League) local CustomInjector = Class.new(Injector) @@ -47,13 +47,13 @@ function CustomInjector:parse(id, widgets) } elseif id == 'customcontent' then if args.player_number then - table.insert(widgets, Title{name = 'Players'}) + table.insert(widgets, Title{children = 'Players'}) table.insert(widgets, Cell{name = 'Number of players', content = {args.player_number}}) end --teams section if args.team_number then - table.insert(widgets, Title{name = 'Teams'}) + table.insert(widgets, Title{children = 'Teams'}) table.insert(widgets, Cell{name = 'Number of teams', content = {args.team_number}}) end end diff --git a/components/infobox/wikis/arenaofvalor/infobox_person_player_custom.lua b/components/infobox/wikis/honorofkings/infobox_person_player_custom.lua similarity index 94% rename from components/infobox/wikis/arenaofvalor/infobox_person_player_custom.lua rename to components/infobox/wikis/honorofkings/infobox_person_player_custom.lua index b4bce243b71..010455adfc1 100644 --- a/components/infobox/wikis/arenaofvalor/infobox_person_player_custom.lua +++ b/components/infobox/wikis/honorofkings/infobox_person_player_custom.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- wiki=arenaofvalor +-- wiki=honorofkings -- page=Module:Infobox/Person/Player/Custom -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute @@ -17,12 +17,12 @@ local Table = require('Module:Table') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local SIZE_HERO = '25x25px' ----@class ArenaofvalorInfoboxPlayer: Person +---@class HonorofkingsInfoboxPlayer: Person ---@field role table ---@field role2 table local CustomPlayer = Class.new(Player) @@ -55,7 +55,7 @@ function CustomInjector:parse(id, widgets) local standardizedHero = HeroNames[hero:lower()] if not standardizedHero then -- we have an invalid hero entry add warning (including tracking category) - table.insert(caller.infobox.warnings, + table.insert(caller.warnings, 'Invalid hero input "' .. hero .. '"[[Category:Pages with invalid hero input]]') end return CharacterIcon.Icon{character = standardizedHero or hero, size = SIZE_HERO} diff --git a/components/infobox/wikis/arenaofvalor/infobox_team_custom.lua b/components/infobox/wikis/honorofkings/infobox_team_custom.lua similarity index 96% rename from components/infobox/wikis/arenaofvalor/infobox_team_custom.lua rename to components/infobox/wikis/honorofkings/infobox_team_custom.lua index 56025394529..f71e370e933 100644 --- a/components/infobox/wikis/arenaofvalor/infobox_team_custom.lua +++ b/components/infobox/wikis/honorofkings/infobox_team_custom.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- wiki=arenaofvalor +-- wiki=honorofkings -- page=Module:Infobox/Team/Custom -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute @@ -34,7 +34,7 @@ local REGION_REMAPPINGS = { ['japan'] = 'asia', } ----@class ArenaofvalorInfoboxTeam: InfoboxTeam +---@class HonorofkingsInfoboxTeam: InfoboxTeam local CustomTeam = Class.new(Team) ---@param frame Frame diff --git a/components/infobox/wikis/leagueoflegends/infobox_league_custom.lua b/components/infobox/wikis/leagueoflegends/infobox_league_custom.lua index 3570b52c3fe..5719b366556 100644 --- a/components/infobox/wikis/leagueoflegends/infobox_league_custom.lua +++ b/components/infobox/wikis/leagueoflegends/infobox_league_custom.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local RIOT_ICON = '[[File:Riot Games Tier Icon.png|x12px|link=Riot Games|Premier Tournament held by Riot Games]]' diff --git a/components/infobox/wikis/leagueoflegends/infobox_person_player_custom.lua b/components/infobox/wikis/leagueoflegends/infobox_person_player_custom.lua index b1d87ab2084..cfc0db361f7 100644 --- a/components/infobox/wikis/leagueoflegends/infobox_person_player_custom.lua +++ b/components/infobox/wikis/leagueoflegends/infobox_person_player_custom.lua @@ -20,7 +20,7 @@ local Template = require('Module:Template') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local ROLES = { diff --git a/components/infobox/wikis/leagueoflegends/infobox_team_custom.lua b/components/infobox/wikis/leagueoflegends/infobox_team_custom.lua index 92c546e4590..970ecc77621 100644 --- a/components/infobox/wikis/leagueoflegends/infobox_team_custom.lua +++ b/components/infobox/wikis/leagueoflegends/infobox_team_custom.lua @@ -19,7 +19,7 @@ local Injector = Lua.import('Module:Widget/Injector') local Region = Lua.import('Module:Region') local Team = Lua.import('Module:Infobox/Team') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local REGION_REMAPPINGS = { diff --git a/components/infobox/wikis/leagueoflegends/infobox_unit_champion_custom.lua b/components/infobox/wikis/leagueoflegends/infobox_unit_champion_custom.lua index 986965bba9b..63eb470a268 100644 --- a/components/infobox/wikis/leagueoflegends/infobox_unit_champion_custom.lua +++ b/components/infobox/wikis/leagueoflegends/infobox_unit_champion_custom.lua @@ -18,7 +18,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Unit = Lua.import('Module:Infobox/Unit') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Breakdown = Widgets.Breakdown local Cell = Widgets.Cell local Center = Widgets.Center @@ -58,7 +58,7 @@ function CustomInjector:parse(id, widgets) }, } elseif id == 'caption' then - table.insert(widgets, Center{content = {args.quote}}) + table.insert(widgets, Center{children = {args.quote}}) elseif id == 'type' then local toBreakDownCell = function(key, title, dataModule) if String.isEmpty(args[key]) then return end @@ -71,7 +71,7 @@ function CustomInjector:parse(id, widgets) toBreakDownCell('secondaryrole', 'Secondary Role', 'ClassIcon') ) return { - Breakdown{classes = {'infobox-center'}, content = breakDownContents}, + Breakdown{classes = {'infobox-center'}, children = breakDownContents}, Cell{name = 'Real Name', content = {args.realname}}, } elseif id == 'cost' then @@ -103,7 +103,7 @@ function CustomChampion:getCustomCells(widgets) ) if Array.any({'hp', 'hplvl', 'hpreg', 'hpreglvl'}, function(key) return String.isNotEmpty(args[key]) end) then - table.insert(widgets, Title{name = 'Base Statistics'}) + table.insert(widgets, Title{children = 'Base Statistics'}) end local function bonusPerLevel(start, bonuslvl) @@ -138,7 +138,7 @@ function CustomChampion:getCustomCells(widgets) local winPercentage = Math.round(wins * 100 / (wins + loses), 2) return Array.append(widgets, - Title{name = 'Esports Statistics'}, + Title{children = 'Esports Statistics'}, Cell{name = 'Win Rate', content = {wins .. 'W : ' .. loses .. 'L (' .. winPercentage .. '%)'}} ) end diff --git a/components/infobox/wikis/magic/infobox_league_custom.lua b/components/infobox/wikis/magic/infobox_league_custom.lua index 7ed06ba7ef7..eda62e27d83 100644 --- a/components/infobox/wikis/magic/infobox_league_custom.lua +++ b/components/infobox/wikis/magic/infobox_league_custom.lua @@ -15,7 +15,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class MagicLeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/mobilelegends/infobox_item_custom.lua b/components/infobox/wikis/mobilelegends/infobox_item_custom.lua index edd6e53b37b..e3001975b0e 100644 --- a/components/infobox/wikis/mobilelegends/infobox_item_custom.lua +++ b/components/infobox/wikis/mobilelegends/infobox_item_custom.lua @@ -19,7 +19,7 @@ local Table = require('Module:Table') local Injector = Lua.import('Module:Widget/Injector') local Item = Lua.import('Module:Infobox/Item') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -59,7 +59,7 @@ function CustomInjector:parse(id, widgets) if id == 'header' then if String.isNotEmpty(args.itemcost) then table.insert(widgets, Breakdown{ - content = caller:_getCostDisplay(), + children = caller:_getCostDisplay(), classes = { 'infobox-header', 'wiki-backgroundcolor-light', @@ -73,7 +73,7 @@ function CustomInjector:parse(id, widgets) if String.isNotEmpty(args.itemtext) then iconImage = iconImage .. '' .. args.itemtext .. '' end - table.insert(widgets, Center{content = {iconImage}}) + table.insert(widgets, Center{children = {iconImage}}) end return widgets elseif id == 'attributes' then @@ -102,7 +102,7 @@ function CustomInjector:parse(id, widgets) } widgets = caller:_getAttributeCells(attributeCells) if not Table.isEmpty(widgets) then - table.insert(widgets, 1, Title{name = 'Attributes'}) + table.insert(widgets, 1, Title{children = 'Attributes'}) end return widgets elseif id == 'ability' then @@ -117,7 +117,7 @@ function CustomInjector:parse(id, widgets) elseif id == 'availability' then if String.isEmpty(args.category) and String.isEmpty(args.drop) then return {} end return { - Title{name = 'Item Tier'}, + Title{children = 'Item Tier'}, Cell{name = 'Category', content = {caller:_categoryDisplay()}}, Cell{name = 'Dropped From', content = {args.drop}}, } @@ -131,7 +131,7 @@ function CustomInjector:parse(id, widgets) ) elseif id == 'recipe' then if String.isEmpty(args.recipe) then return {} end - table.insert(widgets, Center{content = {args.recipe}}) + table.insert(widgets, Center{children = {args.recipe}}) elseif id == 'info' then return {} end diff --git a/components/infobox/wikis/mobilelegends/infobox_league_custom.lua b/components/infobox/wikis/mobilelegends/infobox_league_custom.lua index d667a57403c..05789a46617 100644 --- a/components/infobox/wikis/mobilelegends/infobox_league_custom.lua +++ b/components/infobox/wikis/mobilelegends/infobox_league_custom.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -45,13 +45,13 @@ function CustomInjector:parse(id, widgets) return {Cell{name = 'Patch', content = {CustomLeague._getPatchVersion(args)}}} elseif id == 'customcontent' then if args.player_number then - table.insert(widgets, Title{name = 'Players'}) + table.insert(widgets, Title{children = 'Players'}) table.insert(widgets, Cell{name = 'Number of players', content = {args.player_number}}) end --teams section if args.team_number or (not String.isEmpty(args.team1)) then - table.insert(widgets, Title{name = 'Teams'}) + table.insert(widgets, Title{children = 'Teams'}) end table.insert(widgets, Cell{name = 'Number of teams', content = {args.team_number}}) end diff --git a/components/infobox/wikis/mobilelegends/infobox_person_player_custom.lua b/components/infobox/wikis/mobilelegends/infobox_person_player_custom.lua index f2f1206a79f..182ac4cd2d5 100644 --- a/components/infobox/wikis/mobilelegends/infobox_person_player_custom.lua +++ b/components/infobox/wikis/mobilelegends/infobox_person_player_custom.lua @@ -19,7 +19,7 @@ local TeamHistoryAuto = require('Module:TeamHistoryAuto') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -60,7 +60,7 @@ function CustomInjector:parse(id, widgets) -- we have an invalid hero entry -- add warning (including tracking category) table.insert( - caller.infobox.warnings, + caller.warnings, 'Invalid hero input "' .. hero .. '"[[Category:Pages with invalid hero input]]' ) end @@ -81,9 +81,9 @@ function CustomInjector:parse(id, widgets) if String.isNotEmpty(manualHistory) or automatedHistory then return { - Title{name = 'History'}, - Center{content = {manualHistory}}, - Center{content = {automatedHistory}}, + Title{children = 'History'}, + Center{children = {manualHistory}}, + Center{children = {automatedHistory}}, } end elseif id == 'region' then return {} diff --git a/components/infobox/wikis/mobilelegends/infobox_unit_hero_custom.lua b/components/infobox/wikis/mobilelegends/infobox_unit_hero_custom.lua index 4c469058f72..a066e010305 100644 --- a/components/infobox/wikis/mobilelegends/infobox_unit_hero_custom.lua +++ b/components/infobox/wikis/mobilelegends/infobox_unit_hero_custom.lua @@ -22,7 +22,7 @@ local Table = require('Module:Table') local Injector = Lua.import('Module:Widget/Injector') local Unit = Lua.import('Module:Infobox/Unit') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Breakdown = Widgets.Breakdown local Cell = Widgets.Cell local Center = Widgets.Center @@ -56,7 +56,7 @@ end function CustomInjector:parse(id, widgets) local args = self.caller.args if id == 'caption' then - table.insert(widgets, Center{content = {args.quote}}) + table.insert(widgets, Center{children = {args.quote}}) elseif id == 'type' then local toBreakDownCell = function(key, title) if String.isEmpty(args[key]) then return end @@ -69,7 +69,7 @@ function CustomInjector:parse(id, widgets) toBreakDownCell('secondaryrole', 'Secondary Role') ) return { - Breakdown{classes = {'infobox-center'}, content = breakDownContents}, + Breakdown{classes = {'infobox-center'}, children = breakDownContents}, } elseif id == 'cost' then local cost = Array.append({}, @@ -119,7 +119,7 @@ function CustomHero:addCustomCells(widgets) } if Array.any(baseStats, function(item) return Logic.isNotEmpty(item.value) end) then - table.insert(widgets, Title{name = 'Base Statistics'}) + table.insert(widgets, Title{children = 'Base Statistics'}) end Array.extendWith(widgets, Array.map(baseStats, function(item) @@ -131,7 +131,7 @@ function CustomHero:addCustomCells(widgets) local winPercentage = Math.round(wins * 100 / (wins + loses), 2) return Array.append(widgets, - Title{name = 'Esports Statistics'}, + Title{children = 'Esports Statistics'}, Cell{name = 'Win Rate', content = {wins .. 'W : ' .. loses .. 'L (' .. winPercentage .. '%)'}} ) end diff --git a/components/infobox/wikis/naraka/infobox_league_custom.lua b/components/infobox/wikis/naraka/infobox_league_custom.lua index b0b2cf0048f..0976eaabd7d 100644 --- a/components/infobox/wikis/naraka/infobox_league_custom.lua +++ b/components/infobox/wikis/naraka/infobox_league_custom.lua @@ -13,7 +13,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -53,10 +53,10 @@ function CustomInjector:parse(id, widgets) } elseif id == 'customcontent' then if args.player_number then - table.insert(widgets, Title{name = 'Players'}) + table.insert(widgets, Title{children = 'Players'}) table.insert(widgets, Cell{name = 'Number of players', content = {args.player_number}}) elseif args.team_number then - table.insert(widgets, Title{name = 'Teams'}) + table.insert(widgets, Title{children = 'Teams'}) table.insert(widgets, Cell{name = 'Number of teams', content = {args.team_number}}) end end diff --git a/components/infobox/wikis/naraka/infobox_person_player_custom.lua b/components/infobox/wikis/naraka/infobox_person_player_custom.lua index 56c5a20e8c5..7d15b89b802 100644 --- a/components/infobox/wikis/naraka/infobox_person_player_custom.lua +++ b/components/infobox/wikis/naraka/infobox_person_player_custom.lua @@ -22,7 +22,7 @@ local Template = require('Module:Template') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local ROLES = { diff --git a/components/infobox/wikis/omegastrikers/infobox_character_custom.lua b/components/infobox/wikis/omegastrikers/infobox_character_custom.lua index 4d807907c14..8c40e977eb4 100644 --- a/components/infobox/wikis/omegastrikers/infobox_character_custom.lua +++ b/components/infobox/wikis/omegastrikers/infobox_character_custom.lua @@ -13,7 +13,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Character = Lua.import('Module:Infobox/Character') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -47,7 +47,7 @@ function CustomInjector:parse(id, widgets) Cell{name = 'Affiliation', content = {'[[File:' .. args.affiliation .. ' allmode.png|20px]] ' .. args.affiliation} }, Cell{name = 'Voice Actor(s)', content = {args.voiceactors}}, - Title{name = 'Abilities'}, + Title{children = 'Abilities'}, Cell{name = 'Primary', content = {'[[File:' .. args.name .. ' - Primary.png|20px]] ' .. args.primary}}, Cell{name = 'Secondary', content = {'[[File:' .. args.name .. ' - Secondary.png|20px]] ' .. args.secondary}}, Cell{name = 'Special', content = {'[[File:' .. args.name .. ' - Special.png|20px]] ' .. args.special}} diff --git a/components/infobox/wikis/omegastrikers/infobox_league_custom.lua b/components/infobox/wikis/omegastrikers/infobox_league_custom.lua index f6c486b1853..007bfb6701b 100644 --- a/components/infobox/wikis/omegastrikers/infobox_league_custom.lua +++ b/components/infobox/wikis/omegastrikers/infobox_league_custom.lua @@ -13,7 +13,7 @@ local Logic = require('Module:Logic') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class OmegaStrikersLeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/omegastrikers/infobox_person_player_custom.lua b/components/infobox/wikis/omegastrikers/infobox_person_player_custom.lua index 287ac051ae8..547e62fdf61 100644 --- a/components/infobox/wikis/omegastrikers/infobox_person_player_custom.lua +++ b/components/infobox/wikis/omegastrikers/infobox_person_player_custom.lua @@ -15,7 +15,7 @@ local TeamHistoryAuto = require('Module:TeamHistoryAuto') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local ROLES = { diff --git a/components/infobox/wikis/osu/infobox_league_custom.lua b/components/infobox/wikis/osu/infobox_league_custom.lua index a75f9a175c9..eb22c3eda20 100644 --- a/components/infobox/wikis/osu/infobox_league_custom.lua +++ b/components/infobox/wikis/osu/infobox_league_custom.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class OsuLeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/osu/infobox_person_player_custom.lua b/components/infobox/wikis/osu/infobox_person_player_custom.lua index 5631fb1ce56..daaba7f5b34 100644 --- a/components/infobox/wikis/osu/infobox_person_player_custom.lua +++ b/components/infobox/wikis/osu/infobox_person_player_custom.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local ROLES = { diff --git a/components/infobox/wikis/overwatch/infobox_character_custom.lua b/components/infobox/wikis/overwatch/infobox_character_custom.lua index e27ebf97358..f82bfe711c5 100644 --- a/components/infobox/wikis/overwatch/infobox_character_custom.lua +++ b/components/infobox/wikis/overwatch/infobox_character_custom.lua @@ -15,7 +15,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Character = Lua.import('Module:Infobox/Character') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class OverwatchHeroInfobox: CharacterInfobox diff --git a/components/infobox/wikis/overwatch/infobox_league_custom.lua b/components/infobox/wikis/overwatch/infobox_league_custom.lua index 6988d99696c..427236f1d7d 100644 --- a/components/infobox/wikis/overwatch/infobox_league_custom.lua +++ b/components/infobox/wikis/overwatch/infobox_league_custom.lua @@ -18,7 +18,7 @@ local Logic = require('Module:Logic') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -66,8 +66,8 @@ function CustomInjector:parse(id, widgets) return PageLink.makeInternalLink(map) end) Array.appendWith(widgets, - Logic.isNotEmpty(maps) and table.insert(widgets, Title{name = 'Maps'}) or nil, - Center{content = table.concat(maps, ' • ')} + Logic.isNotEmpty(maps) and table.insert(widgets, Title{children = 'Maps'}) or nil, + Center{children = table.concat(maps, ' • ')} ) end elseif id == 'liquipediatier' then diff --git a/components/infobox/wikis/overwatch/infobox_map_custom.lua b/components/infobox/wikis/overwatch/infobox_map_custom.lua index 9348143ab57..7e84a40a40e 100644 --- a/components/infobox/wikis/overwatch/infobox_map_custom.lua +++ b/components/infobox/wikis/overwatch/infobox_map_custom.lua @@ -16,7 +16,7 @@ local Injector = Lua.import('Module:Widget/Injector') local Map = Lua.import('Module:Infobox/Map') local Flags = Lua.import('Module:Flags') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class OverwatchMapInfobox: MapInfobox diff --git a/components/infobox/wikis/overwatch/infobox_person_player_custom.lua b/components/infobox/wikis/overwatch/infobox_person_player_custom.lua index 1dfb083c194..8e9db80a9e9 100644 --- a/components/infobox/wikis/overwatch/infobox_person_player_custom.lua +++ b/components/infobox/wikis/overwatch/infobox_person_player_custom.lua @@ -22,7 +22,7 @@ local Template = require('Module:Template') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Center = Widgets.Center @@ -102,7 +102,7 @@ function CustomInjector:parse(id, widgets) } elseif id == 'history' then if args.nationalteams then - table.insert(widgets, 1, Center{content = {args.nationalteams}}) + table.insert(widgets, 1, Center{children = {args.nationalteams}}) end table.insert(widgets, Cell{name = 'Retired', content = {args.retired}}) end diff --git a/components/infobox/wikis/overwatch/infobox_person_user_custom.lua b/components/infobox/wikis/overwatch/infobox_person_user_custom.lua index b6ff05327a4..76618cb6c7d 100644 --- a/components/infobox/wikis/overwatch/infobox_person_user_custom.lua +++ b/components/infobox/wikis/overwatch/infobox_person_user_custom.lua @@ -16,7 +16,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local User = Lua.import('Module:Infobox/Person/User') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -52,9 +52,9 @@ function CustomInjector:parse(id, widgets) not (String.isEmpty(args.team_history) and String.isEmpty(args.clan_history)) then return { - Title{ name = 'History' }, - Center{content = {args.team_history}}, - Center{content = {args.clan_history}}, + Title{children = 'History' }, + Center{children = {args.team_history}}, + Center{children = {args.clan_history}}, } end return widgets @@ -76,12 +76,12 @@ function CustomUser:addCustomCells(widgets) ) if not String.isEmpty(args['fav-team-1']) then - table.insert(widgets, Title{name = 'Favorite teams'}) - table.insert(widgets, Center{content = {self:_getFavouriteTeams()}}) + table.insert(widgets, Title{children = 'Favorite teams'}) + table.insert(widgets, Center{children = {self:_getFavouriteTeams()}}) end if not String.isEmpty(args.s1high) then - table.insert(widgets, Title{name = '[[Leaderboards|Skill Ratings]]'}) + table.insert(widgets, Title{children = '[[Leaderboards|Skill Ratings]]'}) end local index = 1 diff --git a/components/infobox/wikis/overwatch/infobox_unofficial_world_champion_custom.lua b/components/infobox/wikis/overwatch/infobox_unofficial_world_champion_custom.lua index 3ff7f311c62..36a2431e16a 100644 --- a/components/infobox/wikis/overwatch/infobox_unofficial_world_champion_custom.lua +++ b/components/infobox/wikis/overwatch/infobox_unofficial_world_champion_custom.lua @@ -15,7 +15,7 @@ local Table = require('Module:Table') local Injector = Lua.import('Module:Widget/Injector') local UnofficialWorldChampion = Lua.import('Module:Infobox/UnofficialWorldChampion') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Breakdown = Widgets.Breakdown local Cell = Widgets.Cell local Title = Widgets.Title @@ -42,12 +42,12 @@ function CustomInjector:parse(id, widgets) if id == 'custom' then --Regional distribution - table.insert(widgets, String.isNotEmpty(args.region1) and Title{name = 'Regional distribution'} or nil) + table.insert(widgets, String.isNotEmpty(args.region1) and Title{children = 'Regional distribution'} or nil) for regionKey, region in Table.iter.pairsByPrefix(args, 'region') do Array.appendWith(widgets, Cell{name = (args[regionKey .. ' no'] or '') .. ' champions', content = {region}}, - Breakdown{content = {args[regionKey .. ' champions']}} + Breakdown{children = {args[regionKey .. ' champions']}} ) end end diff --git a/components/infobox/wikis/pokemon/infobox_league_custom.lua b/components/infobox/wikis/pokemon/infobox_league_custom.lua index e3cd27af367..b81497407a8 100644 --- a/components/infobox/wikis/pokemon/infobox_league_custom.lua +++ b/components/infobox/wikis/pokemon/infobox_league_custom.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -51,10 +51,10 @@ function CustomInjector:parse(id, widgets) } elseif id == 'customcontent' then if args.player_number then - table.insert(widgets, Title{name = 'Players'}) + table.insert(widgets, Title{children = 'Players'}) table.insert(widgets, Cell{name = 'Number of players', content = {args.player_number}}) elseif args.team_number then - table.insert(widgets, Title{name = 'Teams'}) + table.insert(widgets, Title{children = 'Teams'}) table.insert(widgets, Cell{name = 'Number of teams', content = {args.team_number}}) end end diff --git a/components/infobox/wikis/pokemon/infobox_person_player_custom.lua b/components/infobox/wikis/pokemon/infobox_person_player_custom.lua index 84599df71eb..e7a845915f6 100644 --- a/components/infobox/wikis/pokemon/infobox_person_player_custom.lua +++ b/components/infobox/wikis/pokemon/infobox_person_player_custom.lua @@ -17,7 +17,7 @@ local TeamHistoryAuto = require('Module:TeamHistoryAuto') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -60,9 +60,9 @@ function CustomInjector:parse(id, widgets) if String.isNotEmpty(manualHistory) or automatedHistory then return { - Title{name = 'History'}, - Center{content = {manualHistory}}, - Center{content = {automatedHistory}}, + Title{children = 'History'}, + Center{children = {manualHistory}}, + Center{children = {automatedHistory}}, } end elseif id == 'region' then return {} diff --git a/components/infobox/wikis/pubg/infobox_company_custom.lua b/components/infobox/wikis/pubg/infobox_company_custom.lua index ab68aa21c7f..1d1e0e11cf2 100644 --- a/components/infobox/wikis/pubg/infobox_company_custom.lua +++ b/components/infobox/wikis/pubg/infobox_company_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Company = Lua.import('Module:Infobox/Company') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class PubgCompanyInfobox: CompanyInfobox diff --git a/components/infobox/wikis/pubg/infobox_league_custom.lua b/components/infobox/wikis/pubg/infobox_league_custom.lua index e3dad3c021a..a0b84234c01 100644 --- a/components/infobox/wikis/pubg/infobox_league_custom.lua +++ b/components/infobox/wikis/pubg/infobox_league_custom.lua @@ -19,7 +19,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -89,13 +89,13 @@ function CustomInjector:parse(id, widgets) } elseif id == 'customcontent' then if args.player_number then - table.insert(widgets, Title{name = 'Players'}) + table.insert(widgets, Title{children = 'Players'}) table.insert(widgets, Cell{name = 'Number of players', content = {args.player_number}}) end --teams section if args.team_number then - table.insert(widgets, Title{name = 'Teams'}) + table.insert(widgets, Title{children = 'Teams'}) table.insert(widgets, Cell{name = 'Number of teams', content = {args.team_number}}) end end diff --git a/components/infobox/wikis/pubg/infobox_map_custom.lua b/components/infobox/wikis/pubg/infobox_map_custom.lua index bdbc39987bd..4065968c8b1 100644 --- a/components/infobox/wikis/pubg/infobox_map_custom.lua +++ b/components/infobox/wikis/pubg/infobox_map_custom.lua @@ -13,7 +13,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Map = Lua.import('Module:Infobox/Map') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class PUBGMapInfobox: MapInfobox diff --git a/components/infobox/wikis/pubg/infobox_person_player_custom.lua b/components/infobox/wikis/pubg/infobox_person_player_custom.lua index 0739c279387..7347ec8c26b 100644 --- a/components/infobox/wikis/pubg/infobox_person_player_custom.lua +++ b/components/infobox/wikis/pubg/infobox_person_player_custom.lua @@ -17,7 +17,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Center = Widgets.Center local Title = Widgets.Title @@ -85,8 +85,8 @@ function CustomInjector:parse(id, widgets) }}, } elseif id == 'history' and args.nationalteams then - table.insert(widgets, 1, Title{name = 'National Teams'}) - table.insert(widgets, 2, Center{content = {args.nationalteams}}) + table.insert(widgets, 1, Title{children = 'National Teams'}) + table.insert(widgets, 2, Center{children = {args.nationalteams}}) end return widgets end diff --git a/components/infobox/wikis/pubg/infobox_weapon_custom.lua b/components/infobox/wikis/pubg/infobox_weapon_custom.lua index 87d7dfa3416..09b5602cde6 100644 --- a/components/infobox/wikis/pubg/infobox_weapon_custom.lua +++ b/components/infobox/wikis/pubg/infobox_weapon_custom.lua @@ -15,7 +15,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Weapon = Lua.import('Module:Infobox/Weapon') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Center = Widgets.Center local Title = Widgets.Title @@ -54,8 +54,8 @@ function CustomInjector:parse(id, widgets) return tostring(CustomWeapon:_createNoWrappingSpan(PageLink.makeInternalLink({}, map))) end) - table.insert(widgets, Title{name = 'Maps'}) - table.insert(widgets, Center{content = {table.concat(maps, ' • ')}}) + table.insert(widgets, Title{children = 'Maps'}) + table.insert(widgets, Center{children = {table.concat(maps, ' • ')}}) end return widgets diff --git a/components/infobox/wikis/pubgmobile/infobox_league_custom.lua b/components/infobox/wikis/pubgmobile/infobox_league_custom.lua index 13af47a0d82..131ac360206 100644 --- a/components/infobox/wikis/pubgmobile/infobox_league_custom.lua +++ b/components/infobox/wikis/pubgmobile/infobox_league_custom.lua @@ -18,7 +18,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -74,13 +74,13 @@ function CustomInjector:parse(id, widgets) } elseif id == 'customcontent' then if args.player_number then - table.insert(widgets, Title{name = 'Players'}) + table.insert(widgets, Title{children = 'Players'}) table.insert(widgets, Cell{name = 'Number of players', content = {args.player_number}}) end --teams section if args.team_number then - table.insert(widgets, Title{name = 'Teams'}) + table.insert(widgets, Title{children = 'Teams'}) table.insert(widgets, Cell{name = 'Number of teams', content = {args.team_number}}) end end diff --git a/components/infobox/wikis/pubgmobile/infobox_map_custom.lua b/components/infobox/wikis/pubgmobile/infobox_map_custom.lua index cca1427af5a..e774281c8c7 100644 --- a/components/infobox/wikis/pubgmobile/infobox_map_custom.lua +++ b/components/infobox/wikis/pubgmobile/infobox_map_custom.lua @@ -14,7 +14,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Map = Lua.import('Module:Infobox/Map') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class PubgMobileMapInfobox: MapInfobox diff --git a/components/infobox/wikis/pubgmobile/infobox_person_player_custom.lua b/components/infobox/wikis/pubgmobile/infobox_person_player_custom.lua index f5de7451bfb..cfd56cf02c8 100644 --- a/components/infobox/wikis/pubgmobile/infobox_person_player_custom.lua +++ b/components/infobox/wikis/pubgmobile/infobox_person_player_custom.lua @@ -18,7 +18,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -73,9 +73,9 @@ function CustomInjector:parse(id, widgets) if String.isNotEmpty(manualHistory) or automatedHistory then return { - Title{name = 'History'}, - Center{content = {manualHistory}}, - Center{content = {automatedHistory}}, + Title{children = 'History'}, + Center{children = {manualHistory}}, + Center{children = {automatedHistory}}, } end elseif id == 'status' then diff --git a/components/infobox/wikis/rainbowsix/infobox_league_custom.lua b/components/infobox/wikis/rainbowsix/infobox_league_custom.lua index 00c7577e718..c38618ee55d 100644 --- a/components/infobox/wikis/rainbowsix/infobox_league_custom.lua +++ b/components/infobox/wikis/rainbowsix/infobox_league_custom.lua @@ -17,7 +17,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -81,8 +81,8 @@ function CustomInjector:parse(id, widgets) PageLink.makeInternalLink({}, map, map .. game) ))) end - table.insert(widgets, Title{name = 'Maps'}) - table.insert(widgets, Center{content = {table.concat(maps, ' • ')}}) + table.insert(widgets, Title{children = 'Maps'}) + table.insert(widgets, Center{children = {table.concat(maps, ' • ')}}) end elseif id == 'liquipediatier' then if self.caller:_validPublisherTier(args.ubisofttier) then diff --git a/components/infobox/wikis/rainbowsix/infobox_patch_custom.lua b/components/infobox/wikis/rainbowsix/infobox_patch_custom.lua index 7b8d128b52f..ad4c9277df9 100644 --- a/components/infobox/wikis/rainbowsix/infobox_patch_custom.lua +++ b/components/infobox/wikis/rainbowsix/infobox_patch_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Patch = Lua.import('Module:Infobox/Patch') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class R6PatchInfobox: PatchInfobox diff --git a/components/infobox/wikis/rainbowsix/infobox_person_player_custom.lua b/components/infobox/wikis/rainbowsix/infobox_person_player_custom.lua index ffd25cc5d46..0498855bea6 100644 --- a/components/infobox/wikis/rainbowsix/infobox_person_player_custom.lua +++ b/components/infobox/wikis/rainbowsix/infobox_person_player_custom.lua @@ -31,7 +31,7 @@ local ACHIEVEMENTS_BASE_CONDITIONS = { '[[placement::1]]', } -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local BANNED = mw.loadData('Module:Banned') diff --git a/components/infobox/wikis/rainbowsix/infobox_weapon_custom.lua b/components/infobox/wikis/rainbowsix/infobox_weapon_custom.lua index e6ec406727b..3370962194c 100644 --- a/components/infobox/wikis/rainbowsix/infobox_weapon_custom.lua +++ b/components/infobox/wikis/rainbowsix/infobox_weapon_custom.lua @@ -16,7 +16,7 @@ local Table = require('Module:Table') local Injector = Lua.import('Module:Widget/Injector') local Weapon = Lua.import('Module:Infobox/Weapon') -local Widgets = Lua.import('Module:Infobox/Widget/All') +local Widgets = Lua.import('Module:Widget/All') local Cell = Widgets.Cell ---@class RainbowsixWeaponInfobox: WeaponInfobox diff --git a/components/infobox/wikis/rocketleague/infobox_company_custom.lua b/components/infobox/wikis/rocketleague/infobox_company_custom.lua index 35e931c60b6..757c83cd4dc 100644 --- a/components/infobox/wikis/rocketleague/infobox_company_custom.lua +++ b/components/infobox/wikis/rocketleague/infobox_company_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Company = Lua.import('Module:Infobox/Company') local Injector = Lua.import('Module:Widget/Injector') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class RocketleagueCompanyInfobox: CompanyInfobox diff --git a/components/infobox/wikis/rocketleague/infobox_league_custom.lua b/components/infobox/wikis/rocketleague/infobox_league_custom.lua index 991891a32e5..42e0470e036 100644 --- a/components/infobox/wikis/rocketleague/infobox_league_custom.lua +++ b/components/infobox/wikis/rocketleague/infobox_league_custom.lua @@ -11,16 +11,16 @@ local Class = require('Module:Class') local Game = require('Module:Game') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Math = require('Module:MathUtil') local String = require('Module:StringUtils') local Tier = require('Module:Tier/Custom') -local TournamentNotability = require('Module:TournamentNotability') local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') local ReferenceCleaner = Lua.import('Module:ReferenceCleaner') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -28,6 +28,7 @@ local Center = Widgets.Center ---@class RocketleagueLeagueInfobox: InfoboxLeague local CustomLeague = Class.new(League) local CustomInjector = Class.new(Injector) +local NotabilityCalculator = {} local SERIES_RLCS = 'Rocket League Championship Series' local MODE_2v2 = '2v2' @@ -70,12 +71,12 @@ function CustomInjector:parse(id, widgets) ) index = index + 1 end - table.insert(widgets, Title{name = 'Maps'}) - table.insert(widgets, Center{content = maps}) + table.insert(widgets, Title{children = 'Maps'}) + table.insert(widgets, Center{children = maps}) end if not String.isEmpty(args.team_number) then - table.insert(widgets, Title{name = 'Teams'}) + table.insert(widgets, Title{children = 'Teams'}) table.insert(widgets, Cell{ name = 'Number of teams', content = {args.team_number} @@ -166,7 +167,7 @@ function CustomLeague:addToLpdb(lpdbData, args) lpdbData.extradata.mode = args.mode lpdbData.extradata.notabilitymod = args.notabilitymod lpdbData.extradata.liquipediatiertype2 = args.liquipediatiertype2 - lpdbData.extradata.notabilitypercentage = args.edate ~= 'tba' and TournamentNotability.run() or '' + lpdbData.extradata.notabilitypercentage = args.edate ~= 'tba' and NotabilityCalculator.run() or '' return lpdbData end @@ -206,4 +207,86 @@ function CustomLeague:_makeInternalLink(content) return '[[' .. content .. ']]' end +---@return number +function NotabilityCalculator.run() + local pagename = mw.title.getCurrentTitle().text:gsub(' ', '_') + local placements = NotabilityCalculator._getPlacements(pagename) + local allTeams = NotabilityCalculator._getAllTeams() + + local teamsWithAPage = 0 + + -- We need this because sometimes we get a placement like "tbd" + local numberOfPlacements = 0 + + for _, placement in ipairs(placements) do + if placement.participant:lower() ~= '' and placement.participant:lower() ~= 'tbd' then + local doesTeamExist = NotabilityCalculator._findTeam(allTeams, placement.participant) + numberOfPlacements = numberOfPlacements + 1 + + if doesTeamExist == true then + teamsWithAPage = teamsWithAPage + 1 + end + end + end + + if numberOfPlacements == 0 then + return 0 + end + + return Math.round((teamsWithAPage / numberOfPlacements) * 100, 2) +end + +---@param allTeams table +---@param teamToFind string +---@return boolean +function NotabilityCalculator._findTeam(allTeams, teamToFind) + local firstLetter = string.sub(teamToFind, 1, 1):lower() + + if not allTeams[firstLetter] then + return false + end + + for _, team in ipairs(allTeams[firstLetter]) do + if team:lower() == teamToFind:lower() then + return true + end + end + + return false +end + +---@return table +function NotabilityCalculator._getAllTeams() + local teams = mw.ext.LiquipediaDB.lpdb('team', { + query = 'name', + limit = 5000, + }) + + -- Make a table of letters, with each letter mapping to an + -- array of names, to aid in faster lookup + local indexedTeams = {} + + for _, team in pairs(teams) do + local firstLetter = string.sub(team.name, 1, 1):lower() + + if indexedTeams[firstLetter] == nil then + indexedTeams[firstLetter] = {} + end + + table.insert(indexedTeams[firstLetter], team.name) + end + + return indexedTeams +end + +---@param pagename string +---@return {participant: string, participantflag: string, mode: string}[] +function NotabilityCalculator._getPlacements(pagename) + return mw.ext.LiquipediaDB.lpdb('placement', { + conditions = '[[pagename::' .. pagename .. ']] AND [[mode::3v3]]', + query = 'participant, participantflag, mode' + }) +end + + return CustomLeague diff --git a/components/infobox/wikis/rocketleague/infobox_map_custom.lua b/components/infobox/wikis/rocketleague/infobox_map_custom.lua index 3f3a1942aa3..00837bcc94d 100644 --- a/components/infobox/wikis/rocketleague/infobox_map_custom.lua +++ b/components/infobox/wikis/rocketleague/infobox_map_custom.lua @@ -13,7 +13,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Map = Lua.import('Module:Infobox/Map') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class RocketLeagueMapInfobox: MapInfobox diff --git a/components/infobox/wikis/rocketleague/infobox_person_player_custom.lua b/components/infobox/wikis/rocketleague/infobox_person_player_custom.lua index d284512bd27..dd0d941a918 100644 --- a/components/infobox/wikis/rocketleague/infobox_person_player_custom.lua +++ b/components/infobox/wikis/rocketleague/infobox_person_player_custom.lua @@ -22,7 +22,7 @@ local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') local YearsActive = Lua.import('Module:YearsActive') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -117,8 +117,8 @@ function CustomInjector:parse(id, widgets) elseif id == 'history' then local getHistoryCells = function(key, title) return { - String.isNotEmpty(args[key]) and Title{name = title} or nil, - Center{content = {args[key]}}, + String.isNotEmpty(args[key]) and Title{children = title} or nil, + Center{children = {args[key]}}, } end diff --git a/components/infobox/wikis/rocketleague/infobox_team_custom.lua b/components/infobox/wikis/rocketleague/infobox_team_custom.lua index cfae686a3e6..c4fcd1370b2 100644 --- a/components/infobox/wikis/rocketleague/infobox_team_custom.lua +++ b/components/infobox/wikis/rocketleague/infobox_team_custom.lua @@ -14,7 +14,7 @@ local TeamRanking = require('Module:TeamRanking') local Injector = Lua.import('Module:Widget/Injector') local Team = Lua.import('Module:Infobox/Team') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class RocketleagueInfoboxTeam: InfoboxTeam diff --git a/components/infobox/wikis/rocketleague/infobox_unit_car_custom.lua b/components/infobox/wikis/rocketleague/infobox_unit_car_custom.lua index f0239f629bf..3fd7ffe2cf7 100644 --- a/components/infobox/wikis/rocketleague/infobox_unit_car_custom.lua +++ b/components/infobox/wikis/rocketleague/infobox_unit_car_custom.lua @@ -13,7 +13,7 @@ local Namespace = require('Module:Namespace') local Injector = Lua.import('Module:Widget/Injector') local Unit = Lua.import('Module:Infobox/Unit') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class RocketLeagueUnitInfobox: UnitInfobox diff --git a/components/infobox/wikis/sideswipe/infobox_league_custom.lua b/components/infobox/wikis/sideswipe/infobox_league_custom.lua index ff76f139601..6e64193a3f4 100644 --- a/components/infobox/wikis/sideswipe/infobox_league_custom.lua +++ b/components/infobox/wikis/sideswipe/infobox_league_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class SideswipeLeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/simracing/infobox_league_custom.lua b/components/infobox/wikis/simracing/infobox_league_custom.lua index e21b6d79f42..17b2f142a0e 100644 --- a/components/infobox/wikis/simracing/infobox_league_custom.lua +++ b/components/infobox/wikis/simracing/infobox_league_custom.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class SimracingLeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/simracing/infobox_person_player_custom.lua b/components/infobox/wikis/simracing/infobox_person_player_custom.lua index bd2f1a0d26f..0948f2fd9b1 100644 --- a/components/infobox/wikis/simracing/infobox_person_player_custom.lua +++ b/components/infobox/wikis/simracing/infobox_person_player_custom.lua @@ -13,7 +13,7 @@ local GameAppearances = Lua.import('Module:Infobox/Extension/GameAppearances') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class SimracingInfoboxPlayer: Person diff --git a/components/infobox/wikis/smash/infobox_league_custom.lua b/components/infobox/wikis/smash/infobox_league_custom.lua index 2c6983ccefa..5f969b4b79d 100644 --- a/components/infobox/wikis/smash/infobox_league_custom.lua +++ b/components/infobox/wikis/smash/infobox_league_custom.lua @@ -18,7 +18,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Chronology = Widgets.Chronology @@ -161,7 +161,7 @@ function CustomInjector:parse(id, widgets) ) elseif id == 'customcontent' then if args.circuit or args.points or args.circuit_next or args.circuit_previous then - table.insert(widgets, Title{name = 'Circuit Information'}) + table.insert(widgets, Title{children = 'Circuit Information'}) self.caller:_createCircuitInformation(widgets) end if args.circuit2 or args.points2 or args.circuit2_next or args.circuit2_previous then @@ -170,14 +170,14 @@ function CustomInjector:parse(id, widgets) local singles = Array.map(league:getAllArgsForBase(args, 's_stage'), CustomLeague._createNoWrappingSpan) if #singles > 0 then - table.insert(widgets, Title{name = 'Singles Stages'}) - table.insert(widgets, Center{content = {table.concat(singles, ' • ')}}) + table.insert(widgets, Title{children = 'Singles Stages'}) + table.insert(widgets, Center{children = {table.concat(singles, ' • ')}}) end local doubles = Array.map(league:getAllArgsForBase(args, 'd_stage'), CustomLeague._createNoWrappingSpan) if #doubles > 0 then - table.insert(widgets, Title{name = 'Doubles Stages'}) - table.insert(widgets, Center{content = {table.concat(doubles, ' • ')}}) + table.insert(widgets, Title{children = 'Doubles Stages'}) + table.insert(widgets, Center{children = {table.concat(doubles, ' • ')}}) end elseif id == 'dates' then return { @@ -400,7 +400,7 @@ function CustomLeague:_createCircuitInformation(widgets, circuitIndex) Cell{name = 'Circuit Tier', content = {circuitArgs.tier}}, Cell{name = 'Tournament Region', content = {circuitArgs.region}}, Cell{name = 'Points', content = {circuitArgs.points}}, - Chronology{content = {next = circuitArgs.next, previous = circuitArgs.previous}} + Chronology{links = {next = circuitArgs.next, previous = circuitArgs.previous}} ) end diff --git a/components/infobox/wikis/smash/infobox_person_player_custom.lua b/components/infobox/wikis/smash/infobox_person_player_custom.lua index 89bbfdf5844..a3e0fb26273 100644 --- a/components/infobox/wikis/smash/infobox_person_player_custom.lua +++ b/components/infobox/wikis/smash/infobox_person_player_custom.lua @@ -20,7 +20,7 @@ local YearsActive = require('Module:YearsActive') -- TODO Convert to use the com local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -71,7 +71,7 @@ function CustomInjector:parse(id, widgets) local alt = CustomPlayer.inputToCharacterIconList(args['alt-' .. game], game, 'InfoboxCharacter') Array.appendWith(widgets, - (main or former or alt) and Title{name = gameData.name} or nil, + (main or former or alt) and Title{children = gameData.name} or nil, Cell{name = 'Current Mains', content = main or {}}, Cell{name = 'Former Mains', content = former or {}}, Cell{name = 'Secondaries', content = alt or {}} @@ -94,7 +94,7 @@ function CustomInjector:parse(id, widgets) table.insert(achievements, {gameName = gameData.abbreviation, icons = icons}) end) if #achievements == 0 then return {} end - return Array.extend({Title{name = 'Achievements'}}, Array.map(achievements, function(achievement) + return Array.extend({Title{children = 'Achievements'}}, Array.map(achievements, function(achievement) return Cell{name = achievement.gameName, content = {achievement.icons}, options = {columns = 3}} end)) end diff --git a/components/infobox/wikis/smash/infobox_series_custom.lua b/components/infobox/wikis/smash/infobox_series_custom.lua index 70a5846712b..91ea3008711 100644 --- a/components/infobox/wikis/smash/infobox_series_custom.lua +++ b/components/infobox/wikis/smash/infobox_series_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Series = Lua.import('Module:Infobox/Series') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local CustomInjector = Class.new(Injector) diff --git a/components/infobox/wikis/smite/infobox_league_custom.lua b/components/infobox/wikis/smite/infobox_league_custom.lua index 5226631c987..5e8df617a0a 100644 --- a/components/infobox/wikis/smite/infobox_league_custom.lua +++ b/components/infobox/wikis/smite/infobox_league_custom.lua @@ -14,7 +14,7 @@ local Logic = require('Module:Logic') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class SmiteLeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/smite/infobox_person_player_custom.lua b/components/infobox/wikis/smite/infobox_person_player_custom.lua index 3bbc487b9f8..e265f9cc928 100644 --- a/components/infobox/wikis/smite/infobox_person_player_custom.lua +++ b/components/infobox/wikis/smite/infobox_person_player_custom.lua @@ -15,7 +15,7 @@ local TeamHistoryAuto = require('Module:TeamHistoryAuto') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local ROLES = { diff --git a/components/infobox/wikis/smite/infobox_unit_god_custom.lua b/components/infobox/wikis/smite/infobox_unit_god_custom.lua index ac8c03d8512..9cbcd56fc8a 100644 --- a/components/infobox/wikis/smite/infobox_unit_god_custom.lua +++ b/components/infobox/wikis/smite/infobox_unit_god_custom.lua @@ -18,7 +18,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Unit = Lua.import('Module:Infobox/Unit') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Breakdown = Widgets.Breakdown local Cell = Widgets.Cell local Center = Widgets.Center @@ -59,7 +59,7 @@ function CustomInjector:parse(id, widgets) }, } elseif id == 'caption' then - table.insert(widgets, Center {content = {args.quote}}) + table.insert(widgets, Center{children = {args.quote}}) elseif id == 'type' then local toBreakDownCell = function(key, title, dataModule) if String.isEmpty(args[key]) then return end @@ -72,7 +72,7 @@ function CustomInjector:parse(id, widgets) toBreakDownCell('powertype', 'Power Type', 'PowerTypeIcon') ) return { - Breakdown{classes = {'infobox-center'}, content = breakDownContents}, + Breakdown{classes = {'infobox-center'}, children = breakDownContents}, Cell{name = 'Real Name', content = {args.realname}}, } elseif id == 'cost' then @@ -120,7 +120,7 @@ function CustomGod:getCustomCells(widgets) Cell{name = 'Attack Speed', content = {bonusPerLevel(args.attackspeed, args.attackspeedlvl)}}, Cell{name = 'Attack Damage', content = {bonusPerLevel(args.damage, args.damagelvl), args.damagebonus}}, Cell{name = 'Progression', content = {args.progression}}, - Title{name = 'Protections'}, + Title{children = 'Protections'}, Cell{name = 'Physical', content = {bonusPerLevel(args.physical, args.physicallvl)}}, Cell{name = 'Magical', content = {bonusPerLevel(args.magical, args.magicallvl)}} ) @@ -131,7 +131,7 @@ function CustomGod:getCustomCells(widgets) local winPercentage = Math.round(wins * 100 / (wins + loses), 2) return Array.append(widgets, - Title{name = 'Esports Statistics'}, + Title{children = 'Esports Statistics'}, Cell{name = 'Win Rate', content = {wins .. 'W : ' .. loses .. 'L (' .. winPercentage .. '%)'}} ) end diff --git a/components/infobox/wikis/splatoon/infobox_league_custom.lua b/components/infobox/wikis/splatoon/infobox_league_custom.lua index 80e2d8d0207..39573ce340a 100644 --- a/components/infobox/wikis/splatoon/infobox_league_custom.lua +++ b/components/infobox/wikis/splatoon/infobox_league_custom.lua @@ -16,7 +16,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class SplatoonLeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/splatoon/infobox_person_player_custom.lua b/components/infobox/wikis/splatoon/infobox_person_player_custom.lua index 090a78ceb8c..df6f6f9a743 100644 --- a/components/infobox/wikis/splatoon/infobox_person_player_custom.lua +++ b/components/infobox/wikis/splatoon/infobox_person_player_custom.lua @@ -19,7 +19,7 @@ local TeamHistoryAuto = require('Module:TeamHistoryAuto') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local SIZE_WEAPON = '25x25px' @@ -59,7 +59,7 @@ function CustomInjector:parse(id, widgets) -- we have an invalid weapon entry -- add warning (including tracking category) table.insert( - caller.infobox.warnings, + caller.warnings, 'Invalid weapon input "' .. weapon .. '"[[Category:Pages with invalid weapon input]]' ) end diff --git a/components/infobox/wikis/squadrons/infobox_league_custom.lua b/components/infobox/wikis/squadrons/infobox_league_custom.lua index 1fdb6cd3efb..12553cc0fe2 100644 --- a/components/infobox/wikis/squadrons/infobox_league_custom.lua +++ b/components/infobox/wikis/squadrons/infobox_league_custom.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -55,8 +55,8 @@ function CustomInjector:parse(id, widgets) end Array.appendWith(widgets, - Title{name = 'Maps'}, - Center{content = {table.concat(maps, ' • ')}} + Title{children = 'Maps'}, + Center{children = {table.concat(maps, ' • ')}} ) end return widgets diff --git a/components/infobox/wikis/starcraft/infobox_building_custom.lua b/components/infobox/wikis/starcraft/infobox_building_custom.lua index 7da68ac5270..76fc5747315 100644 --- a/components/infobox/wikis/starcraft/infobox_building_custom.lua +++ b/components/infobox/wikis/starcraft/infobox_building_custom.lua @@ -16,7 +16,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Building = Lua.import('Module:Infobox/Building') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class StarcraftBuildingInfobox: BuildingInfobox diff --git a/components/infobox/wikis/starcraft/infobox_league_custom.lua b/components/infobox/wikis/starcraft/infobox_league_custom.lua index d3a16fa5997..047740332f5 100644 --- a/components/infobox/wikis/starcraft/infobox_league_custom.lua +++ b/components/infobox/wikis/starcraft/infobox_league_custom.lua @@ -19,7 +19,7 @@ local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') local RaceBreakdown = Lua.import('Module:Infobox/Extension/RaceBreakdown') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Breakdown = Widgets.Breakdown local Cell = Widgets.Cell local Center = Widgets.Center @@ -152,10 +152,10 @@ function CustomInjector:parse(id, widgets) elseif id == 'customcontent' then if args.player_number and args.player_number > 0 or args.team_number then Array.appendWith(widgets, - Title{name = 'Participants'}, + Title{children = 'Participants'}, Cell{name = 'Number of Players', content = {self.caller.data.raceBreakDown.total}}, Cell{name = 'Number of Teams', content = {args.team_number}}, - Breakdown{content = self.caller.data.raceBreakDown.display or {}, classes = { 'infobox-center' }} + Breakdown{children = self.caller.data.raceBreakDown.display or {}, classes = { 'infobox-center' }} ) end @@ -166,8 +166,8 @@ function CustomInjector:parse(id, widgets) local displayMaps = function(prefix, defaultTitle, maps) if String.isEmpty(args[prefix .. 1]) then return end Array.appendWith(widgets, - Title{name = args[prefix .. 'title'] or defaultTitle}, - Center{content = self.caller:_mapsDisplay(maps or self.caller:_getMaps(prefix, args))} + Title{children = args[prefix .. 'title'] or defaultTitle}, + Center{children = self.caller:_mapsDisplay(maps or self.caller:_getMaps(prefix, args))} ) end diff --git a/components/infobox/wikis/starcraft/infobox_map_custom.lua b/components/infobox/wikis/starcraft/infobox_map_custom.lua index 82097bdaeae..a1aac95d8a6 100644 --- a/components/infobox/wikis/starcraft/infobox_map_custom.lua +++ b/components/infobox/wikis/starcraft/infobox_map_custom.lua @@ -15,7 +15,7 @@ local Template = require('Module:Template') local Injector = Lua.import('Module:Widget/Injector') local Map = Lua.import('Module:Infobox/Map') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class StarcraftMapInfobox: MapInfobox diff --git a/components/infobox/wikis/starcraft/infobox_person_player_custom.lua b/components/infobox/wikis/starcraft/infobox_person_player_custom.lua index ce9867e786d..506f13f5d2b 100644 --- a/components/infobox/wikis/starcraft/infobox_person_player_custom.lua +++ b/components/infobox/wikis/starcraft/infobox_person_player_custom.lua @@ -27,7 +27,7 @@ local Injector = Lua.import('Module:Widget/Injector') local Opponent = Lua.import('Module:Opponent') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Condition = require('Module:Condition') diff --git a/components/infobox/wikis/starcraft/infobox_series_custom.lua b/components/infobox/wikis/starcraft/infobox_series_custom.lua index 1a280690a41..c3faa2df6e9 100644 --- a/components/infobox/wikis/starcraft/infobox_series_custom.lua +++ b/components/infobox/wikis/starcraft/infobox_series_custom.lua @@ -21,7 +21,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local Series = Lua.import('Module:Infobox/Series') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local CustomInjector = Class.new(Injector) diff --git a/components/infobox/wikis/starcraft/infobox_skill_custom.lua b/components/infobox/wikis/starcraft/infobox_skill_custom.lua index 348511df9d8..b76c9a000a6 100644 --- a/components/infobox/wikis/starcraft/infobox_skill_custom.lua +++ b/components/infobox/wikis/starcraft/infobox_skill_custom.lua @@ -16,7 +16,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Skill = Lua.import('Module:Infobox/Skill') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class StarcraftSkillInfobox: SkillInfobox diff --git a/components/infobox/wikis/starcraft/infobox_strategy_custom.lua b/components/infobox/wikis/starcraft/infobox_strategy_custom.lua index f273fff06cd..d9e604fbb08 100644 --- a/components/infobox/wikis/starcraft/infobox_strategy_custom.lua +++ b/components/infobox/wikis/starcraft/infobox_strategy_custom.lua @@ -15,7 +15,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Strategy = Lua.import('Module:Infobox/Strategy') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header @@ -35,7 +35,7 @@ function CustomStrategy.run(frame) if Namespace.isMain() then local categories = strategy:_getCategories(strategy.args.race, strategy.args.matchups) - strategy.infobox:categories(unpack(categories)) + strategy:categories(unpack(categories)) end return strategy:createInfobox() diff --git a/components/infobox/wikis/starcraft/infobox_team_custom.lua b/components/infobox/wikis/starcraft/infobox_team_custom.lua index e03775e447c..d22696c7680 100644 --- a/components/infobox/wikis/starcraft/infobox_team_custom.lua +++ b/components/infobox/wikis/starcraft/infobox_team_custom.lua @@ -28,7 +28,7 @@ local Team = Lua.import('Module:Infobox/Team') local OpponentLibraries = require('Module:OpponentLibraries') local Opponent = OpponentLibraries.Opponent -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Breakdown = Widgets.Breakdown local Cell = Widgets.Cell local Title = Widgets.Title @@ -87,9 +87,9 @@ function CustomInjector:parse(id, widgets) local raceBreakdown = RaceBreakdown.run(args) if raceBreakdown then Array.appendWith(widgets, - Title{name = 'Player Breakdown'}, + Title{children = 'Player Breakdown'}, Cell{name = 'Number of Players', content = {raceBreakdown.total}}, - Breakdown{content = raceBreakdown.display, classes = { 'infobox-center' }} + Breakdown{children = raceBreakdown.display, classes = { 'infobox-center' }} ) end elseif id == 'history' then diff --git a/components/infobox/wikis/starcraft/infobox_unit_custom.lua b/components/infobox/wikis/starcraft/infobox_unit_custom.lua index bb06155d3af..4a2a48728e7 100644 --- a/components/infobox/wikis/starcraft/infobox_unit_custom.lua +++ b/components/infobox/wikis/starcraft/infobox_unit_custom.lua @@ -17,7 +17,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Unit = Lua.import('Module:Infobox/Unit') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Center = Widgets.Center local Title = Widgets.Title @@ -83,11 +83,11 @@ function CustomInjector:parse(id, widgets) if not aoeArgs or String.isEmpty(aoeArgs.name) then return {} end return { - Title{name = aoeArgs.name}, + Title{children = aoeArgs.name}, Cell{name = 'Inner', content = {aoeArgs.size1}}, Cell{name = 'Medium', content = {aoeArgs.size2}}, Cell{name = 'Outer', content = {aoeArgs.size3}}, - Center{content = {aoeArgs.footnotes and ('' .. aoeArgs.footnotes .. '') or nil}} + Center{children = {aoeArgs.footnotes and ('' .. aoeArgs.footnotes .. '') or nil}} } elseif id == 'custom' then return self.caller:getCustomCells(widgets) @@ -105,7 +105,7 @@ function CustomUnit:getCustomCells(widgets) end return { - Title{name = 'Unit stats'}, + Title{children = 'Unit stats'}, Cell{name = 'Attributes', content = {args.att}}, Cell{name = 'Defense', content = {CustomUnit:_defenseDisplay(args)}}, Cell{name = 'Damage', content = {args.damage}}, diff --git a/components/infobox/wikis/starcraft2/infobox_building_custom.lua b/components/infobox/wikis/starcraft2/infobox_building_custom.lua index 074ea6f334e..ae711f430a7 100644 --- a/components/infobox/wikis/starcraft2/infobox_building_custom.lua +++ b/components/infobox/wikis/starcraft2/infobox_building_custom.lua @@ -18,7 +18,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Building = Lua.import('Module:Infobox/Building') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Center = Widgets.Center @@ -59,7 +59,7 @@ function CustomInjector:parse(id, widgets) ) if args.game ~= GAME_LOTV then - table.insert(widgets, Center{content = { + table.insert(widgets, Center{children = { 'Note: ' .. 'All time-related values are expressed assuming Normal speed, as they were before LotV.' .. ' See [[Game Speed]].' diff --git a/components/infobox/wikis/starcraft2/infobox_campaign_mission_custom.lua b/components/infobox/wikis/starcraft2/infobox_campaign_mission_custom.lua index 677a5db7b74..d760e61885f 100644 --- a/components/infobox/wikis/starcraft2/infobox_campaign_mission_custom.lua +++ b/components/infobox/wikis/starcraft2/infobox_campaign_mission_custom.lua @@ -14,7 +14,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Mission = Lua.import('Module:Infobox/CampaignMission') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header diff --git a/components/infobox/wikis/starcraft2/infobox_league_custom.lua b/components/infobox/wikis/starcraft2/infobox_league_custom.lua index b63ee216b5b..6c0ae89c95a 100644 --- a/components/infobox/wikis/starcraft2/infobox_league_custom.lua +++ b/components/infobox/wikis/starcraft2/infobox_league_custom.lua @@ -10,6 +10,8 @@ local AllowedServers = require('Module:Server') local Array = require('Module:Array') local Autopatch = require('Module:Automated Patch') local Class = require('Module:Class') +local Countdown = require('Module:Countdown') +local DateExt = require('Module:Date/Ext') local Game = require('Module:Game') local Json = require('Module:Json') local Logic = require('Module:Logic') @@ -25,7 +27,7 @@ local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') local RaceBreakdown = Lua.import('Module:Infobox/Extension/RaceBreakdown') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Breakdown = Widgets.Breakdown local Cell = Widgets.Cell local Center = Widgets.Center @@ -67,10 +69,33 @@ function CustomLeague:customParseArguments(args) self.data.publishertier = tostring(Logic.readBool(args.featured)) self.data.status = self:_getStatus(args) + self.data.startTime = Logic.wrapTryOrLog(CustomLeague._readStartTime)(self) + self:_computeChronology(args) self:_computePatch(args) end +---@return {display: string?, storage: string?} +function CustomLeague:_readStartTime() + ---@type string? + local timePart = self.args.start_time + local startDate = self.data.startDate + if Logic.isEmpty(timePart) or Logic.isEmpty(startDate) then return {} end + ---@cast timePart -nil + ---@cast startDate -nil + + local dateString = startDate .. ' - ' .. timePart + local timestamp = DateExt.readTimestamp(dateString) + + assert(timestamp, 'Invalid date time combination: ' + .. '"|start_time=' .. timePart .. '" with "|(s)date=' .. startDate .. '"') + + return { + display = dateString, + storage = DateExt.formatTimestamp('c', timestamp), + } +end + ---@param args table function CustomLeague:_computePatch(args) local prefixPatch = function(patch) @@ -166,20 +191,32 @@ end ---@param widgets Widget[] ---@return Widget[] function CustomInjector:parse(id, widgets) - local args = self.caller.args + local caller = self.caller + local args = caller.args + local data = caller.data if id == 'gamesettings' then return { - Cell{name = 'Game Version', content = {self.caller:_getGameVersion(args)}}, - Cell{name = 'Server', content = {self.caller:_getServer(args)}} + Cell{name = 'Game Version', content = {caller:_getGameVersion(args)}}, + Cell{name = 'Server', content = {caller:_getServer(args)}} + } + elseif id == 'dates' and data.startTime.display then + local startTime = Countdown._create{date = data.startTime.display, rawdatetime = true} + + if data.startDate == data.endDate then + return {Cell{name = 'Start Time', content = {startTime}}} + end + return { + Cell{name = 'Start Time', content = {startTime}}, + Cell{name = 'End Date', content = {args.edate}}, } elseif id == 'customcontent' then if args.player_number and args.player_number > 0 or args.team_number then Array.appendWith(widgets, - Title{name = 'Participants'}, + Title{children = 'Participants'}, Cell{name = 'Number of Players', content = {args.raceBreakDown.total}}, Cell{name = 'Number of Teams', content = {args.team_number}}, - Breakdown{content = args.raceBreakDown.display or {}, classes = { 'infobox-center' }} + Breakdown{children = args.raceBreakDown.display or {}, classes = { 'infobox-center' }} ) end @@ -190,8 +227,8 @@ function CustomInjector:parse(id, widgets) local displayMaps = function(prefix, defaultTitle, maps) if String.isEmpty(args[prefix .. 1]) then return end Array.appendWith(widgets, - Title{name = args[prefix .. 'title'] or defaultTitle}, - Center{content = self.caller:_mapsDisplay(maps or self.caller:_getMaps(prefix, args))} + Title{children = args[prefix .. 'title'] or defaultTitle}, + Center{children = self.caller:_mapsDisplay(maps or self.caller:_getMaps(prefix, args))} ) end @@ -323,6 +360,7 @@ function CustomLeague:defineCustomPageVariables(args) Variables.varDefine('headtohead', args.headtohead or 'true') Variables.varDefine('tournament_maps', Json.stringify(args.maps)) Variables.varDefine('tournament_series_number', args.number and string.format('%05i', args.number) or nil) + Variables.varDefine('match_date', self.data.startTime.storage) end ---@param prefix string @@ -348,6 +386,7 @@ function CustomLeague:addToLpdb(lpdbData, args) lpdbData.maps = Json.stringify(args.maps) lpdbData.extradata.seriesnumber = args.number and string.format('%05i', args.number) or nil + lpdbData.extradata.starttime = self.data.startTime.storage return lpdbData end diff --git a/components/infobox/wikis/starcraft2/infobox_map_custom.lua b/components/infobox/wikis/starcraft2/infobox_map_custom.lua index e8b1aa5be3b..6fa0f09a188 100644 --- a/components/infobox/wikis/starcraft2/infobox_map_custom.lua +++ b/components/infobox/wikis/starcraft2/infobox_map_custom.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local Map = Lua.import('Module:Infobox/Map') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class Starcraft2MapInfobox: MapInfobox diff --git a/components/infobox/wikis/starcraft2/infobox_patch_custom.lua b/components/infobox/wikis/starcraft2/infobox_patch_custom.lua index 6565588fb59..1c99ab1e599 100644 --- a/components/infobox/wikis/starcraft2/infobox_patch_custom.lua +++ b/components/infobox/wikis/starcraft2/infobox_patch_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Patch = Lua.import('Module:Infobox/Patch') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class Starcraft2PatchInfobox: PatchInfobox diff --git a/components/infobox/wikis/starcraft2/infobox_person_map_maker_custom.lua b/components/infobox/wikis/starcraft2/infobox_person_map_maker_custom.lua index 88e82e93acf..1bae142d368 100644 --- a/components/infobox/wikis/starcraft2/infobox_person_map_maker_custom.lua +++ b/components/infobox/wikis/starcraft2/infobox_person_map_maker_custom.lua @@ -13,7 +13,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local CustomPerson = Lua.import('Module:Infobox/Person/Custom') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -53,7 +53,7 @@ function CustomInjector:parse(id, widgets) elseif id == 'achievements' then if String.isNotEmpty(args.maps_ladder) or String.isNotEmpty(args.maps_special) then return { - Title{name = 'Achievements'}, + Title{children = 'Achievements'}, Cell{name = 'Ladder maps created', content = {args.maps_ladder}}, Cell{name = 'Non-ladder competitive maps created', content = {args.maps_special}} } diff --git a/components/infobox/wikis/starcraft2/infobox_person_player_custom.lua b/components/infobox/wikis/starcraft2/infobox_person_player_custom.lua index 9e15f8e1d43..beac8e83e60 100644 --- a/components/infobox/wikis/starcraft2/infobox_person_player_custom.lua +++ b/components/infobox/wikis/starcraft2/infobox_person_player_custom.lua @@ -51,7 +51,7 @@ local RACE_FIELD_AS_CATEGORY_LINK = true local CURRENT_YEAR = tonumber(os.date('%Y')) local Injector = Lua.import('Module:Widget/Injector') -local Widgets = Lua.import('Module:Infobox/Widget/All') +local Widgets = Lua.import('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -140,8 +140,8 @@ function CustomInjector:parse(id, widgets) end return { - Title{name = 'Achievements'}, - Center{content = {Achievements.display(caller.infoboxAchievements)}}, + Title{children = 'Achievements'}, + Center{children = {Achievements.display(caller.infoboxAchievements)}}, Cell{name = 'All-Kills', content = {allkills > 0 and (ALL_KILL_ICON .. allkills) or nil}} } elseif id == 'achievements' then return {} diff --git a/components/infobox/wikis/starcraft2/infobox_series_custom.lua b/components/infobox/wikis/starcraft2/infobox_series_custom.lua index 21e71fe31ec..902593aec7d 100644 --- a/components/infobox/wikis/starcraft2/infobox_series_custom.lua +++ b/components/infobox/wikis/starcraft2/infobox_series_custom.lua @@ -23,7 +23,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local Series = Lua.import('Module:Infobox/Series') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local GAME_MOD = 'mod' diff --git a/components/infobox/wikis/starcraft2/infobox_show_custom.lua b/components/infobox/wikis/starcraft2/infobox_show_custom.lua index b643c5e2eae..ff8a2226341 100644 --- a/components/infobox/wikis/starcraft2/infobox_show_custom.lua +++ b/components/infobox/wikis/starcraft2/infobox_show_custom.lua @@ -14,7 +14,7 @@ local Namespace = require('Module:Namespace') local Injector = Lua.import('Module:Widget/Injector') local Show = Lua.import('Module:Infobox/Show') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class Starcraft2ShowInfobox: ShowInfobox @@ -29,7 +29,7 @@ function CustomShow.run(frame) show:setWidgetInjector(CustomInjector(show)) if Namespace.isMain() and show.args.edate == nil then - show.infobox:categories('Active Shows') + show:categories('Active Shows') end return show:createInfobox() diff --git a/components/infobox/wikis/starcraft2/infobox_skill_custom.lua b/components/infobox/wikis/starcraft2/infobox_skill_custom.lua index 9440a4c5d04..41f9e4431d3 100644 --- a/components/infobox/wikis/starcraft2/infobox_skill_custom.lua +++ b/components/infobox/wikis/starcraft2/infobox_skill_custom.lua @@ -19,7 +19,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Skill = Lua.import('Module:Infobox/Skill') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class Starcraft2SkillInfobox: SkillInfobox diff --git a/components/infobox/wikis/starcraft2/infobox_strategy_custom.lua b/components/infobox/wikis/starcraft2/infobox_strategy_custom.lua index bdc0ab1c5f4..ee4688f16b5 100644 --- a/components/infobox/wikis/starcraft2/infobox_strategy_custom.lua +++ b/components/infobox/wikis/starcraft2/infobox_strategy_custom.lua @@ -16,7 +16,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Strategy = Lua.import('Module:Infobox/Strategy') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header @@ -84,7 +84,7 @@ function CustomStrategy:setWikiCategories() return self end - self.infobox:categories(unpack(self:_getCategories())) + self:categories(unpack(self:_getCategories())) return self end diff --git a/components/infobox/wikis/starcraft2/infobox_team_custom.lua b/components/infobox/wikis/starcraft2/infobox_team_custom.lua index 1d4309be4a0..3cf0a451013 100644 --- a/components/infobox/wikis/starcraft2/infobox_team_custom.lua +++ b/components/infobox/wikis/starcraft2/infobox_team_custom.lua @@ -27,7 +27,7 @@ local Team = Lua.import('Module:Infobox/Team') local OpponentLibraries = require('Module:OpponentLibraries') local Opponent = OpponentLibraries.Opponent -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Breakdown = Widgets.Breakdown local Cell = Widgets.Cell local Center = Widgets.Center @@ -78,13 +78,13 @@ function CustomInjector:parse(id, widgets) local achievements, soloAchievements = Achievements.teamAndTeamSolo() widgets = {} if achievements then - table.insert(widgets, Title{name = 'Achievements'}) - table.insert(widgets, Center{content = {achievements}}) + table.insert(widgets, Title{children = 'Achievements'}) + table.insert(widgets, Center{children = {achievements}}) end if soloAchievements then - table.insert(widgets, Title{name = 'Solo Achievements'}) - table.insert(widgets, Center{content = {soloAchievements}}) + table.insert(widgets, Title{children = 'Solo Achievements'}) + table.insert(widgets, Center{children = {soloAchievements}}) end --need this ABOVE the history display and below the @@ -92,9 +92,9 @@ function CustomInjector:parse(id, widgets) local raceBreakdown = RaceBreakdown.run(args) if raceBreakdown then Array.appendWith(widgets, - Title{name = 'Player Breakdown'}, + Title{children = 'Player Breakdown'}, Cell{name = 'Number of Players', content = {raceBreakdown.total}}, - Breakdown{content = raceBreakdown.display, classes = {'infobox-center'}} + Breakdown{children = raceBreakdown.display, classes = {'infobox-center'}} ) end diff --git a/components/infobox/wikis/starcraft2/infobox_unit_custom.lua b/components/infobox/wikis/starcraft2/infobox_unit_custom.lua index d7550ecbd6a..bc21e9ee11b 100644 --- a/components/infobox/wikis/starcraft2/infobox_unit_custom.lua +++ b/components/infobox/wikis/starcraft2/infobox_unit_custom.lua @@ -19,7 +19,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Unit = Lua.import('Module:Infobox/Unit') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -99,7 +99,7 @@ function CustomInjector:parse(id, widgets) elseif id == 'custom' then Array.appendWith( widgets, - Title{name = 'Unit stats'}, + Title{children = 'Unit stats'}, Cell{name = 'Defense', content = {self.caller:_defenseDisplay()}}, Cell{name = 'Attributes', content = {args.attributes}}, Cell{name = 'Energy', content = {args.energy}}, @@ -117,7 +117,7 @@ function CustomInjector:parse(id, widgets) ) if args.game ~= GAME_LOTV and args.buildtime then - table.insert(widgets, Center{content = { + table.insert(widgets, Center{children = { 'Note: ' .. 'All time-related values are expressed assuming Normal speed, as they were before LotV.' .. ' See [[Game Speed]].' @@ -217,7 +217,7 @@ function CustomUnit:_getAttack(index) self:_storeAttack(index) return { - Title{name = attackHeader}, + Title{children = attackHeader}, Cell{name = 'Targets', content = {args['attack' .. index .. '_target']}}, Cell{name = 'Damage', content = {args['attack' .. index .. '_damage']}}, Cell{name = '[[Damage Per Second|DPS]]', content = {args['attack' .. index .. '_dps']}}, diff --git a/components/infobox/wikis/starcraft2/infobox_unofficial_world_champion_custom.lua b/components/infobox/wikis/starcraft2/infobox_unofficial_world_champion_custom.lua index 769a2fb98d3..a096bea0a18 100644 --- a/components/infobox/wikis/starcraft2/infobox_unofficial_world_champion_custom.lua +++ b/components/infobox/wikis/starcraft2/infobox_unofficial_world_champion_custom.lua @@ -16,7 +16,7 @@ local Injector = Lua.import('Module:Widget/Injector') local UnofficialWorldChampion = Lua.import('Module:Infobox/UnofficialWorldChampion') local RaceBreakdown = Lua.import('Module:Infobox/Extension/RaceBreakdown') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Breakdown = Widgets.Breakdown local Cell = Widgets.Cell local Title = Widgets.Title @@ -57,8 +57,8 @@ function CustomInjector:parse(id, widgets) Array.extendWith(widgets, { - raceBreakdown and Title{name = 'Racial Distribution of Champions'} or nil, - raceBreakdown and Breakdown{content = raceBreakdown.display, classes = { 'infobox-center' }} or nil, + raceBreakdown and Title{children = 'Racial Distribution of Champions'} or nil, + raceBreakdown and Breakdown{children = raceBreakdown.display, classes = { 'infobox-center' }} or nil, }, self.caller:_buildCellsFromBase('countries with multiple champions', 'Countries with Multiple Champions'), self.caller:_buildCellsFromBase('teams with multiple champions', 'Teams with Multiple Champions') @@ -76,7 +76,7 @@ function CustomUnofficialWorldChampion:_buildCellsFromBase(base, title) return {} end - local widgets = {Title{name = title}} + local widgets = {Title{children = title}} for key, value in Table.iter.pairsByPrefix(args, base .. ' ') do table.insert(widgets, Cell{name = (args[key .. ' no'] or '?') .. ' champions', content = {value}}) end diff --git a/components/infobox/wikis/stormgate/infobox_building_custom.lua b/components/infobox/wikis/stormgate/infobox_building_custom.lua index 0d045c2aab9..5e846e21eda 100644 --- a/components/infobox/wikis/stormgate/infobox_building_custom.lua +++ b/components/infobox/wikis/stormgate/infobox_building_custom.lua @@ -13,6 +13,7 @@ local Class = require('Module:Class') local CostDisplay = require('Module:Infobox/Extension/CostDisplay') local Faction = require('Module:Faction') local Hotkeys = require('Module:Hotkey') +local Logic = require('Module:Logic') local Lua = require('Module:Lua') local Page = require('Module:Page') local String = require('Module:StringUtils') @@ -22,7 +23,7 @@ local MessageBox = require('Module:Message box') local Injector = Lua.import('Module:Widget/Injector') local Building = Lua.import('Module:Infobox/Building') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -144,6 +145,18 @@ function CustomBuilding:nameDisplay(args) return factionIcon .. (args.name or self.pagename) end +---@param args table +---@return string? +function CustomBuilding:subHeaderDisplay(args) + if Logic.isEmpty(args.subfaction) or + string.find(args.subfaction, '1v1') or + string.find(args.subfaction, self.pagename) then return end + return tostring(mw.html.create('span') + :css('font-size', '90%') + :wikitext('Hero: ' .. self:_displayCsvAsPageCsv(args.subfaction)) + ) +end + ---@param hotkey1 string? ---@param hotkey2 string? ---@return string? @@ -191,6 +204,7 @@ function CustomBuilding:setLpdbData(args) extradata = mw.ext.LiquipediaDB.lpdb_create_json{ deprecated = args.deprecated or '', introduced = args.introduced or '', + subfaction = Array.parseCommaSeparatedString(args.subfaction), size = tonumber(args.size), sight = tonumber(args.sight), luminite = tonumber(args.luminite), @@ -314,7 +328,7 @@ function CustomBuilding:_parseForCreeps(id, widgets) Cell{name = 'Start Level', content = {startLevel}}, Cell{name = 'Defenders', content = {self._displayCreepDefenders(creeps)}}, Cell{name = 'Respawn', content = {args.respawn and args.respawn .. 's'}}, - Title{name = 'Tower Rewards'}, + Title{children = 'Tower Rewards'}, Cell{name = 'Capture Point', content = {args.capture_point}}, Cell{name = 'Global Buff', content = {args.global_buff}}, } diff --git a/components/infobox/wikis/stormgate/infobox_league_custom.lua b/components/infobox/wikis/stormgate/infobox_league_custom.lua index 15f0864c899..1e37583adf2 100644 --- a/components/infobox/wikis/stormgate/infobox_league_custom.lua +++ b/components/infobox/wikis/stormgate/infobox_league_custom.lua @@ -21,7 +21,7 @@ local League = Lua.import('Module:Infobox/League') local PatchAuto = Lua.import('Module:Infobox/Extension/PatchAuto') local RaceBreakdown = Lua.import('Module:Infobox/Extension/RaceBreakdown') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Breakdown = Widgets.Breakdown local Cell = Widgets.Cell local Center = Widgets.Center @@ -105,16 +105,16 @@ function CustomInjector:parse(id, widgets) elseif id == 'customcontent' then if args.player_number and args.player_number > 0 then Array.appendWith(widgets, - Title{name = 'Player Breakdown'}, + Title{children = 'Player Breakdown'}, Cell{name = 'Number of Players', content = {args.raceBreakDown.total}}, - Breakdown{content = args.raceBreakDown.display, classes = { 'infobox-center' }} + Breakdown{children = args.raceBreakDown.display, classes = { 'infobox-center' }} ) end --teams section if Logic.isNumeric(args.team_number) and tonumber(args.team_number) > 0 then Array.appendWith(widgets, - Title{name = 'Teams'}, + Title{children = 'Teams'}, Cell{name = 'Number of Teams', content = {args.team_number}} ) end @@ -122,8 +122,8 @@ function CustomInjector:parse(id, widgets) --maps if String.isNotEmpty(args.map1) then Array.appendWith(widgets, - Title{name = 'Maps'}, - Center{content = {self.caller:_mapsDisplay(args.maps)}} + Title{children = 'Maps'}, + Center{children = {self.caller:_mapsDisplay(args.maps)}} ) end end diff --git a/components/infobox/wikis/stormgate/infobox_map_custom.lua b/components/infobox/wikis/stormgate/infobox_map_custom.lua index e22a05549c9..1adcd05be1d 100644 --- a/components/infobox/wikis/stormgate/infobox_map_custom.lua +++ b/components/infobox/wikis/stormgate/infobox_map_custom.lua @@ -17,7 +17,7 @@ local Table = require('Module:Table') local Injector = Lua.import('Module:Widget/Injector') local Map = Lua.import('Module:Infobox/Map') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -130,7 +130,7 @@ function CustomInjector:parse(id, widgets) Cell{name = 'Available Resources', content = {self.caller:_resourcesDisplay(args)}}, }, self.caller:_addCellsFromDataTable(args, LADDER_HISTORY), - {hasCampData and Title{name = 'Camp Information'} or nil}, + {hasCampData and Title{children = 'Camp Information'} or nil}, self.caller:_addCellsFromDataTable(args, CAMPS) ) end diff --git a/components/infobox/wikis/stormgate/infobox_person_player_custom.lua b/components/infobox/wikis/stormgate/infobox_person_player_custom.lua index 7975ec8d0f6..a5803d21479 100644 --- a/components/infobox/wikis/stormgate/infobox_person_player_custom.lua +++ b/components/infobox/wikis/stormgate/infobox_person_player_custom.lua @@ -42,7 +42,7 @@ local ROLES = { } local Injector = Lua.import('Module:Widget/Injector') -local Widgets = Lua.import('Module:Infobox/Widget/All') +local Widgets = Lua.import('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -107,8 +107,8 @@ function CustomInjector:parse(id, widgets) if not achievements then return {} end return { - Title{name = 'Achievements'}, - Center{content = {achievements}}, + Title{children = 'Achievements'}, + Center{children = {achievements}}, } elseif id == 'history' and string.match(args.retired or '', '%d%d%d%d') then table.insert(widgets, Cell{name = 'Retired', content = {args.retired}}) diff --git a/components/infobox/wikis/stormgate/infobox_skill_custom.lua b/components/infobox/wikis/stormgate/infobox_skill_custom.lua index c01a6cf1cb5..7c101fb169b 100644 --- a/components/infobox/wikis/stormgate/infobox_skill_custom.lua +++ b/components/infobox/wikis/stormgate/infobox_skill_custom.lua @@ -20,7 +20,7 @@ local MessageBox = require('Module:Message box') local Injector = Lua.import('Module:Widget/Injector') local Skill = Lua.import('Module:Infobox/Skill') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class StormgateSkillInfobox: SkillInfobox diff --git a/components/infobox/wikis/stormgate/infobox_team_custom.lua b/components/infobox/wikis/stormgate/infobox_team_custom.lua index 93e36166172..2d5e8ae8570 100644 --- a/components/infobox/wikis/stormgate/infobox_team_custom.lua +++ b/components/infobox/wikis/stormgate/infobox_team_custom.lua @@ -16,7 +16,7 @@ local Injector = Lua.import('Module:Widget/Injector') local RaceBreakdown = Lua.import('Module:Infobox/Extension/RaceBreakdown') local Team = Lua.import('Module:Infobox/Team') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Breakdown = Widgets.Breakdown local Cell = Widgets.Cell local Center = Widgets.Center @@ -46,21 +46,21 @@ function CustomInjector:parse(id, widgets) local achievements, soloAchievements = Achievements.teamAndTeamSolo() widgets = {} if achievements then - table.insert(widgets, Title{name = 'Achievements'}) - table.insert(widgets, Center{content = {achievements}}) + table.insert(widgets, Title{children = 'Achievements'}) + table.insert(widgets, Center{children = {achievements}}) end if soloAchievements then - table.insert(widgets, Title{name = 'Solo Achievements'}) - table.insert(widgets, Center{content = {soloAchievements}}) + table.insert(widgets, Title{children = 'Solo Achievements'}) + table.insert(widgets, Center{children = {soloAchievements}}) end local raceBreakdown = RaceBreakdown.run(args) if raceBreakdown then Array.appendWith(widgets, - Title{name = 'Player Breakdown'}, + Title{children = 'Player Breakdown'}, Cell{name = 'Number of Players', content = {raceBreakdown.total}}, - Breakdown{content = raceBreakdown.display, classes = { 'infobox-center' }} + Breakdown{children = raceBreakdown.display, classes = { 'infobox-center' }} ) end diff --git a/components/infobox/wikis/stormgate/infobox_unit_custom.lua b/components/infobox/wikis/stormgate/infobox_unit_custom.lua index 6e70bc3cb2a..6d371f02893 100644 --- a/components/infobox/wikis/stormgate/infobox_unit_custom.lua +++ b/components/infobox/wikis/stormgate/infobox_unit_custom.lua @@ -13,6 +13,7 @@ local Class = require('Module:Class') local CostDisplay = require('Module:Infobox/Extension/CostDisplay') local Faction = require('Module:Faction') local Hotkeys = require('Module:Hotkey') +local Logic = require('Module:Logic') local Lua = require('Module:Lua') local Page = require('Module:Page') local String = require('Module:StringUtils') @@ -21,7 +22,7 @@ local MessageBox = require('Module:Message box') local Injector = Lua.import('Module:Widget/Injector') local Unit = Lua.import('Module:Infobox/Unit') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class Stormgate2UnitInfobox: UnitInfobox @@ -155,7 +156,9 @@ end ---@param args table ---@return string? function CustomUnit:subHeaderDisplay(args) - if string.find(args.subfaction, '1v1') or string.find(args.subfaction, self.pagename) then return end + if Logic.isEmpty(args.subfaction) or + string.find(args.subfaction, '1v1') or + string.find(args.subfaction, self.pagename) then return end return tostring(mw.html.create('span') :css('font-size', '90%') :wikitext('Hero: ' .. self:_displayCsvAsPageCsv(args.subfaction)) diff --git a/components/infobox/wikis/teamfortress/infobox_company_custom.lua b/components/infobox/wikis/teamfortress/infobox_company_custom.lua index 74ab9e2b911..8a27946c0ba 100644 --- a/components/infobox/wikis/teamfortress/infobox_company_custom.lua +++ b/components/infobox/wikis/teamfortress/infobox_company_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Company = Lua.import('Module:Infobox/Company') local Injector = Lua.import('Module:Widget/Injector') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class TeamfortressCompanyInfobox: CompanyInfobox diff --git a/components/infobox/wikis/teamfortress/infobox_league_custom.lua b/components/infobox/wikis/teamfortress/infobox_league_custom.lua index 5e84bb94e16..2fc9bc2e8e4 100644 --- a/components/infobox/wikis/teamfortress/infobox_league_custom.lua +++ b/components/infobox/wikis/teamfortress/infobox_league_custom.lua @@ -15,7 +15,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -59,8 +59,8 @@ function CustomInjector:parse(id, widgets) PageLink.makeInternalLink(map) ))) end - table.insert(widgets, Title{name = 'Maps'}) - table.insert(widgets, Center{content = {table.concat(maps, ' • ')}}) + table.insert(widgets, Title{children = 'Maps'}) + table.insert(widgets, Center{children = {table.concat(maps, ' • ')}}) end end return widgets diff --git a/components/infobox/wikis/teamfortress/infobox_person_player_custom.lua b/components/infobox/wikis/teamfortress/infobox_person_player_custom.lua index 7902a407fe8..600137d4259 100644 --- a/components/infobox/wikis/teamfortress/infobox_person_player_custom.lua +++ b/components/infobox/wikis/teamfortress/infobox_person_player_custom.lua @@ -10,7 +10,7 @@ local Class = require('Module:Class') local Lua = require('Module:Lua') local Template = require('Module:Template') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Injector = Lua.import('Module:Widget/Injector') diff --git a/components/infobox/wikis/tetris/infobox_league_custom.lua b/components/infobox/wikis/tetris/infobox_league_custom.lua index d69d5e4d479..d2b429a3878 100644 --- a/components/infobox/wikis/tetris/infobox_league_custom.lua +++ b/components/infobox/wikis/tetris/infobox_league_custom.lua @@ -17,7 +17,7 @@ local Table = require('Module:Table') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class TetrisLeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/tetris/infobox_person_player_custom.lua b/components/infobox/wikis/tetris/infobox_person_player_custom.lua index 6aca474f80e..13d8b1e1790 100644 --- a/components/infobox/wikis/tetris/infobox_person_player_custom.lua +++ b/components/infobox/wikis/tetris/infobox_person_player_custom.lua @@ -13,7 +13,7 @@ local Role = require('Module:Role') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class TetrisInfoboxPlayer: Person diff --git a/components/infobox/wikis/tft/infobox_league_custom.lua b/components/infobox/wikis/tft/infobox_league_custom.lua index 3478fcfaf47..f9670b04044 100644 --- a/components/infobox/wikis/tft/infobox_league_custom.lua +++ b/components/infobox/wikis/tft/infobox_league_custom.lua @@ -16,7 +16,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class TftLeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/tft/infobox_person_player_custom.lua b/components/infobox/wikis/tft/infobox_person_player_custom.lua index 4af80427efe..575ce9e32fc 100644 --- a/components/infobox/wikis/tft/infobox_person_player_custom.lua +++ b/components/infobox/wikis/tft/infobox_person_player_custom.lua @@ -13,7 +13,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local ROLES = { diff --git a/components/infobox/wikis/trackmania/infobox_league_custom.lua b/components/infobox/wikis/trackmania/infobox_league_custom.lua index 20e13189ef0..a7f7bf8c8d6 100644 --- a/components/infobox/wikis/trackmania/infobox_league_custom.lua +++ b/components/infobox/wikis/trackmania/infobox_league_custom.lua @@ -18,7 +18,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -67,7 +67,7 @@ function CustomInjector:parse(id, widgets) end) }) elseif id == 'customcontent' then - table.insert(widgets, Title{name = String.isNotEmpty(args.team_number) and 'Teams' or 'Players'}) + table.insert(widgets, Title{children = String.isNotEmpty(args.team_number) and 'Teams' or 'Players'}) table.insert(widgets, Cell{ name = 'Number of Teams', content = {args.team_number} @@ -79,12 +79,12 @@ function CustomInjector:parse(id, widgets) local maps = self.caller:getAllArgsForBase(args, 'map') if #maps > 0 then - table.insert(widgets, Title{name = 'Maps'}) - table.insert(widgets, Center{content = {table.concat(maps, ' • ')}}) + table.insert(widgets, Title{children = 'Maps'}) + table.insert(widgets, Center{children = {table.concat(maps, ' • ')}}) end if args.circuit or args.circuit_next or args.circuit_previous then - table.insert(widgets, Title{name = 'Circuit Information'}) + table.insert(widgets, Title{children = 'Circuit Information'}) self.caller:_createCircuitInformation(widgets) end end @@ -166,7 +166,7 @@ function CustomLeague:_createCircuitInformation(widgets) }, Cell{name = 'Circuit Tier', content = {args.circuittier}}, Cell{name = 'Tournament Region', content = {args.region}}, - Chronology{content = {next = args.circuit_next, previous = args.circuit_previous}} + Chronology{links = {next = args.circuit_next, previous = args.circuit_previous}} ) end diff --git a/components/infobox/wikis/trackmania/infobox_person_player_custom.lua b/components/infobox/wikis/trackmania/infobox_person_player_custom.lua index b0f27fb722c..212c380e36e 100644 --- a/components/infobox/wikis/trackmania/infobox_person_player_custom.lua +++ b/components/infobox/wikis/trackmania/infobox_person_player_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class TrackmaniaInfoboxPlayer: Person diff --git a/components/infobox/wikis/valorant/infobox_league_custom.lua b/components/infobox/wikis/valorant/infobox_league_custom.lua index b5fc47f9eaa..799666fe963 100644 --- a/components/infobox/wikis/valorant/infobox_league_custom.lua +++ b/components/infobox/wikis/valorant/infobox_league_custom.lua @@ -19,7 +19,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -71,8 +71,8 @@ function CustomInjector:parse(id, widgets) ))) end table.sort(maps) - table.insert(widgets, Title{name = 'Maps'}) - table.insert(widgets, Center{content = {table.concat(maps, ' • ')}}) + table.insert(widgets, Title{children = 'Maps'}) + table.insert(widgets, Center{children = {table.concat(maps, ' • ')}}) end elseif id == 'gamesettings' then table.insert(widgets, Cell{ diff --git a/components/infobox/wikis/valorant/infobox_map_custom.lua b/components/infobox/wikis/valorant/infobox_map_custom.lua index 52295854208..f12a681cd49 100644 --- a/components/infobox/wikis/valorant/infobox_map_custom.lua +++ b/components/infobox/wikis/valorant/infobox_map_custom.lua @@ -14,7 +14,7 @@ local Injector = Lua.import('Module:Widget/Injector') local Map = Lua.import('Module:Infobox/Map') local Flags = Lua.import('Module:Flags') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class ValorantMapInfobox: MapInfobox diff --git a/components/infobox/wikis/valorant/infobox_person_player_custom.lua b/components/infobox/wikis/valorant/infobox_person_player_custom.lua index 242780169ab..0b46e28009f 100644 --- a/components/infobox/wikis/valorant/infobox_person_player_custom.lua +++ b/components/infobox/wikis/valorant/infobox_person_player_custom.lua @@ -24,7 +24,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local ROLES = { diff --git a/components/infobox/wikis/valorant/infobox_series_custom.lua b/components/infobox/wikis/valorant/infobox_series_custom.lua index 6bfb6dd0673..db4583aeb52 100644 --- a/components/infobox/wikis/valorant/infobox_series_custom.lua +++ b/components/infobox/wikis/valorant/infobox_series_custom.lua @@ -15,7 +15,7 @@ local Table = require('Module:Table') local Injector = Lua.import('Module:Widget/Injector') local Series = Lua.import('Module:Infobox/Series') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Chronology = Widgets.Chronology local Title = Widgets.Title @@ -56,9 +56,9 @@ function CustomInjector:parse(id, widgets) elseif id == 'customcontent' then if String.isNotEmpty(args.previous) or String.isNotEmpty(args.next) then return { - Title{name = 'Chronology'}, + Title{children = 'Chronology'}, Chronology{ - content = { + links = { previous = args.previous, next = args.next, } diff --git a/components/infobox/wikis/valorant/infobox_team_custom.lua b/components/infobox/wikis/valorant/infobox_team_custom.lua index 4286b170f77..1b596ee3e83 100644 --- a/components/infobox/wikis/valorant/infobox_team_custom.lua +++ b/components/infobox/wikis/valorant/infobox_team_custom.lua @@ -14,7 +14,7 @@ local Template = require('Module:Template') local Injector = Lua.import('Module:Widget/Injector') local Team = Lua.import('Module:Infobox/Team') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class ValorantInfoboxTeam: InfoboxTeam diff --git a/components/infobox/wikis/warcraft/infobox_building_custom.lua b/components/infobox/wikis/warcraft/infobox_building_custom.lua index df6fffe3b39..aafcfa29473 100644 --- a/components/infobox/wikis/warcraft/infobox_building_custom.lua +++ b/components/infobox/wikis/warcraft/infobox_building_custom.lua @@ -22,7 +22,7 @@ local Injector = Lua.import('Module:Widget/Injector') local Building = Lua.import('Module:Infobox/Building') local Shared = Lua.import('Module:Infobox/Extension/BuildingUnitShared') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Center = Widgets.Center local Title = Widgets.Title @@ -97,15 +97,15 @@ function CustomBuilding:addCustomCells(widgets) local pathingMap = CustomBuilding._pathingMap(args) if pathingMap then Array.appendWith(widgets, - Title{name = 'Pathing Map'}, - Center{content = {pathingMap}} + Title{children = 'Pathing Map'}, + Center{children = {pathingMap}} ) end if args.icon then Array.appendWith(widgets, - Title{name = 'Icon'}, - Center{content = {'[[File:Wc3BTN' .. args.icon .. '.png]]'}} + Title{children = 'Icon'}, + Center{children = {'[[File:Wc3BTN' .. args.icon .. '.png]]'}} ) end diff --git a/components/infobox/wikis/warcraft/infobox_character_custom.lua b/components/infobox/wikis/warcraft/infobox_character_custom.lua index 79cd333a23e..adb72630d6c 100644 --- a/components/infobox/wikis/warcraft/infobox_character_custom.lua +++ b/components/infobox/wikis/warcraft/infobox_character_custom.lua @@ -17,7 +17,7 @@ local Math = require('Module:MathUtil') local Injector = Lua.import('Module:Widget/Injector') local Character = Lua.import('Module:Infobox/Character') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local BreakDown = Widgets.Breakdown local Cell = Widgets.Cell local Center = Widgets.Center @@ -72,18 +72,18 @@ function CustomInjector:parse(id, widgets) if id == 'custom' then return Array.append(widgets, - Title{name = 'Attributes'}, - BreakDown{content = { + Title{children = 'Attributes'}, + BreakDown{children = { self.caller:_basicAttribute('str'), self.caller:_basicAttribute('agi'), self.caller:_basicAttribute('int'), }, classes = {'infobox-center'}}, - BreakDown{content = { + BreakDown{children = { self.caller:_getDamageAttribute(), self.caller:_getArmorAttribute(), self.caller:_getPrimaryAttribute(), }, classes = {'infobox-center'}}, - Title{name = 'Base Stats'}, + Title{children = 'Base Stats'}, Cell{name = '[[Movement Speed]]', content = {args.basespeed}}, Cell{name = '[[Sight Range]]', content = {args.sightrange or ( Abbreviation.make(args.daysight or 1800, 'Day') .. ' / ' .. Abbreviation.make(args.nightsight or 800, 'Night') @@ -94,10 +94,10 @@ function CustomInjector:parse(id, widgets) Cell{name = 'Base Attack Time', content = {args.attacktime}}, Cell{name = 'Turn Rate', content = {args.turnrate}}, Cell{name = 'Hotkey', content = {Hotkeys.hotkey(args.hotkey)}}, - args.icon and Title{name = 'Icon'} or nil, - Center{content = {self.caller:_displayIcon()}}, - Title{name = 'Level Changes'}, - BreakDown{content = {'[[Experience|Level]]:', 1, 5, 10}, contentClasses = LEVEL_CHANGE_CLASSES}, + args.icon and Title{children = 'Icon'} or nil, + Center{children = {self.caller:_displayIcon()}}, + Title{children = 'Level Changes'}, + BreakDown{children = {'[[Experience|Level]]:', 1, 5, 10}, contentClasses = LEVEL_CHANGE_CLASSES}, BreakDown(CustomCharacter._toLevelChangesRow( function(gainFactor) return self.caller:_calculateHitPoints(gainFactor) end, '[[Hit Points]]:')), BreakDown(CustomCharacter._toLevelChangesRow(function(gainFactor) @@ -345,7 +345,7 @@ end ---@param title string ---@return {content: table, contentClasses: table}} function CustomCharacter._toLevelChangesRow(funct, title) - return {content = {title, funct(0), funct(4), funct(9)}, contentClasses = LEVEL_CHANGE_CLASSES} + return {children = {title, funct(0), funct(4), funct(9)}, contentClasses = LEVEL_CHANGE_CLASSES} end return CustomCharacter diff --git a/components/infobox/wikis/warcraft/infobox_company_custom.lua b/components/infobox/wikis/warcraft/infobox_company_custom.lua index 8b019071aff..60fdc278d1b 100644 --- a/components/infobox/wikis/warcraft/infobox_company_custom.lua +++ b/components/infobox/wikis/warcraft/infobox_company_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Company = Lua.import('Module:Infobox/Company') local Injector = Lua.import('Module:Widget/Injector') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class WarcraftCompanyInfobox: CompanyInfobox diff --git a/components/infobox/wikis/warcraft/infobox_item_custom.lua b/components/infobox/wikis/warcraft/infobox_item_custom.lua index a781dbe614e..fdd0a040b6c 100644 --- a/components/infobox/wikis/warcraft/infobox_item_custom.lua +++ b/components/infobox/wikis/warcraft/infobox_item_custom.lua @@ -21,7 +21,7 @@ local Table = require('Module:Table') local Injector = Lua.import('Module:Widget/Injector') local Item = Lua.import('Module:Infobox/Item') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Center = Widgets.Center local Title = Widgets.Title @@ -94,14 +94,14 @@ function CustomInjector:parse(id, widgets) local args = self.caller.args if id == 'custom' then return { - args.desc and Title{name = 'Description'} or nil, - Center{content = {args.desc}}, - args.history and Title{name = 'Item History'} or nil, - Center{content = {args.history}} + args.desc and Title{children = 'Description'} or nil, + Center{children = {args.desc}}, + args.history and Title{children = 'Item History'} or nil, + Center{children = {args.history}} } elseif id == 'info' then return { - Title{name = 'Item Information'}, + Title{children = 'Item Information'}, Cell{name = 'Level', content = {args.level}}, Cell{name = 'Class', content = {CLASSES[(args.class or ''):lower()] or args.class}}, Cell{name = 'Charges', content = {args.charges}}, diff --git a/components/infobox/wikis/warcraft/infobox_league_custom.lua b/components/infobox/wikis/warcraft/infobox_league_custom.lua index 6b581c3c497..a613f0ffcc0 100644 --- a/components/infobox/wikis/warcraft/infobox_league_custom.lua +++ b/components/infobox/wikis/warcraft/infobox_league_custom.lua @@ -27,7 +27,7 @@ local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') local RaceBreakdown = Lua.import('Module:Infobox/Extension/RaceBreakdown') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Breakdown = Widgets.Breakdown local Cell = Widgets.Cell local Center = Widgets.Center @@ -44,8 +44,10 @@ local OFFLINE = 'offline' local ONLINE = 'online' local GAME_REFORGED = 'wc3r' -local GAME_FROZEN_THRONE = 'tft' -local GAME_DEFAULT_SWITCH_DATE = '2020-01-01' +local GAME_REIGN_OF_CHAOS = 'reignofchaos' +local GAME_FROZEN_THRONE = 'frozenthrone' +local START_DATE_FROZEN_THRONE = '2003-07-01' +local START_DATE_REFORGED = '2020-01-01' local MODES = { team = {tier = 'Team', store = 'team', category = 'Team'}, @@ -177,16 +179,18 @@ end ---@param game string? ---@return string? function CustomLeague:_determineGame(game) - game = Game.toIdentifier{game = game} + game = Game.toIdentifier{game = game, useDefault = false} if game then return game end local startDate = self.data.startDate or self.data.endDate - if startDate and startDate > GAME_DEFAULT_SWITCH_DATE then + if startDate and startDate > START_DATE_REFORGED then return Game.toIdentifier{game = GAME_REFORGED} + elseif startDate and startDate > START_DATE_FROZEN_THRONE then + return Game.toIdentifier{game = GAME_FROZEN_THRONE} end - return Game.toIdentifier{game = GAME_FROZEN_THRONE} + return Game.toIdentifier{game = GAME_REIGN_OF_CHAOS} end ---@param args table @@ -266,13 +270,13 @@ function CustomInjector:parse(id, widgets) elseif id == 'customcontent' then local playerNumber = caller.data.playerNumberDisplay if playerNumber or args.team_number then - table.insert(widgets, Title{name = 'Participants breakdown'}) + table.insert(widgets, Title{children = 'Participants breakdown'}) end if playerNumber then Array.appendWith(widgets, Cell{name = 'Number of Players', content = {playerNumber}}, - Breakdown{content = caller.data.raceBreakDown.display or {}, classes = { 'infobox-center' }} + Breakdown{children = caller.data.raceBreakDown.display or {}, classes = { 'infobox-center' }} ) end @@ -291,8 +295,8 @@ function CustomInjector:parse(id, widgets) local displayMaps = function(prefix, defaultTitle, maps) if String.isEmpty(args[prefix .. 1]) then return end Array.appendWith(widgets, - Title{name = args[prefix .. 'title'] or defaultTitle}, - Center{content = caller:_mapsDisplay(maps or caller:_getMaps(prefix, args))} + Title{children = args[prefix .. 'title'] or defaultTitle}, + Center{children = caller:_mapsDisplay(maps or caller:_getMaps(prefix, args))} ) end diff --git a/components/infobox/wikis/warcraft/infobox_map_custom.lua b/components/infobox/wikis/warcraft/infobox_map_custom.lua index 478d70a4fff..5eb9750ee26 100644 --- a/components/infobox/wikis/warcraft/infobox_map_custom.lua +++ b/components/infobox/wikis/warcraft/infobox_map_custom.lua @@ -15,7 +15,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Map = Lua.import('Module:Infobox/Map') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class WarcraftMapInfobox: MapInfobox diff --git a/components/infobox/wikis/warcraft/infobox_patch_custom.lua b/components/infobox/wikis/warcraft/infobox_patch_custom.lua index bf00926353c..5ac6d9a28f4 100644 --- a/components/infobox/wikis/warcraft/infobox_patch_custom.lua +++ b/components/infobox/wikis/warcraft/infobox_patch_custom.lua @@ -17,7 +17,7 @@ local Table = require('Module:Table') local Injector = Lua.import('Module:Widget/Injector') local Patch = Lua.import('Module:Infobox/Patch') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class WarcraftPatchInfobox: PatchInfobox diff --git a/components/infobox/wikis/warcraft/infobox_person_player_custom.lua b/components/infobox/wikis/warcraft/infobox_person_player_custom.lua index 0f19d5b36dd..3fea5aea73b 100644 --- a/components/infobox/wikis/warcraft/infobox_person_player_custom.lua +++ b/components/infobox/wikis/warcraft/infobox_person_player_custom.lua @@ -16,7 +16,7 @@ local Achievements = Lua.import('Module:Infobox/Extension/Achievements') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local CURRENT_YEAR = tonumber(os.date('%Y')) diff --git a/components/infobox/wikis/warcraft/infobox_skill_custom.lua b/components/infobox/wikis/warcraft/infobox_skill_custom.lua index 30e9998f6a5..430e6b661d8 100644 --- a/components/infobox/wikis/warcraft/infobox_skill_custom.lua +++ b/components/infobox/wikis/warcraft/infobox_skill_custom.lua @@ -16,7 +16,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Skill = Lua.import('Module:Infobox/Skill') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class WarcraftSkillInfobox: SkillInfobox diff --git a/components/infobox/wikis/warcraft/infobox_strategy_custom.lua b/components/infobox/wikis/warcraft/infobox_strategy_custom.lua index 8cf6548dcd4..70c4fbd8b2f 100644 --- a/components/infobox/wikis/warcraft/infobox_strategy_custom.lua +++ b/components/infobox/wikis/warcraft/infobox_strategy_custom.lua @@ -15,7 +15,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Strategy = Lua.import('Module:Infobox/Strategy') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Header = Widgets.Header @@ -34,7 +34,7 @@ function CustomStrategy.run(frame) if Namespace.isMain() then local categories = strategy:_getCategories(strategy.args.race, strategy.args.matchups) - strategy.infobox:categories(unpack(categories)) + strategy:categories(unpack(categories)) end return strategy:createInfobox() diff --git a/components/infobox/wikis/warcraft/infobox_team_custom.lua b/components/infobox/wikis/warcraft/infobox_team_custom.lua index dc301d5cd8e..2e6be0c23a4 100644 --- a/components/infobox/wikis/warcraft/infobox_team_custom.lua +++ b/components/infobox/wikis/warcraft/infobox_team_custom.lua @@ -18,7 +18,7 @@ local Achievements = Lua.import('Module:Infobox/Extension/Achievements') local Injector = Lua.import('Module:Widget/Injector') local Team = Lua.import('Module:Infobox/Team') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Center = Widgets.Center local Title = Widgets.Title @@ -76,8 +76,8 @@ function CustomInjector:parse(id, widgets) return param, self.caller:_createProfile(profileData, args[param]) end)) if not Table.isEmpty(profiles) then - table.insert(widgets, Title{name = 'Profiles'}) - table.insert(widgets, Center{content = profiles}) + table.insert(widgets, Title{children = 'Profiles'}) + table.insert(widgets, Center{children = profiles}) end end return widgets diff --git a/components/infobox/wikis/warcraft/infobox_unit_custom.lua b/components/infobox/wikis/warcraft/infobox_unit_custom.lua index b1939957bd0..5933760f5ed 100644 --- a/components/infobox/wikis/warcraft/infobox_unit_custom.lua +++ b/components/infobox/wikis/warcraft/infobox_unit_custom.lua @@ -21,7 +21,7 @@ local Injector = Lua.import('Module:Widget/Injector') local Unit = Lua.import('Module:Infobox/Unit') local Shared = Lua.import('Module:Infobox/Extension/BuildingUnitShared') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Center = Widgets.Center local Title = Widgets.Title @@ -93,8 +93,8 @@ function CustomUnit:addCustomCells(widgets) if args.icon then Array.appendWith(widgets, - Title{name = 'Icon'}, - Center{content = {'[[File:Wc3BTN' .. args.icon .. '.png]]'}} + Title{children = 'Icon'}, + Center{children = {'[[File:Wc3BTN' .. args.icon .. '.png]]'}} ) end diff --git a/components/infobox/wikis/wildrift/infobox_item_custom.lua b/components/infobox/wikis/wildrift/infobox_item_custom.lua index 9cf64fbb754..d60c49f106f 100644 --- a/components/infobox/wikis/wildrift/infobox_item_custom.lua +++ b/components/infobox/wikis/wildrift/infobox_item_custom.lua @@ -18,7 +18,7 @@ local Template = require('Module:Template') local Injector = Lua.import('Module:Widget/Injector') local Item = Lua.import('Module:Infobox/Item') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Breakdown = Widgets.Breakdown local Cell = Widgets.Cell local Center = Widgets.Center @@ -59,7 +59,7 @@ function CustomInjector:parse(id, widgets) if id == 'header' then if String.isNotEmpty(args.itemcost) then table.insert(widgets, Breakdown{ - content = caller:_getCostDisplay(), + children = caller:_getCostDisplay(), classes = { 'infobox-header', 'wiki-backgroundcolor-light', @@ -73,12 +73,12 @@ function CustomInjector:parse(id, widgets) if String.isNotEmpty(args.itemtext) then iconImage = iconImage .. '' .. args.itemtext .. '' end - table.insert(widgets, Center{content = {iconImage}}) + table.insert(widgets, Center{children = {iconImage}}) elseif id == 'attributes' then if not CustomItem._hasAttributes(args) then return {} end if not (String.isEmpty(args.str) and String.isEmpty(args.agi) and String.isEmpty(args.int)) then - table.insert(widgets, Breakdown{classes = {'infobox-center'}, content = { + table.insert(widgets, Breakdown{classes = {'infobox-center'}, children = { caller:_attributeIcons('str'), caller:_attributeIcons('agi'), caller:_attributeIcons('int'), @@ -139,7 +139,7 @@ function CustomInjector:parse(id, widgets) return {} end return { - Title{name = 'Item Tier'}, + Title{children = 'Item Tier'}, Cell{name = 'Category', content = {caller:_categoryDisplay()}}, Cell{name = 'Bought From', content = caller:_shopDisplay()}, Cell{name = 'Dropped From', content = {args.drop}}, @@ -152,7 +152,7 @@ function CustomInjector:parse(id, widgets) ) elseif id == 'recipe' then if String.isEmpty(args.recipe) then return {} end - table.insert(widgets, Center{content = {args.recipe}}) + table.insert(widgets, Center{children = {args.recipe}}) elseif id == 'info' then return {} end diff --git a/components/infobox/wikis/wildrift/infobox_league_custom.lua b/components/infobox/wikis/wildrift/infobox_league_custom.lua index 05c560c58e6..50c8d58ae91 100644 --- a/components/infobox/wikis/wildrift/infobox_league_custom.lua +++ b/components/infobox/wikis/wildrift/infobox_league_custom.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title @@ -45,13 +45,13 @@ function CustomInjector:parse(id, widgets) return {Cell{name = 'Patch', content = {self.caller:_getPatchVersion()}}} elseif id == 'customcontent' then if args.player_number then - table.insert(widgets, Title{name = 'Players'}) + table.insert(widgets, Title{children = 'Players'}) table.insert(widgets, Cell{name = 'Number of players', content = {args.player_number}}) end --teams section if args.team_number or (not String.isEmpty(args.team1)) then - table.insert(widgets, Title{name = 'Teams'}) + table.insert(widgets, Title{children = 'Teams'}) end table.insert(widgets, Cell{name = 'Number of teams', content = {args.team_number}}) end diff --git a/components/infobox/wikis/wildrift/infobox_person_player_custom.lua b/components/infobox/wikis/wildrift/infobox_person_player_custom.lua index dbb992828d9..47622253a54 100644 --- a/components/infobox/wikis/wildrift/infobox_person_player_custom.lua +++ b/components/infobox/wikis/wildrift/infobox_person_player_custom.lua @@ -21,7 +21,7 @@ local Template = require('Module:Template') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local ROLES = { diff --git a/components/infobox/wikis/wildrift/infobox_team_custom.lua b/components/infobox/wikis/wildrift/infobox_team_custom.lua index b9b622fd91b..7d5c93e97f1 100644 --- a/components/infobox/wikis/wildrift/infobox_team_custom.lua +++ b/components/infobox/wikis/wildrift/infobox_team_custom.lua @@ -15,7 +15,7 @@ local Template = require('Module:Template') local Injector = Lua.import('Module:Widget/Injector') local Team = Lua.import('Module:Infobox/Team') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class WildriftInfoboxTeam: InfoboxTeam diff --git a/components/infobox/wikis/wildrift/infobox_unit_champion_custom.lua b/components/infobox/wikis/wildrift/infobox_unit_champion_custom.lua index a3ba831b91b..7c234139f7a 100644 --- a/components/infobox/wikis/wildrift/infobox_unit_champion_custom.lua +++ b/components/infobox/wikis/wildrift/infobox_unit_champion_custom.lua @@ -18,7 +18,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local Unit = Lua.import('Module:Infobox/Unit') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Breakdown = Widgets.Breakdown local Cell = Widgets.Cell local Center = Widgets.Center @@ -58,7 +58,7 @@ function CustomInjector:parse(id, widgets) }, } elseif id == 'caption' then - table.insert(widgets, Center{content = {args.quote}}) + table.insert(widgets, Center{children = {args.quote}}) elseif id == 'type' then local toBreakDownCell = function(key, title, dataModule) if String.isEmpty(args[key]) then return end @@ -70,7 +70,7 @@ function CustomInjector:parse(id, widgets) toBreakDownCell('secondaryrole', 'Secondary Role', 'ClassIcon') ) return { - Breakdown{classes = {'infobox-center'}, content = breakDownContents}, + Breakdown{classes = {'infobox-center'}, children = breakDownContents}, Cell{name = 'Real Name', content = {args.realname}}, } elseif id == 'cost' then @@ -101,7 +101,7 @@ function CustomChampion:getCustomCells(widgets) ) if Array.any({'hp', 'hplvl', 'hpreg', 'hpreglvl'}, function(key) return String.isNotEmpty(args[key]) end) then - table.insert(widgets, Title{name = 'Base Statistics'}) + table.insert(widgets, Title{children = 'Base Statistics'}) end Array.appendWith( @@ -135,7 +135,7 @@ function CustomChampion:getCustomCells(widgets) local winPercentage = Math.round(wins * 100 / (wins + loses), 2) return Array.append(widgets, - Title{name = 'Esports Statistics'}, + Title{children = 'Esports Statistics'}, Cell{name = 'Win Rate', content = {wins .. 'W : ' .. loses .. 'L (' .. winPercentage .. '%)'}} ) end diff --git a/components/infobox/wikis/worldoftanks/infobox_league_custom.lua b/components/infobox/wikis/worldoftanks/infobox_league_custom.lua index 7097cd4c793..2a997c68079 100644 --- a/components/infobox/wikis/worldoftanks/infobox_league_custom.lua +++ b/components/infobox/wikis/worldoftanks/infobox_league_custom.lua @@ -17,7 +17,7 @@ local String = require('Module:StringUtils') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center @@ -62,8 +62,8 @@ function CustomInjector:parse(id, widgets) local maps = Array.map(self.caller:getAllArgsForBase(args, 'map'), function(map) return tostring(self.caller:_createNoWrappingSpan(PageLink.makeInternalLink(map))) end) - table.insert(widgets, Title{name = 'Maps'}) - table.insert(widgets, Center{content = {table.concat(maps, ' • ')}}) + table.insert(widgets, Title{children = 'Maps'}) + table.insert(widgets, Center{children = {table.concat(maps, ' • ')}}) end end return widgets diff --git a/components/infobox/wikis/worldoftanks/infobox_map_custom.lua b/components/infobox/wikis/worldoftanks/infobox_map_custom.lua index 66a46fa2bce..5e2a4df178c 100644 --- a/components/infobox/wikis/worldoftanks/infobox_map_custom.lua +++ b/components/infobox/wikis/worldoftanks/infobox_map_custom.lua @@ -18,7 +18,7 @@ local Injector = Lua.import('Module:Widget/Injector') local Map = Lua.import('Module:Infobox/Map') local Flags = Lua.import('Module:Flags') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class WorldoftanksMapInfobox: MapInfobox diff --git a/components/infobox/wikis/worldoftanks/infobox_person_player_custom.lua b/components/infobox/wikis/worldoftanks/infobox_person_player_custom.lua index 45e0343bbea..b65e5fdc561 100644 --- a/components/infobox/wikis/worldoftanks/infobox_person_player_custom.lua +++ b/components/infobox/wikis/worldoftanks/infobox_person_player_custom.lua @@ -16,7 +16,7 @@ local Variables = require('Module:Variables') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local ROLES = { diff --git a/components/infobox/wikis/worldoftanks/infobox_unit_tank_custom.lua b/components/infobox/wikis/worldoftanks/infobox_unit_tank_custom.lua index c071bae894e..8e2822ca72f 100644 --- a/components/infobox/wikis/worldoftanks/infobox_unit_tank_custom.lua +++ b/components/infobox/wikis/worldoftanks/infobox_unit_tank_custom.lua @@ -18,7 +18,7 @@ local Unit = Lua.import('Module:Infobox/Unit') local Nation = Lua.import('Module:Infobox/Extension/Nation') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class WorldofTanksUnitInfobox: UnitInfobox diff --git a/components/infobox/wikis/worldofwarcraft/infobox_company_custom.lua b/components/infobox/wikis/worldofwarcraft/infobox_company_custom.lua index 1109dc3e719..00c36ff5d01 100644 --- a/components/infobox/wikis/worldofwarcraft/infobox_company_custom.lua +++ b/components/infobox/wikis/worldofwarcraft/infobox_company_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Company = Lua.import('Module:Infobox/Company') local Injector = Lua.import('Module:Widget/Injector') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class WorldofwarcraftCompanyInfobox: CompanyInfobox diff --git a/components/infobox/wikis/worldofwarcraft/infobox_person_player_custom.lua b/components/infobox/wikis/worldofwarcraft/infobox_person_player_custom.lua index c46d20069b7..29c9526ab7d 100644 --- a/components/infobox/wikis/worldofwarcraft/infobox_person_player_custom.lua +++ b/components/infobox/wikis/worldofwarcraft/infobox_person_player_custom.lua @@ -12,7 +12,7 @@ local Lua = require('Module:Lua') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell local ROLES = { diff --git a/components/infobox/wikis/zula/infobox_league_custom.lua b/components/infobox/wikis/zula/infobox_league_custom.lua index ac33815fa79..3c2cd87dcaf 100644 --- a/components/infobox/wikis/zula/infobox_league_custom.lua +++ b/components/infobox/wikis/zula/infobox_league_custom.lua @@ -14,7 +14,7 @@ local Logic = require('Module:Logic') local Injector = Lua.import('Module:Widget/Injector') local League = Lua.import('Module:Infobox/League') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Cell = Widgets.Cell ---@class ZulaLeagueInfobox: InfoboxLeague diff --git a/components/infobox/wikis/zula/infobox_person_player_custom.lua b/components/infobox/wikis/zula/infobox_person_player_custom.lua index c9d07a41d9a..9495ca8e047 100644 --- a/components/infobox/wikis/zula/infobox_person_player_custom.lua +++ b/components/infobox/wikis/zula/infobox_person_player_custom.lua @@ -15,7 +15,7 @@ local TeamHistoryAuto = require('Module:TeamHistoryAuto') local Injector = Lua.import('Module:Widget/Injector') local Player = Lua.import('Module:Infobox/Person') -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local Title = Widgets.Title local Center = Widgets.Center @@ -54,9 +54,9 @@ function CustomInjector:parse(id, widgets) if String.isNotEmpty(manualHistory) or automatedHistory then return { - Title{name = 'History'}, - Center{content = {manualHistory}}, - Center{content = {automatedHistory}}, + Title{children = 'History'}, + Center{children = {manualHistory}}, + Center{children = {automatedHistory}}, } end end diff --git a/components/match2/commons/brkts_wiki_specific_base.lua b/components/match2/commons/brkts_wiki_specific_base.lua index 46f7734615c..22d01d72a7c 100644 --- a/components/match2/commons/brkts_wiki_specific_base.lua +++ b/components/match2/commons/brkts_wiki_specific_base.lua @@ -24,17 +24,6 @@ WikiSpecificBase.processMatch = FnUtil.lazilyDefineFunction(function() or error('Function "processMatch" not implemented on wiki in "Module:MatchGroup/Input/Custom"') end) --- called from Module:Match/Subobjects --- used to transform wiki-specific input of templates to the generalized --- format that is required by Module:MatchGroup --- @parameter map - a map --- @returns the map after changes have been applied -WikiSpecificBase.processMap = FnUtil.lazilyDefineFunction(function() - local InputModule = Lua.import('Module:MatchGroup/Input/Custom') - return InputModule and InputModule.processMap - or error('Function "processMap" not implemented on wiki in "Module:MatchGroup/Input/Custom"') -end) - --[[ Converts a match record to a structurally typed table with the appropriate data types for field values. The match record is either a match created in the store diff --git a/components/match2/commons/match.lua b/components/match2/commons/match.lua index 113cccf575d..d5e9ee43b19 100644 --- a/components/match2/commons/match.lua +++ b/components/match2/commons/match.lua @@ -164,7 +164,7 @@ end ---and removes direct references between a match record and its subobject records. ---@param match table ---@return {matchRecord: table, gameRecords: table[], opponentRecords: table[], playerRecords: table[]} ----@overload fun(match: table): {} +---@overload fun(match: any): {} function Match.splitRecordsByType(match) if match == nil or type(match) ~= 'table' then return {} @@ -385,6 +385,16 @@ end function Match._prepareGameRecordForStore(matchRecord, gameRecord) gameRecord.parent = matchRecord.parent gameRecord.tournament = matchRecord.tournament + if not gameRecord.participants then + gameRecord.participants = {} + for opponentId, opponent in ipairs(gameRecord.opponents or {}) do + for playerId, player in pairs(opponent.players) do + -- Deep copy have to be used here, otherwise a json.stringify complains about circular references + -- between participants and opponents + gameRecord.participants[opponentId .. '_' .. playerId] = Table.deepCopy(player) + end + end + end Match.clampFields(gameRecord, Match.gameFields) end @@ -452,6 +462,7 @@ Match.gameFields = Table.map({ 'parent', 'participants', 'patch', + 'opponents', -- Not a real field yet, but will be in the future. Also for use in Match/Legacy 'resulttype', 'rounds', 'scores', diff --git a/components/match2/commons/match_group_base.lua b/components/match2/commons/match_group_base.lua index 66c31564476..063bd7fce0c 100644 --- a/components/match2/commons/match_group_base.lua +++ b/components/match2/commons/match_group_base.lua @@ -47,6 +47,11 @@ function MatchGroupBase.readOptions(args, matchGroupType) end end + if not Variables.varDefault('tournament_parent') then + table.insert(warnings, 'Missing tournament context. Ensure the page has a InfoboxLeague or a HiddenDataBox.') + mw.ext.TeamLiquidIntegration.add_category('Pages with missing tournament context') + end + if Logic.readBool(args.isLegacy) then if matchGroupType == 'matchlist' then table.insert(warnings, 'This is a legacy matchlist! Please use the new matchlist instead.') diff --git a/components/match2/commons/match_group_display_helper.lua b/components/match2/commons/match_group_display_helper.lua index 7154676c9f5..c82f4aaf903 100644 --- a/components/match2/commons/match_group_display_helper.lua +++ b/components/match2/commons/match_group_display_helper.lua @@ -161,7 +161,9 @@ end ---@param winner integer? ---@return string function DisplayHelper.MapScore(score, opponentIndex, resultType, walkover, winner) - if resultType == 'default' then + if resultType == 'np' then + return '' + elseif resultType == 'default' then return opponentIndex == winner and 'W' or string.upper(walkover or '') end return score and tostring(score) or '' diff --git a/components/match2/commons/match_group_input.lua b/components/match2/commons/match_group_input.lua index ac0656b80bd..f3e5f9476d3 100644 --- a/components/match2/commons/match_group_input.lua +++ b/components/match2/commons/match_group_input.lua @@ -45,7 +45,7 @@ local VALID_GSL_GROUP_STYLES = { ---@param match table function MatchGroupInput._applyTournamentVarsToMaps(match) for _, map in ipairs(MatchGroupUtil.normalizeSubtype(match, 'map')) do - MatchGroupInputUtil.getCommonTournamentVars(map, match) + Table.mergeInto(map, MatchGroupInputUtil.getTournamentContext(map, match)) end end diff --git a/components/match2/commons/match_group_input_util.lua b/components/match2/commons/match_group_input_util.lua index f68dde8b47f..ddb7ee1f6c6 100644 --- a/components/match2/commons/match_group_input_util.lua +++ b/components/match2/commons/match_group_input_util.lua @@ -30,12 +30,25 @@ local globalVars = PageVariableNamespace{cached = true} local MatchGroupInputUtil = {} -local DEFAULT_ALLOWED_VETOES = { - 'decider', - 'pick', - 'ban', - 'defaultban', -} +---@class MGIParsedPlayer +---@field displayname string? +---@field name string? +---@field flag string? +---@field faction string? +---@field index integer +---@field extradata table + +---@class MGIParsedOpponent +---@field type OpponentType +---@field name string +---@field template string? +---@field icon string? +---@field icondark string? +---@field score integer? +---@field status string? +---@field placement integer? +---@field match2players MGIParsedPlayer[] +---@field extradata table local NOT_PLAYED_INPUTS = { 'skip', @@ -44,6 +57,13 @@ local NOT_PLAYED_INPUTS = { 'cancelled', } +MatchGroupInputUtil.DEFAULT_ALLOWED_VETOES = { + 'decider', + 'pick', + 'ban', + 'defaultban', +} + MatchGroupInputUtil.STATUS_INPUTS = { DEFAULT_WIN = 'W', DEFAULT_LOSS = 'L', @@ -73,6 +93,7 @@ local ASSUME_FINISHED_AFTER = { EXACT = 30800, ESTIMATE = 86400, } +MatchGroupInputUtil.ASSUME_FINISHED_AFTER = ASSUME_FINISHED_AFTER local NOW = os.time() local contentLanguage = mw.getContentLanguage() @@ -94,7 +115,6 @@ local contentLanguage = mw.getContentLanguage() ---@field maxNumPlayers integer? ---@field resolveRedirect boolean? ---@field pagifyTeamNames boolean? ----@field pagifyPlayerNames boolean? ---@class MatchGroupInputSubstituteInformation ---@field substitute standardPlayer @@ -177,19 +197,21 @@ end ---Warning, mutates first argument by removing the key `opponentX` where X is the second argument ---@param match table ---@param opponentIndex integer ----@param options readOpponentOptions ----@return table? +---@param options readOpponentOptions? +---@return MGIParsedOpponent? function MatchGroupInputUtil.readOpponent(match, opponentIndex, options) options = options or {} local opponentInput = Json.parseIfString(Table.extract(match, 'opponent' .. opponentIndex)) if not opponentInput then - return opponentIndex <= 2 and Opponent.blank() or nil + return opponentIndex <= 2 and MatchGroupInputUtil.mergeRecordWithOpponent({}, Opponent.blank()) or nil end --- or Opponent.blank() is only needed because readOpponentArg can return nil for team opponents local opponent = Opponent.readOpponentArgs(opponentInput) or Opponent.blank() if Opponent.isBye(opponent) then - return {type = Opponent.literal, name = 'BYE'} + local byeOpponent = Opponent.blank() + byeOpponent.name = 'BYE' + return MatchGroupInputUtil.mergeRecordWithOpponent({}, byeOpponent) end ---@type number|string? @@ -207,8 +229,7 @@ function MatchGroupInputUtil.readOpponent(match, opponentIndex, options) if opponent.type == Opponent.team then local manualPlayersInput = MatchGroupInputUtil.extractManualPlayersInput(match, opponentIndex, opponentInput) substitutions = manualPlayersInput.substitutions - --a variation of `MatchGroupInput.readPlayersOfTeam` that returns a player array - opponent.players = MatchGroupInputUtil.readPlayersOfTeamNew( + opponent.players = MatchGroupInputUtil.readPlayersOfTeam( Opponent.toName(opponent) or '', manualPlayersInput, options, @@ -216,11 +237,9 @@ function MatchGroupInputUtil.readOpponent(match, opponentIndex, options) ) end - if options.pagifyPlayerNames then - Array.forEach(opponent.players or {}, function(player) - player.pageName = Page.pageifyLink(player.pageName) - end) - end + Array.forEach(opponent.players or {}, function(player) + player.pageName = Page.pageifyLink(player.pageName) + end) local record = MatchGroupInputUtil.mergeRecordWithOpponent(opponentInput, opponent, substitutions) @@ -243,7 +262,7 @@ Using the team template extension, the opponent struct is standardised and not u ---@param record table ---@param opponent standardOpponent|StarcraftStandardOpponent|StormgateStandardOpponent|WarcraftStandardOpponent ---@param substitutions MatchGroupInputSubstituteInformation[]? ----@return table +---@return MGIParsedOpponent function MatchGroupInputUtil.mergeRecordWithOpponent(record, opponent, substitutions) if opponent.type == Opponent.team then record.template = opponent.template or record.template @@ -251,8 +270,8 @@ function MatchGroupInputUtil.mergeRecordWithOpponent(record, opponent, substitut record.icondark = opponent.icondark or record.icondark end - if not record.match2players and Logic.isNotEmpty(opponent.players) then - record.match2players = Array.map(opponent.players, function(player) + if not record.match2players then + record.match2players = Array.map(opponent.players or {}, function(player) return { displayname = player.displayName, flag = player.flag, @@ -273,36 +292,38 @@ end ---@param obj table ---@param parent table? ---@return table -function MatchGroupInputUtil.getCommonTournamentVars(obj, parent) +---@nodiscard +function MatchGroupInputUtil.getTournamentContext(obj, parent) parent = parent or {} - obj.game = Logic.emptyOr(obj.game, parent.game, globalVars:get('tournament_game')) - obj.icon = Logic.emptyOr(obj.icon, parent.icon, globalVars:get('tournament_icon')) - obj.icondark = Logic.emptyOr(obj.iconDark, parent.icondark, globalVars:get('tournament_icondark')) - obj.liquipediatier = Logic.emptyOr( + local vars = {} + vars.game = Logic.emptyOr(obj.game, parent.game, globalVars:get('tournament_game')) + vars.icon = Logic.emptyOr(obj.icon, parent.icon, globalVars:get('tournament_icon')) + vars.icondark = Logic.emptyOr(obj.iconDark, parent.icondark, globalVars:get('tournament_icondark')) + vars.liquipediatier = Logic.emptyOr( obj.liquipediatier, parent.liquipediatier, globalVars:get('tournament_liquipediatier') ) - obj.liquipediatiertype = Logic.emptyOr( + vars.liquipediatiertype = Logic.emptyOr( obj.liquipediatiertype, parent.liquipediatiertype, globalVars:get('tournament_liquipediatiertype') ) - obj.publishertier = Logic.emptyOr( + vars.publishertier = Logic.emptyOr( obj.publishertier, parent.publishertier, globalVars:get('tournament_publishertier') ) - obj.series = Logic.emptyOr(obj.series, parent.series, globalVars:get('tournament_series')) - obj.shortname = Logic.emptyOr(obj.shortname, parent.shortname, globalVars:get('tournament_shortname')) - obj.tickername = Logic.emptyOr(obj.tickername, parent.tickername, globalVars:get('tournament_tickername')) - obj.tournament = Logic.emptyOr(obj.tournament, parent.tournament, globalVars:get('tournament_name')) - obj.type = Logic.emptyOr(obj.type, parent.type, globalVars:get('tournament_type')) - obj.patch = Logic.emptyOr(obj.patch, parent.patch, globalVars:get('tournament_patch')) - obj.date = Logic.emptyOr(obj.date, parent.date) - obj.mode = Logic.emptyOr(obj.mode, parent.mode) - - return obj + vars.series = Logic.emptyOr(obj.series, parent.series, globalVars:get('tournament_series')) + vars.shortname = Logic.emptyOr(obj.shortname, parent.shortname, globalVars:get('tournament_shortname')) + vars.tickername = Logic.emptyOr(obj.tickername, parent.tickername, globalVars:get('tournament_tickername')) + vars.tournament = Logic.emptyOr(obj.tournament, parent.tournament, globalVars:get('tournament_name')) + vars.type = Logic.emptyOr(obj.type, parent.type, globalVars:get('tournament_type')) + vars.patch = Logic.emptyOr(obj.patch, parent.patch, globalVars:get('tournament_patch')) + vars.date = Logic.emptyOr(obj.date, parent.date) + vars.mode = Logic.emptyOr(obj.mode, parent.mode) + + return vars end ---@param match table @@ -400,7 +421,7 @@ end ---@param options readOpponentOptions ---@param dateTimeInfo {timestamp: integer?, timezoneOffset: string?} ---@return table -function MatchGroupInputUtil.readPlayersOfTeamNew(teamName, manualPlayersInput, options, dateTimeInfo) +function MatchGroupInputUtil.readPlayersOfTeam(teamName, manualPlayersInput, options, dateTimeInfo) local players = {} local playersIndex = 0 @@ -448,6 +469,7 @@ function MatchGroupInputUtil.readPlayersOfTeamNew(teamName, manualPlayersInput, if not name then teamName = teamName:gsub(' ', '_') varPrefix = teamName .. '_p' .. playerIndex + name = globalVars:get(varPrefix) end while name do @@ -486,138 +508,6 @@ function MatchGroupInputUtil.readPlayersOfTeamNew(teamName, manualPlayersInput, return playersArray end ----reads the players of a team from input and wiki variables ----@deprecated ----@param match table ----@param opponentIndex integer ----@param teamName string ----@param options MatchGroupInputReadPlayersOfTeamOptions? ----@return table -function MatchGroupInputUtil.readPlayersOfTeam(match, opponentIndex, teamName, options) - options = options or {} - - local opponent = match['opponent' .. opponentIndex] - local players = {} - local playersIndex = 0 - - local insertIntoPlayers = function(player) - if type(player) ~= 'table' or Logic.isEmpty(player) or Logic.isEmpty(player.name or player.pageName) then - return - end - - player.name = Logic.emptyOr(player.name, player.pageName) --[[@as string]] - player.name = options.resolveRedirect and mw.ext.TeamLiquidIntegration.resolve_redirect(player.name) or player.name - player.name = options.applyUnderScores and player.name:gsub(' ', '_') or player.name - player.flag = Flags.CountryName(player.flag) - player.displayname = Logic.emptyOr(player.displayname, player.displayName) - playersIndex = playersIndex + 1 - player.index = playersIndex - - players[player.name] = players[player.name] or {} - Table.mergeInto(players[player.name], player) - end - - local playerIndex = 1 - local varPrefix = teamName .. '_p' .. playerIndex - local name = globalVars:get(varPrefix) - while name do - if options.maxNumPlayers and (playersIndex >= options.maxNumPlayers) then break end - - local wasPresentInMatch = function() - if not match.timestamp then return true end - - local joinDate = DateExt.readTimestamp(globalVars:get(varPrefix .. 'joindate') or '') - local leaveDate = DateExt.readTimestamp(globalVars:get(varPrefix .. 'leavedate') or '') - - if (not joinDate) and (not leaveDate) then return true end - - -- need to offset match time to correct timezone as transfers do not have a time associated with them - local timestampLocal = match.timestamp + DateExt.getOffsetSeconds(match.timezoneOffset or '') - - return (not joinDate or (joinDate <= timestampLocal)) and - (not leaveDate or (leaveDate > timestampLocal)) - end - - if wasPresentInMatch() then - insertIntoPlayers{ - pageName = name, - displayName = globalVars:get(varPrefix .. 'dn'), - flag = globalVars:get(varPrefix .. 'flag'), - } - end - playerIndex = playerIndex + 1 - varPrefix = teamName .. '_p' .. playerIndex - name = globalVars:get(varPrefix) - end - - --players from manual input as `opponnetX_pY` - for _, player in Table.iter.pairsByPrefix(match, 'opponent' .. opponentIndex .. '_p') do - local playerTable = Json.parseIfString(player) or {} - insertIntoPlayers(playerTable) - end - - --players from manual input in `opponent.players` - local playersData = Json.parseIfString(opponent.players) or {} - for playerPrefix, playerName in Table.iter.pairsByPrefix(playersData, 'p') do - insertIntoPlayers{ - pageName = playerName, - displayName = playersData[playerPrefix .. 'dn'], - flag = playersData[playerPrefix .. 'flag'], - } - end - - ---@param playerData table|string|nil - ---@return standardPlayer? - local getStandardPlayer = function(playerData) - if not playerData then return end - playerData = type(playerData) == 'string' and {playerData} or playerData - local player = { - displayName = Logic.emptyOr(playerData.displayName, playerData.displayname, playerData[1] or playerData.name), - pageName = Logic.emptyOr(playerData.pageName, playerData.pagename, playerData.link), - flag = playerData.flag, - } - if Logic.isEmpty(player.displayName) then return end - player = PlayerExt.populatePlayer(player) - player.pageName = options.applyUnderScores and player.pageName:gsub(' ', '_') or player.pageName - return player - end - - local substitutions, parseFailure = Json.parseStringified(opponent.substitutes) - if parseFailure then - substitutions = {} - end - - --handle `substitutes` input for opponenets - Array.forEach(substitutions, function(substitution) - if type(substitution) ~= 'table' or not substitution['in'] then return end - local substitute = getStandardPlayer(substitution['in']) - - local subbedGames = substitution['games'] - - local player = getStandardPlayer(substitution['out']) - if player then - players[player.pageName] = subbedGames and players[player.pageName] or nil - end - - opponent.extradata = Table.merge({substitutions = {}}, opponent.extradata or {}) - table.insert(opponent.extradata.substitutions, { - substitute = substitute, - player = player, - games = subbedGames and Array.map(mw.text.split(subbedGames, ';'), String.trim) or nil, - reason = substitution['reason'], - }) - - insertIntoPlayers(substitute) - end) - - opponent.match2players = Array.extractValues(players) - Array.sortInPlaceBy(opponent.match2players, function (player) - return player.index - end) - - return match -end - ---reads the caster input of a match ---@param match table ---@param options {noSort: boolean?}? @@ -686,7 +576,7 @@ end function MatchGroupInputUtil.getMapVeto(match, allowedVetoes) if not match.mapveto then return nil end - allowedVetoes = allowedVetoes or DEFAULT_ALLOWED_VETOES + allowedVetoes = allowedVetoes or MatchGroupInputUtil.DEFAULT_ALLOWED_VETOES match.mapveto = Json.parseIfString(match.mapveto) @@ -743,7 +633,7 @@ end ---@param resultType string? ---@param winnerInput integer|string|nil ----@param opponents {score: number, status: string}[] +---@param opponents {score: number, status: string, placement: integer?}[] ---@return integer? # Winner function MatchGroupInputUtil.getWinner(resultType, winnerInput, opponents) if resultType == MatchGroupInputUtil.RESULT_TYPE.NOT_PLAYED then @@ -754,11 +644,26 @@ function MatchGroupInputUtil.getWinner(resultType, winnerInput, opponents) return MatchGroupInputUtil.WINNER_DRAW elseif resultType == MatchGroupInputUtil.RESULT_TYPE.DEFAULT then return MatchGroupInputUtil.getDefaultWinner(opponents) + elseif MatchGroupInputUtil.findOpponentWithFirstPlace(opponents) then + return MatchGroupInputUtil.findOpponentWithFirstPlace(opponents) else return MatchGroupInputUtil.getHighestScoringOpponent(opponents) end end +---Find the opponent with placement 1 +---If multiple opponents share this placement, the first one is returned +---@param opponents {placement: integer?}[] +---@return integer? +function MatchGroupInputUtil.findOpponentWithFirstPlace(opponents) + local firstPlace = Array.indexOf(opponents, function(opponent) + return opponent.placement == 1 + end) + if firstPlace > 0 then + return firstPlace + end +end + ---Find the opponent with the highest score ---If multiple opponents share the highest score, the first one is returned ---@param opponents {score: number}[] @@ -945,12 +850,12 @@ end -- Special cases: -- If Winner = 0, that means draw, and placementLoser isn't used. Both teams will get placementWinner -- If Winner = -1, that mean no team won, and placementWinner isn't used. Both teams will get placementLoser ----@param opponents table[] +---@param opponents MGIParsedOpponent[] ---@param winner integer? ---@param placementWinner integer ---@param placementLoser integer ---@param resultType string? ----@return table[] +---@return MGIParsedOpponent[] function MatchGroupInputUtil.setPlacement(opponents, winner, placementWinner, placementLoser, resultType) if not opponents or #opponents ~= 2 or resultType == MatchGroupInputUtil.RESULT_TYPE.NOT_PLAYED then return opponents @@ -1089,6 +994,71 @@ function MatchGroupInputUtil.getCharacterName(alias, character) return (assert(alias[character:lower()], 'Invalid character:' .. character)) end +---@param players MGIParsedPlayer[] +---@param playerInput string? +---@param playerLink string? +---@return integer? +function MatchGroupInputUtil.findPlayerId(players, playerInput, playerLink) + if Logic.isEmpty(playerInput) and Logic.isEmpty(playerLink) then + return + end + + local playerLinks = Array.map(players, Operator.property('name')) + local playerIndex = Array.indexOf(playerLinks, FnUtil.curry(Operator.eq, playerLink)) + if playerIndex > 0 then + return playerIndex + end + + local playerDisplayNames = Array.map(players, Operator.property('displayname')) + playerIndex = Array.indexOf(playerDisplayNames, FnUtil.curry(Operator.eq, playerInput)) + if playerIndex > 0 then + return playerIndex + end + mw.log('Player with id ' .. playerInput .. ' not found in opponent data') +end + +---@param name string +---@return string +function MatchGroupInputUtil.makeLinkFromName(name) + return Page.pageifyLink(name) --[[@as string]] +end + +---@alias PlayerInputData {name: string?, link: string?} +---@param playerIds MGIParsedPlayer[] +---@param inputPlayers table[] +---@param indexToPlayer fun(playerIndex: integer): PlayerInputData? +---@param transform fun(playerIndex: integer, playerIdData: MGIParsedPlayer?, playerInputData: PlayerInputData): table? +---@return table, table +function MatchGroupInputUtil.parseParticipants(playerIds, inputPlayers, indexToPlayer, transform) + local participants = {} + local unattachedParticipants = {} + local function parsePlayer(_, playerIndex) + local playerInputData = indexToPlayer(playerIndex) or {} + if playerInputData.name and not playerInputData.link then + playerInputData.link = MatchGroupInputUtil.makeLinkFromName(playerInputData.name) + end + local playerId = MatchGroupInputUtil.findPlayerId(playerIds, playerInputData.name, playerInputData.link) + local toStoreData = transform(playerIndex, playerIds[playerId] or {}, playerInputData) + if playerId then + participants[playerId] = toStoreData + else + table.insert(unattachedParticipants, toStoreData) + end + end + Array.forEach(inputPlayers, parsePlayer) + + return participants, unattachedParticipants +end + +---@generic T:table +---@param opponentIndex integer +---@return fun(playerIndex: integer, data: T): string, T +function MatchGroupInputUtil.prefixPartcipants(opponentIndex) + return function(playerIndex, data) + return opponentIndex .. '_' .. playerIndex, data + end +end + --- Warning, both match and standalone match may be mutated ---@param match table ---@param standaloneMatch table diff --git a/components/match2/commons/match_group_legacy.lua b/components/match2/commons/match_group_legacy.lua index 7aa8863eb61..4b53dde5391 100644 --- a/components/match2/commons/match_group_legacy.lua +++ b/components/match2/commons/match_group_legacy.lua @@ -62,6 +62,7 @@ function MatchGroupLegacy._getMatchMapping(match, match2mapping, lowerHeaders, l local isReset = false if id == THIRD_PLACE_MATCH then round = lastRound + match2mapping[THIRD_PLACE_MATCH .. 'header'] = 'L' .. round.R elseif id == RESET_MATCH then round = lastRound round.G = round.G - 2 diff --git a/components/match2/commons/match_group_util.lua b/components/match2/commons/match_group_util.lua index 527fe00768f..880ee102f79 100644 --- a/components/match2/commons/match_group_util.lua +++ b/components/match2/commons/match_group_util.lua @@ -9,10 +9,12 @@ local Array = require('Module:Array') local Date = require('Module:Date/Ext') local FnUtil = require('Module:FnUtil') +local Info = require('Module:Info') local Json = require('Module:Json') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local StringUtils = require('Module:StringUtils') +local Operator = require('Module:Operator') +local String = require('Module:StringUtils') local Table = require('Module:Table') local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') @@ -23,7 +25,7 @@ local WikiSpecific = Lua.import('Module:Brkts/WikiSpecific') local TBD_DISPLAY = 'TBD' local NOW = os.time() -local nilIfEmpty = StringUtils.nilIfEmpty +local nilIfEmpty = String.nilIfEmpty --[[ Non-display utility functions for brackets, matchlists, matches, opponents, @@ -204,6 +206,7 @@ MatchGroupUtil.types.Walkover = TypeUtil.literalUnion('l', 'ff', 'dq') ---@field map string? ---@field mapDisplayName string? ---@field mode string? +---@field opponents {players: table[]}[] ---@field participants table ---@field resultType ResultType? ---@field scores number[] @@ -428,7 +431,7 @@ function MatchGroupUtil.computeRootMatchIds(bracketDatasById) local rootMatchIds = {} for matchId, bracketData in pairs(bracketDatasById) do if not bracketData.upperMatchId - and not StringUtils.endsWith(matchId, 'RxMBR') then + and not String.endsWith(matchId, 'RxMBR') then table.insert(rootMatchIds, matchId) end end @@ -498,7 +501,8 @@ end ---@return MatchGroupUtilMatch function MatchGroupUtil.matchFromRecord(record) local extradata = MatchGroupUtil.parseOrCopyExtradata(record.extradata) - local opponents = Array.map(record.match2opponents, MatchGroupUtil.opponentFromRecord) + local opponents = Array.map(record.match2opponents, FnUtil.curry(MatchGroupUtil.opponentFromRecord, record)) + local games = Array.map(record.match2games, function(game) return MatchGroupUtil.gameFromRecord(game, #opponents) end) local bracketData = MatchGroupUtil.bracketDataFromRecord(Json.parseIfString(record.match2bracketdata)) if bracketData.type == 'bracket' then bracketData.lowerEdges = bracketData.lowerEdges @@ -516,7 +520,7 @@ function MatchGroupUtil.matchFromRecord(record) dateIsExact = Logic.readBool(record.dateexact), finished = Logic.readBool(record.finished), game = record.game, - games = Array.map(record.match2games, MatchGroupUtil.gameFromRecord), + games = games, links = Json.parseIfString(record.links) or {}, matchId = record.match2id, liquipediatier = record.liquipediatier, @@ -602,10 +606,32 @@ function MatchGroupUtil.bracketDataToRecord(bracketData) } end +---@param matchRecord table ---@param record table +---@param opponentIndex integer ---@return standardOpponent -function MatchGroupUtil.opponentFromRecord(record) +function MatchGroupUtil.opponentFromRecord(matchRecord, record, opponentIndex) local extradata = MatchGroupUtil.parseOrCopyExtradata(record.extradata) + + local score = tonumber(record.score) + local status = record.status + local bestof = tonumber(matchRecord.bestof) + local game1 = (matchRecord.match2games or {})[1] + if bestof == 1 and Info.config.match2.gameScoresIfBo1 and game1 then + local winner = tonumber(game1.winner) + if game1.resulttype == 'default' then + score = -1 + if winner == 0 then + status = 'D' + else + status = winner == opponentIndex and 'W' or string.upper(game1.walkover) + end + elseif game1.scores[opponentIndex] then + score = game1.scores[opponentIndex] + status = 'S' + end + end + return { advanceBg = nilIfEmpty(Table.extract(extradata, 'bg')), advances = Logic.readBoolOrNil(Table.extract(extradata, 'advances')), @@ -614,8 +640,8 @@ function MatchGroupUtil.opponentFromRecord(record) name = nilIfEmpty(record.name), placement = tonumber(record.placement), players = Array.map(record.match2players, MatchGroupUtil.playerFromRecord), - score = tonumber(record.score), - status = record.status, + score = score, + status = status, template = nilIfEmpty(record.template), type = nilIfEmpty(record.type) or 'literal', } @@ -650,12 +676,41 @@ function MatchGroupUtil.playerFromRecord(record) end ---@param record table +---@param opponentCount integer? ---@return MatchGroupUtilGame -function MatchGroupUtil.gameFromRecord(record) +function MatchGroupUtil.gameFromRecord(record, opponentCount) local extradata = MatchGroupUtil.parseOrCopyExtradata(record.extradata) + local participants = Json.parseIfString(record.participants) or {} local walkover = nilIfEmpty(record.walkover) + local function getParticipantsOfOpponent(allParticipants, opponentIndex) + local prefix = opponentIndex .. '_' + local function indexFromKey(key) + if String.startsWith(key, prefix) then + return tonumber(string.sub(key, #prefix + 1)) + else + return nil + end + end + local participantsOfOpponent = Array.extractValues(Table.mapArguments( + allParticipants, + indexFromKey, + function (key, index) + if Logic.isEmpty(allParticipants[key]) then + return nil + end + return Table.merge({playerId = index}, allParticipants[key]) + end, + true + )) + return Array.sortBy(participantsOfOpponent, Operator.property('playerId')) + end + + local opponents = Array.map(Array.range(1, opponentCount or 2), function (_, index) + return {players = getParticipantsOfOpponent(participants, index)} + end) + return { comment = nilIfEmpty(Table.extract(extradata, 'comment')), date = record.date, @@ -666,7 +721,8 @@ function MatchGroupUtil.gameFromRecord(record) map = nilIfEmpty(record.map), mapDisplayName = nilIfEmpty(Table.extract(extradata, 'displayname')), mode = nilIfEmpty(record.mode), - participants = Json.parseIfString(record.participants) or {}, + opponents = opponents, + participants = participants, resultType = nilIfEmpty(record.resulttype), scores = Json.parseIfString(record.scores) or {}, subgroup = tonumber(record.subgroup), diff --git a/components/match2/commons/match_subobjects.lua b/components/match2/commons/match_subobjects.lua index 32510897ad1..b0783b4ca7d 100644 --- a/components/match2/commons/match_subobjects.lua +++ b/components/match2/commons/match_subobjects.lua @@ -12,8 +12,6 @@ local Json = require('Module:Json') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local WikiSpecific = Lua.import('Module:Brkts/WikiSpecific') - local ENTRY_POINT_NAMES = {'getMap'} local MatchSubobjects = {} @@ -31,20 +29,8 @@ function MatchSubobjects.luaGetMap(args) -- dont save map if 'map' is not filled in if Logic.isEmpty(args.map) then return nil - else - args = WikiSpecific.processMap(args) - - args.participants = args.participants or {} - for key, item in pairs(args.participants) do - if not key:match('%d_%d') then - error('Key \'' .. key .. '\' in match2game.participants has invalid format: \'_\' expected') - elseif type(item) ~= 'table' then - error('Item \'' .. tostring(item) .. '\' in match2game.participants has invalid format: table expected') - end - end - - return args end + return args end if FeatureFlag.get('perf') then diff --git a/components/match2/commons/match_summary_base.lua b/components/match2/commons/match_summary_base.lua index 08ba7e7704f..53c67669863 100644 --- a/components/match2/commons/match_summary_base.lua +++ b/components/match2/commons/match_summary_base.lua @@ -6,6 +6,7 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- +local Abbreviation = require('Module:Abbreviation') local Array = require('Module:Array') local Class = require('Module:Class') local Flags = require('Module:Flags') @@ -25,6 +26,17 @@ local OpponentLibraries = require('Module:OpponentLibraries') local Opponent = OpponentLibraries.Opponent local OpponentDisplay = OpponentLibraries.OpponentDisplay +local ARROW_LEFT = '[[File:Arrow sans left.svg|15x15px|link=|Left team starts]]' +local ARROW_RIGHT = '[[File:Arrow sans right.svg|15x15px|link=|Right team starts]]' +local DEFAULT_VETO_TYPE_TO_TEXT = { + ban = 'BAN', + pick = 'PICK', + decider = 'DECIDER', + defaultban = 'DEFAULT BAN', +} +local TBD = Abbreviation.make('TBD', 'To Be Determined') +local VETO_DECIDER = 'decider' + ---just a base class to avoid anno warnings ---@class MatchSummaryRowInterface ---@field create fun(self): Html @@ -473,6 +485,141 @@ function Match:create() return self.root end +-- Map Veto Class +---@class VetoDisplay: MatchSummaryRowInterface +---@operator call: self +---@field root Html +---@field table Html +---@field vetoTypeToText table +local MapVeto = Class.new( + function(self, vetoTypeToText) + self.root = mw.html.create('div') + :addClass('brkts-popup-mapveto') + + self.table = self.root:tag('table') + :addClass('wikitable-striped') + :addClass('collapsible') + :addClass('collapsed') + + self.vetoTypeToText = vetoTypeToText or DEFAULT_VETO_TYPE_TO_TEXT + + self:createHeader() + end +) + +---@return self +function MapVeto:createHeader() + self.table:tag('tr') + :tag('th'):css('width','33%'):done() + :tag('th'):css('width','34%'):wikitext('Map Veto'):done() + :tag('th'):css('width','33%'):done() + return self +end + +---@param firstVeto number? +---@param format string? +---@return self +function MapVeto:vetoStart(firstVeto, format) + format = format and ('Veto format: ' .. format) + local textLeft + local textCenter + local textRight + if firstVeto == 1 then + textLeft = 'Start Map Veto' + textCenter = ARROW_LEFT + textRight = format + elseif firstVeto == 2 then + textLeft = format + textCenter = ARROW_RIGHT + textRight = 'Start Map Veto' + else + return self + end + + self.table:tag('tr'):addClass('brkts-popup-mapveto-vetostart') + :tag('th'):wikitext(textLeft):done() + :tag('th'):wikitext(textCenter):done() + :tag('th'):wikitext(textRight):done() + + return self +end + +---@param map string? +---@return self +function MapVeto:addDecider(map) + local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') + + self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', self.vetoTypeToText.decider) + self:addColumnVetoMap(row, self:displayMap(map)) + self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', self.vetoTypeToText.decider) + + self.table:node(row) + return self +end + +---@param vetoType string? +---@param map1 string? +---@param map2 string? +---@return self +function MapVeto:addRound(vetoType, map1, map2) + map1, map2 = self:displayMaps(map1, map2) + + local vetoText = self.vetoTypeToText[vetoType] + + if not vetoText then return self end + + local class = 'brkts-popup-mapveto-' .. vetoType + + local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') + + self:addColumnVetoMap(row, map1) + self:addColumnVetoType(row, class, vetoText) + self:addColumnVetoMap(row, map2) + + self.table:node(row) + return self +end + +---@param map1 string? +---@param map2 string? +---@return string +---@return string +function MapVeto:displayMaps(map1, map2) + return self:displayMap(map1), self:displayMap(map2) +end + +---@param map string? +---@return string +function MapVeto:displayMap(map) + return Page.makeInternalLink(map) or TBD +end + +---@param row Html +---@param styleClass string +---@param vetoText string +---@return self +function MapVeto:addColumnVetoType(row, styleClass, vetoText) + row:tag('td') + :tag('span') + :addClass(styleClass) + :addClass('brkts-popup-mapveto-vetotype') + :wikitext(vetoText) + return self +end + +---@param row Html +---@param map string +---@return self +function MapVeto:addColumnVetoMap(row, map) + row:tag('td'):wikitext(map) + return self +end + +---@return Html +function MapVeto:create() + return self.root +end + ---@class MatchSummary ---@operator call(string?):MatchSummary ---@field Header MatchSummaryHeader @@ -484,6 +631,8 @@ end ---@field Mvp MatchSummaryMvp ---@field Casters MatchSummaryCasters ---@field Match MatchSummaryMatch +---@field MapVeto VetoDisplay +---@field DEFAULT_VETO_TYPE_TO_TEXT table ---@field matches Html[]? ---@field headerElement Html? ---@field root Html? @@ -497,6 +646,8 @@ MatchSummary.Break = Break MatchSummary.Mvp = Mvp MatchSummary.Casters = Casters MatchSummary.Match = Match +MatchSummary.MapVeto = MapVeto +MatchSummary.DEFAULT_VETO_TYPE_TO_TEXT = DEFAULT_VETO_TYPE_TO_TEXT ---@param width string? ---@return MatchSummary @@ -717,4 +868,28 @@ function MatchSummary.makeCastersRow(castersInput) return casterRow end +---@param match MatchGroupUtilMatch +---@param mapVeto VetoDisplay? +---@return VetoDisplay? +function MatchSummary.defaultMapVetoDisplay(match, mapVeto) + local vetoData = match.extradata.mapveto + if Logic.isEmpty(vetoData) then + return + end + + mapVeto = mapVeto or MapVeto() + Array.forEach(vetoData, function(vetoRound) + if vetoRound.vetostart then + mapVeto:vetoStart(tonumber(vetoRound.vetostart), vetoRound.format) + end + if vetoRound.type == VETO_DECIDER then + mapVeto:addDecider(vetoRound.decider) + else + mapVeto:addRound(vetoRound.type, vetoRound.team1, vetoRound.team2) + end + end) + + return mapVeto +end + return MatchSummary diff --git a/components/match2/commons/starcraft_starcraft2/match_external_links_starcraft.lua b/components/match2/commons/starcraft_starcraft2/match_external_links_starcraft.lua deleted file mode 100644 index bbec1660a6e..00000000000 --- a/components/match2/commons/starcraft_starcraft2/match_external_links_starcraft.lua +++ /dev/null @@ -1,136 +0,0 @@ ---- --- @Liquipedia --- wiki=commons --- page=Module:MatchExternalLinks/Starcraft --- --- Please see https://github.com/Liquipedia/Lua-Modules to contribute --- - -local Arguments = require('Module:Arguments') -local Array = require('Module:Array') - ---[[ -Utility module for working with starcraft and starcraft2 specific external -media links of matches. This includes the preview, lrthread, vodN, interviewN, -recap, and review params of both matches and games. -]] - -local StarcraftMatchExternalLinks = {} - --- List of supported external link parameters, in display order ----@enum StarcraftMatchExternalLinkTypes -local PARAM_TYPES = { - 'preview', - 'lrthread', - 'vod', - 'vodgame', - 'interview', - 'recap', - 'review', -} -StarcraftMatchExternalLinks.paramTypes = PARAM_TYPES - -StarcraftMatchExternalLinks.paramTypeIndexes = {} -for index, paramType in ipairs(StarcraftMatchExternalLinks.paramTypes) do - StarcraftMatchExternalLinks.paramTypeIndexes[paramType] = index -end - ----Renders a single external link. ----@param props {number: number?, type: StarcraftMatchExternalLinkTypes, url: string} ----@return string -function StarcraftMatchExternalLinks.ExternalLink(props) - if props.type == 'preview' then - return '[[File:Preview_Icon32.png|link=' .. props.url .. '|alt=preview|16px|Preview]] ' - end - - if props.type == 'lrthread' then - return '[[File:LiveReport32.png|link=' .. props.url .. '|alt=lrthread|16px|Live Report Thread]] ' - end - - if props.type == 'vod' then - if props.number and 1 <= props.number and props.number <= 9 then - return '[[File:VOD Icon' .. props.number .. '.png|16px|link=' .. props.url .. '|Watch Game ' .. props.number .. ']] ' - else - return '[[File:VOD Icon.png|link=' .. props.url .. '|16px|Watch VOD]] ' - end - end - - if props.type == 'interview' then - return '[[File:Interview32.png|link=' .. props.url .. '|16px|Interview]] ' - end - - if props.type == 'recap' then - return '[[File:Reviews32.png|link=' .. props.url .. '|16px|Recap]] ' - end - if props.type == 'review' then - return '[[File:Reviews32.png|link=' .. props.url .. '|16px|Review]] ' - end - - error('Unsupported prop type ' .. tostring(props.type)) -end - ----Extracts match media link relevant args from a generic args array, and returns an array of link propss that can be ----rendered via StarcraftMatchExternalLinks.MatchExternalLinks. ----@param args table ----@return {number: number?, type: StarcraftMatchExternalLinkTypes, url: string}[] -function StarcraftMatchExternalLinks.extractFromArgs(args) - local links = {} - for paramName, url in pairs(args) do - local type, number = tostring(paramName):match('^(%a+)(%d*)$') - if StarcraftMatchExternalLinks.paramTypeIndexes[type] and url ~= '' then - table.insert(links, { - type = type, - number = tonumber(number), - url = url, - }) - end - end - - return links -end - ----@param match table ----@return {number: number?, type: StarcraftMatchExternalLinkTypes, url: string}[] -function StarcraftMatchExternalLinks.extractFromMatch(match) - local links = StarcraftMatchExternalLinks.extractFromArgs(match.links) - - if match.vod then - table.insert(links, {type = 'vod', url = match.vod}) - end - for gameIx, game in ipairs(match.games) do - if game.vod then - table.insert(links, {type = 'vod', number = gameIx, url = game.vod}) - end - end - - return links -end - ----@param props {links: {number: number?, type: StarcraftMatchExternalLinkTypes, url: string}[]} ----@return Html -function StarcraftMatchExternalLinks.MatchExternalLinks(props) - local links = Array.sortBy(props.links, function(link) - return { - StarcraftMatchExternalLinks.paramTypeIndexes[link.type], - -- Unnumbered vods appear before numbered ones - link.number or -1, - } - end) - - local list = mw.html.create('span'):addClass('starcraft-match-external-links') - for _, link in ipairs(links) do - list:node(StarcraftMatchExternalLinks.ExternalLink(link)) - end - return list -end - --- Entry point of Template:MatchExternalLink ----@param frame Frame ----@return Html -function StarcraftMatchExternalLinks.TemplateMatchExternalLink(frame) - local args = Arguments.getArgs(frame) - local links = StarcraftMatchExternalLinks.extractFromArgs(args) - return StarcraftMatchExternalLinks.MatchExternalLinks({links = links}) -end - -return StarcraftMatchExternalLinks diff --git a/components/match2/commons/starcraft_starcraft2/match_group_input_starcraft.lua b/components/match2/commons/starcraft_starcraft2/match_group_input_starcraft.lua index 8530f8c6781..9b66d1b7a61 100644 --- a/components/match2/commons/starcraft_starcraft2/match_group_input_starcraft.lua +++ b/components/match2/commons/starcraft_starcraft2/match_group_input_starcraft.lua @@ -12,12 +12,10 @@ local Flags = require('Module:Flags') local Logic = require('Module:Logic') local Lua = require('Module:Lua') local Operator = require('Module:Operator') -local Page = require('Module:Page') local String = require('Module:StringUtils') local Table = require('Module:Table') local Variables = require('Module:Variables') -local DeprecatedCustomMatchGroupInput = Lua.import('Module:MatchGroup/Input/Starcraft/deprecated') local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') local OpponentLibraries = require('Module:OpponentLibraries') local Opponent = OpponentLibraries.Opponent @@ -26,7 +24,6 @@ local Streams = Lua.import('Module:Links/Stream') local OPPONENT_CONFIG = { resolveRedirect = true, pagifyTeamNames = true, - pagifyPlayerNames = true, } local TBD = 'TBD' local TBA = 'TBA' @@ -35,38 +32,23 @@ local MODE_MIXED = 'mixed' local StarcraftMatchGroupInput = {} local MatchFunctions = {} local MapFunctions = {} +-- make these available for ffa +StarcraftMatchGroupInput.MatchFunctions = MatchFunctions +StarcraftMatchGroupInput.MapFunctions = MapFunctions ---@param match table ---@param options table? ---@return table function StarcraftMatchGroupInput.processMatch(match, options) if Logic.readBool(match.ffa) then - return DeprecatedCustomMatchGroupInput.processMatch(match, options) + -- have to import here to avoid import loops + local FfaStarcraftMatchGroupInput = Lua.import('Module:MatchGroup/Input/Starcraft/Ffa') + return FfaStarcraftMatchGroupInput.processMatch(match, options) end Table.mergeInto(match, MatchFunctions.readDate(match.date)) - local opponents = Array.mapIndexes(function(opponentIndex) - return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) - end) - - -- TODO: check how we can get rid of this legacy stuff ... - Array.forEach(opponents, function(opponent, opponentIndex) - local opponentHasWon = Table.extract(opponent, 'win') - if not Logic.readBool(opponentHasWon) then return end - match.winner = match.winner or opponentIndex - end) - - Array.forEach(opponents, function(opponent) - opponent.extradata = opponent.extradata or {} - Table.mergeInto(opponent.extradata, MatchFunctions.getOpponentExtradata(opponent)) - -- make sure match2players is not nil to avoid indexing nil - opponent.match2players = opponent.match2players or {} - Array.forEach(opponent.match2players, function(player) - player.extradata = player.extradata or {} - player.extradata.faction = MatchFunctions.getPlayerFaction(player) - end) - end) + local opponents = MatchFunctions.readOpponents(match) local games = MatchFunctions.extractMaps(match, opponents) @@ -102,7 +84,7 @@ function StarcraftMatchGroupInput.processMatch(match, options) MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - MatchGroupInputUtil.getCommonTournamentVars(match) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) match.stream = Streams.processStreams(match) match.vod = Logic.nilIfEmpty(match.vod) @@ -116,16 +98,37 @@ function StarcraftMatchGroupInput.processMatch(match, options) return match end +---@param match table +---@return table[] +function MatchFunctions.readOpponents(match) + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) + + Array.forEach(opponents, function(opponent) + opponent.extradata = opponent.extradata or {} + Table.mergeInto(opponent.extradata, MatchFunctions.getOpponentExtradata(opponent)) + -- make sure match2players is not nil to avoid indexing nil + opponent.match2players = opponent.match2players or {} + Array.forEach(opponent.match2players, function(player) + player.extradata = player.extradata or {} + player.extradata.faction = MatchFunctions.getPlayerFaction(player) + end) + end) + + return opponents +end + ---@param dateInput string? ---@return {date: string, dateexact: boolean, timestamp: integer, timezoneId: string?, timezoneOffset: string?} function MatchFunctions.readDate(dateInput) local dateProps = MatchGroupInputUtil.readDate(dateInput, { - 'matchDate', + 'match_date', 'tournament_startdate', 'tournament_enddate', }) if dateProps.dateexact then - Variables.varDefine('matchDate', dateProps.date) + Variables.varDefine('match_date', dateProps.date) end return dateProps end @@ -181,7 +184,7 @@ end ---@param player table ---@return string function MatchFunctions.getPlayerFaction(player) - return player.extradata.faction or Faction.defaultFaction + return Faction.read(player.extradata.faction) or Faction.defaultFaction end ---@param opponents {type: OpponentType} @@ -371,73 +374,52 @@ end ---@param opponentIndex integer ---@return table function MapFunctions.getTeamParticipants(mapInput, opponent, opponentIndex) - local players = opponent.match2players - local archonFaction = Faction.read(mapInput['t' .. opponentIndex .. 'p1race']) or Faction.read(mapInput['opponent' .. opponentIndex .. 'race']) - or ((players[1] or {}).extradata or {}).faction + or ((opponent.match2players[1] or {}).extradata or {}).faction local isArchon = MapFunctions.isArchon(mapInput, opponent, opponentIndex) - ---@type {input: string, faction: string?, link: string?}[] - local participantsList = Array.mapIndexes(function(playerIndex) - local prefix = 't' .. opponentIndex .. 'p' .. playerIndex - - if Logic.isEmpty(mapInput[prefix]) then return end - - return { - input = mapInput[prefix], - link = Logic.nilIfEmpty(mapInput[prefix .. 'link']), - faction = isArchon and archonFaction or Faction.read(mapInput[prefix .. 'race']), - } + local players = Array.mapIndexes(function(playerIndex) + return Logic.nilIfEmpty(mapInput['t' .. opponentIndex .. 'p' .. playerIndex]) end) - local participants = {} - - Array.forEach(participantsList, function(participantInput, position) - local nameInput = participantInput.input - - local isTBD = nameInput:upper() == TBD or nameInput:upper() == TBA - - local link = participantInput.link or Variables.varDefault(nameInput .. '_page') or nameInput - link = Page.pageifyLink(link) --[[@as string -- can't be nil as input isn't nil]] - - local playerIndex = MapFunctions.getPlayerIndex(players, link, nameInput) - - -- in case we have a TBD or a player not known in match2players inster a new player in match2players - if isTBD or playerIndex == 0 then - table.insert(players, { - name = isTBD and TBD or link, - displayname = isTBD and TBD or nameInput, - extradata = {faction = participantInput.faction or Faction.defaultFaction}, - }) - playerIndex = #players + local participants, unattachedParticipants = MatchGroupInputUtil.parseParticipants( + opponent.match2players, + players, + function(playerIndex) + local prefix = 't' .. opponentIndex .. 'p' .. playerIndex + return { + name = mapInput[prefix], + link = Logic.nilIfEmpty(mapInput[prefix .. 'link']) or Variables.varDefault(mapInput[prefix] .. '_page'), + } + end, + function(playerIndex, playerIdData, playerInputData) + local factionKey = 't' .. opponentIndex .. 'p' .. playerIndex .. 'race' + local faction = isArchon and archonFaction or Faction.read(mapInput[factionKey]) + return { + faction = faction or (playerIdData.extradata or {}).faction or Faction.defaultFaction, + player = playerIdData.name or playerInputData.link or playerInputData.name:gsub(' ', '_'), + flag = Flags.CountryName(playerIdData.flag), + position = playerIndex, + } end + ) - local player = players[playerIndex] - - participants[opponentIndex .. '_' .. playerIndex] = { - faction = participantInput.faction or player.extradata.faction, - player = link, - position = position, - flag = Flags.CountryName(player.flag), - } + Array.forEach(unattachedParticipants, function(participant) + local name = mapInput['t' .. opponentIndex .. 'p' .. participant.position] + local nameUpper = name:upper() + local isTBD = nameUpper == TBD or nameUpper == TBA + + table.insert(opponent.match2players, { + name = isTBD and TBD or participant.player, + displayname = isTBD and TBD or name, + flag = participant.flag, + extradata = {faction = participant.faction}, + }) + participants[#opponent.match2players] = participant end) - return participants -end - ----@param players {name: string, displayname: string} ----@param name string ----@param displayName string ----@return integer -function MapFunctions.getPlayerIndex(players, name, displayName) - local playerIndex = Array.indexOf(players, function(player) return player.name == name end) - - if playerIndex ~= 0 then - return playerIndex - end - - return Array.indexOf(players, function(player) return player.displayname == displayName end) + return Table.map(participants, MatchGroupInputUtil.prefixPartcipants(opponentIndex)) end ---@param mapInput table diff --git a/components/match2/commons/starcraft_starcraft2/match_group_input_starcraft_deprecated.lua b/components/match2/commons/starcraft_starcraft2/match_group_input_starcraft_deprecated.lua deleted file mode 100644 index 18c2fe1add2..00000000000 --- a/components/match2/commons/starcraft_starcraft2/match_group_input_starcraft_deprecated.lua +++ /dev/null @@ -1,1064 +0,0 @@ ---- --- @Liquipedia --- wiki=commons --- page=Module:MatchGroup/Input/Starcraft/deprecated --- --- Please see https://github.com/Liquipedia/Lua-Modules to contribute --- - ----@deprecated ----only used for the ffa part until that gets cleaned up too - - -local Array = require('Module:Array') -local Faction = require('Module:Faction') -local Flags = require('Module:Flags') -local FnUtil = require('Module:FnUtil') -local Json = require('Module:Json') -local Logic = require('Module:Logic') -local Lua = require('Module:Lua') -local String = require('Module:StringUtils') -local Table = require('Module:Table') -local Variables = require('Module:Variables') - -local config = Lua.requireIfExists('Module:Match/Config', {loadData = true}) or {} -local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') -local Streams = Lua.import('Module:Links/Stream') - -local MAX_NUM_MAPS = config.MAX_NUM_MAPS or 20 -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L'} -local CONVERT_STATUS_INPUT = {W = 'W', FF = 'FF', L = 'L', DQ = 'DQ', ['-'] = 'L'} -local DEFAULT_LOSS_STATUSES = {'FF', 'L', 'DQ'} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_PLAYERS = 30 -local MAX_NUM_VODGAMES = 9 -local DEFAULT_BEST_OF = 99 -local OPPONENT_MODE_TO_PARTIAL_MATCH_MODE = { - solo = '1', - duo = '2', - trio = '3', - quad = '4', - team = 'team', - literal = 'literal', -} -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) -local TBD = 'tbd' -local TBA = 'tba' - -local getStarcraftFfaInputModule = FnUtil.memoize(function() - return Lua.import('Module:MatchGroup/Input/Starcraft/Ffa') -end) - ----Module for converting input args of match group objects into LPDB records. ----This module is specific to the Starcraft and Starcraft2 wikis. -local StarcraftMatchGroupInput = {} - --- called from Module:MatchGroup ----@param match table ----@param options table? ----@return table -function StarcraftMatchGroupInput.processMatch(match, options) - Table.mergeInto( - match, - StarcraftMatchGroupInput._readDate(match) - ) - match = StarcraftMatchGroupInput._getTournamentVars(match) - if Logic.readBool(match.ffa) then - match = getStarcraftFfaInputModule().adjustData(match) - else - match = StarcraftMatchGroupInput._adjustData(match) - end - match = StarcraftMatchGroupInput._checkFinished(match) - match = StarcraftMatchGroupInput._getVodStuff(match) - match = StarcraftMatchGroupInput._getLinks(match) - match = StarcraftMatchGroupInput._getExtraData(match) - - return match -end - ----@param matchArgs table ----@return {date: string, dateexact: boolean, timestamp: integer, timezoneId: string?, timezoneOffset: string?} -function StarcraftMatchGroupInput._readDate(matchArgs) - local dateProps = MatchGroupInputUtil.readDate(matchArgs.date, { - 'matchDate', - 'tournament_startdate', - 'tournament_enddate', - }) - if dateProps.dateexact then - Variables.varDefine('matchDate', dateProps.date) - end - return dateProps -end - ----@param match table ----@return table -function StarcraftMatchGroupInput._checkFinished(match) - if Logic.readBoolOrNil(match.finished) == false then - match.finished = false - return match - elseif Logic.readBool(match.finished) then - match.finished = true - elseif Logic.isNotEmpty(match.winner) then - match.finished = true - end - - -- Match is automatically marked finished upon page edit after a - -- certain amount of time (depending on whether the date is exact) - if match.finished ~= true then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - return match -end - ----@param match table ----@return table -function StarcraftMatchGroupInput._getTournamentVars(match) - match.cancelled = Logic.emptyOr(match.cancelled, Variables.varDefault('cancelled tournament', 'false')) - match.headtohead = Logic.emptyOr(match.headtohead, Variables.varDefault('headtohead')) - Variables.varDefine('headtohead', match.headtohead) - match.publishertier = Logic.emptyOr(match.featured, Variables.varDefault('tournament_publishertier')) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof')) - Variables.varDefine('bestof', match.bestof) - - return MatchGroupInputUtil.getCommonTournamentVars(match) -end - ----@param match table ----@return table -function StarcraftMatchGroupInput._getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod) - - return match -end - ----@param match table ----@return table -function StarcraftMatchGroupInput._getLinks(match) - match.links = { - preview = match.preview, - preview2 = match.preview2, - interview = match.interview, - interview2 = match.interview2, - review = match.review, - recap = match.recap, - lrthread = match.lrthread, - } - return match -end - ----@param match table ----@return table -function StarcraftMatchGroupInput._getExtraData(match) - local extradata - if Logic.readBool(match.ffa) then - match.extradata = getStarcraftFfaInputModule().getExtraData(match) - return match - end - - extradata = { - casters = MatchGroupInputUtil.readCasters(match, {noSort = true}), - headtohead = match.headtohead, - ffa = 'false', - } - - for prefix, vetoMap, vetoIndex in Table.iter.pairsByPrefix(match, 'veto') do - StarcraftMatchGroupInput._getVeto(extradata, vetoMap, match, prefix, vetoIndex) - end - - for subGroupIndex = 1, MAX_NUM_MAPS do - extradata['subGroup' .. subGroupIndex .. 'header'] - = StarcraftMatchGroupInput._getSubGroupHeader(subGroupIndex, match) - end - - match.extradata = extradata - - return match -end - ----@param extradata table ----@param map string ----@param match table ----@param prefix string ----@param vetoIndex integer -function StarcraftMatchGroupInput._getVeto(extradata, map, match, prefix, vetoIndex) - extradata[prefix] = map and mw.ext.TeamLiquidIntegration.resolve_redirect(map) or nil - extradata[prefix .. 'by'] = match['vetoplayer' .. vetoIndex] or match['vetoopponent' .. vetoIndex] - extradata[prefix .. 'displayname'] = match[prefix .. 'displayName'] -end - ----@param subGroupIndex integer ----@param match table ----@return string? -function StarcraftMatchGroupInput._getSubGroupHeader(subGroupIndex, match) - local header = Logic.emptyOr( - match['subGroup' .. subGroupIndex .. 'header'], - match['subgroup' .. subGroupIndex .. 'header'], - match['submatch' .. subGroupIndex .. 'header'] - ) - - return String.nilIfEmpty(header) -end - ----@param match table ----@return table -function StarcraftMatchGroupInput._adjustData(match) - --parse opponents + set base sumscores + determine match mode - match.mode = '' - match = StarcraftMatchGroupInput._opponentInput(match) - - --main processing done here - local subGroupIndex = 0 - for _, _, mapIndex in Table.iter.pairsByPrefix(match, 'map') do - match, subGroupIndex = StarcraftMatchGroupInput._mapInput(match, mapIndex, subGroupIndex) - end - - --apply vodgames - for index = 1, MAX_NUM_VODGAMES do - local vodgame = match['vodgame' .. index] - if Logic.isNotEmpty(vodgame) and Logic.isNotEmpty(match['map' .. index]) then - match['map' .. index].vod = match['map' .. index].vod or vodgame - end - end - - match = StarcraftMatchGroupInput._matchWinnerProcessing(match) - - return match -end - ----@param match table ----@return table -function StarcraftMatchGroupInput._matchWinnerProcessing(match) - local bestof = tonumber(match.bestof) or DEFAULT_BEST_OF - local walkover = match.walkover or '' - local numberofOpponents = 0 - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local opponent = match['opponent' .. opponentIndex] - if Logic.isNotEmpty(opponent) then - numberofOpponents = numberofOpponents + 1 - --determine opponent scores, status and placement - --determine MATCH winner, resulttype and walkover - --the following ignores the possibility of > 2 opponents - --as > 2 opponents is only possible in ffa - if Logic.isNotEmpty(walkover) then - if Logic.isNumeric(walkover) then - local numericWalkover = tonumber(walkover) - if numericWalkover == opponentIndex then - match.winner = opponentIndex - match.walkover = 'L' - opponent.status = 'W' - elseif numericWalkover == 0 then - match.winner = 0 - match.walkover = 'L' - opponent.status = 'L' - else - local score = string.upper(opponent.score or '') - opponent.status = CONVERT_STATUS_INPUT[score] or 'L' - end - elseif Table.includes(ALLOWED_STATUSES, string.upper(walkover)) then - if tonumber(match.winner or 0) == opponentIndex then - opponent.status = 'W' - else - opponent.status = CONVERT_STATUS_INPUT[string.upper(walkover)] or 'L' - end - else - local score = string.upper(opponent.score or '') - opponent.status = CONVERT_STATUS_INPUT[score] or 'L' - match.walkover = 'L' - end - opponent.score = -1 - match.finished = true - match.resulttype = 'default' - elseif CONVERT_STATUS_INPUT[string.upper(opponent.score or '')] then - if string.upper(opponent.score) == 'W' then - match.winner = opponentIndex - match.resulttype = 'default' - match.finished = true - opponent.score = -1 - opponent.status = 'W' - else - match.resulttype = 'default' - match.finished = true - match.walkover = CONVERT_STATUS_INPUT[string.upper(opponent.score)] - local score = string.upper(opponent.score) - opponent.status = CONVERT_STATUS_INPUT[score] - opponent.score = -1 - end - else - opponent.status = 'S' - opponent.score = tonumber(opponent.score) or - tonumber(opponent.sumscore) or -1 - if opponent.score > bestof / 2 then - match.finished = Logic.emptyOr(match.finished, true) - match.winner = tonumber(match.winner or '') or opponentIndex - end - end - - if Logic.readBool(match.cancelled) then - match.finished = true - if String.isEmpty(match.resulttype) and Logic.isEmpty(opponent.score) then - match.resulttype = 'np' - opponent.score = opponent.score or -1 - end - end - else - break - end - end - - StarcraftMatchGroupInput._determineWinnerIfMissing(match) - - for opponentIndex = 1, numberofOpponents do - local opponent = match['opponent' .. opponentIndex] - if match.winner == 'draw' or tonumber(match.winner) == 0 or - (match.opponent1.score == bestof / 2 and match.opponent1.score == match.opponent2.score) then - match.finished = true - match.winner = 0 - match.resulttype = 'draw' - end - - if tonumber(match.winner) == opponentIndex or - match.resulttype == 'draw' then - opponent.placement = 1 - elseif Logic.isNumeric(match.winner) then - opponent.placement = 2 - end - end - - return match -end - ----@param match table ----@return table -function StarcraftMatchGroupInput._determineWinnerIfMissing(match) - if Logic.readBool(match.finished) and Logic.isEmpty(match.winner) then - local scores = Array.mapIndexes(function(opponentIndex) - local opponent = match['opponent' .. opponentIndex] - if not opponent then - return nil - end - return match['opponent' .. opponentIndex].score or -1 end - ) - local maxScore = math.max(unpack(scores) or 0) - -- if we have a positive score and the match is finished we also have a winner - if maxScore > 0 then - local maxIndexFound = false - for opponentIndex, score in pairs(scores) do - if maxIndexFound and score == maxScore then - match.winner = 0 - break - elseif score == maxScore then - maxIndexFound = true - match.winner = opponentIndex - end - end - end - end - - return match -end - ---OpponentInput functions - ----@param match table ----@return table -function StarcraftMatchGroupInput._opponentInput(match) - local opponentIndex = 1 - local opponent = match['opponent' .. opponentIndex] - - while opponentIndex <= MAX_NUM_OPPONENTS and Logic.isNotEmpty(opponent) do - opponent = Json.parseIfString(opponent) - - -- Convert byes to literals - if - string.lower(opponent.template or '') == 'bye' - or string.lower(opponent.name or '') == 'bye' - then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - -- Fix legacy winner - if Logic.isNotEmpty(opponent.win) then - if Logic.isEmpty(match.winner) then - match.winner = tostring(opponentIndex) - else - match.winner = '0' - end - opponent.win = nil - end - - -- Opponent processing (first part) - -- Sort out extradata - opponent.extradata = { - advantage = opponent.advantage, - penalty = opponent.penalty, - score2 = opponent.score2, - isarchon = opponent.isarchon - } - - --process input depending on type - if opponent.type == Opponent.solo then - opponent = - StarcraftMatchGroupInput.ProcessSoloOpponentInput(opponent) - elseif opponent.type == Opponent.duo then - opponent = StarcraftMatchGroupInput.ProcessDuoOpponentInput(opponent) - elseif opponent.type == Opponent.trio then - opponent = StarcraftMatchGroupInput.ProcessOpponentInput(opponent, 3) - elseif opponent.type == Opponent.quad then - opponent = StarcraftMatchGroupInput.ProcessOpponentInput(opponent, 4) - elseif opponent.type == Opponent.team then - opponent = StarcraftMatchGroupInput.ProcessTeamOpponentInput(opponent, match.date) - elseif opponent.type == Opponent.literal then - opponent = StarcraftMatchGroupInput.ProcessLiteralOpponentInput(opponent) - else - error('Unsupported Opponent Type') - end - - --set initial opponent sumscore - opponent.sumscore = - tonumber(opponent.extradata.advantage) or tonumber('-' .. (opponent.extradata.penalty or '')) - - local mode = OPPONENT_MODE_TO_PARTIAL_MATCH_MODE[opponent.type] - if mode == '2' and Logic.readBool(opponent.extradata.isarchon) then - mode = 'Archon' - end - - match.mode = match.mode .. (opponentIndex ~= 1 and '_' or '') .. mode - - match['opponent' .. opponentIndex] = opponent - - opponentIndex = opponentIndex + 1 - opponent = match['opponent' .. opponentIndex] - end - - return match -end - ----@param opponent table ----@return table -function StarcraftMatchGroupInput.ProcessSoloOpponentInput(opponent) - local name = Logic.emptyOr( - opponent.name, - opponent.p1, - opponent[1] - ) or '' - local link = Logic.emptyOr(opponent.link, Variables.varDefault(name .. '_page')) or name - link = mw.ext.TeamLiquidIntegration.resolve_redirect(link):gsub(' ', '_') - local faction = Logic.emptyOr(opponent.race, Variables.varDefault(name .. '_faction')) - local players = {} - local flag = Logic.emptyOr(opponent.flag, Variables.varDefault(name .. '_flag')) - players[1] = { - displayname = name, - name = link, - flag = Flags.CountryName(flag), - extradata = {faction = Faction.read(faction) or Faction.defaultFaction} - } - - return { - type = opponent.type, - name = link, - score = opponent.score, - extradata = opponent.extradata, - match2players = players - } -end - ----@param opponent table ----@return table -function StarcraftMatchGroupInput.ProcessDuoOpponentInput(opponent) - opponent.p1 = opponent.p1 or '' - opponent.p2 = opponent.p2 or '' - opponent.link1 = mw.ext.TeamLiquidIntegration.resolve_redirect(Logic.emptyOr( - opponent.p1link, - Variables.varDefault(opponent.p1 .. '_page') - ) or opponent.p1):gsub(' ', '_') - opponent.link2 = mw.ext.TeamLiquidIntegration.resolve_redirect(Logic.emptyOr( - opponent.p2link, - Variables.varDefault(opponent.p2 .. '_page') - ) or opponent.p2):gsub(' ', '_') - if Logic.readBool(opponent.extradata.isarchon) then - opponent.p1faction = Faction.read(opponent.race) or Faction.defaultFaction - opponent.p2faction = opponent.p1faction - else - opponent.p1faction = Faction.read(Logic.emptyOr( - opponent.p1race, - Variables.varDefault(opponent.p1 .. '_faction') - )) or Faction.defaultFaction - opponent.p2faction = Faction.read(Logic.emptyOr( - opponent.p2race, - Variables.varDefault(opponent.p2 .. '_faction') - )) or Faction.defaultFaction - end - - local players = {} - for playerIndex = 1, 2 do - local flag = Logic.emptyOr( - opponent['p' .. playerIndex .. 'flag'], - Variables.varDefault(opponent['p' .. playerIndex] .. '_flag') - ) - - players[playerIndex] = { - displayname = opponent['p' .. playerIndex], - name = opponent['link' .. playerIndex], - flag = Flags.CountryName(flag), - extradata = {faction = Faction.read(opponent['p' .. playerIndex .. 'faction']) or Faction.defaultFaction} - } - end - local name = opponent.link1 .. ' / ' .. opponent.link2 - - return { - type = opponent.type, - name = name, - score = opponent.score, - extradata = opponent.extradata, - match2players = players - } -end - ----@param opponent table ----@param playernumber integer ----@return table -function StarcraftMatchGroupInput.ProcessOpponentInput(opponent, playernumber) - local name = '' - - local players = {} - for playerIndex = 1, playernumber do - local playerName = opponent['p' .. playerIndex] or '' - local link = mw.ext.TeamLiquidIntegration.resolve_redirect(Logic.emptyOr( - opponent['p' .. playerIndex .. 'link'], - Variables.varDefault(playerName .. '_page') - ) or playerName):gsub(' ', '_') - local faction = Logic.emptyOr( - opponent['p' .. playerIndex .. 'race'], - Variables.varDefault(playerName .. '_faction'), - '' - ) - name = name .. (playerIndex ~= 1 and ' / ' or '') .. link - local flag = Logic.emptyOr( - opponent['p' .. playerIndex .. 'flag'], - Variables.varDefault((opponent['p' .. playerIndex] or '') .. '_flag') - ) - - players[playerIndex] = { - displayname = playerName, - name = link, - flag = Flags.CountryName(flag), - extradata = {faction = Faction.read(faction) or Faction.defaultFaction} - } - end - - return { - type = opponent.type, - name = name, - score = opponent.score, - extradata = opponent.extradata, - match2players = players - } -end - ----@param opponent table ----@return table -function StarcraftMatchGroupInput.ProcessLiteralOpponentInput(opponent) - local faction = opponent.race or '' - local flag = opponent.flag or '' - - local players = {} - if String.isNotEmpty(faction) or String.isNotEmpty(flag) then - players[1] = { - displayname = opponent[1], - name = '', - flag = Flags.CountryName(flag), - extradata = {faction = Faction.read(faction) or Faction.defaultFaction} - } - local extradata = opponent.extradata or {} - extradata.hasFactionOrFlag = true - end - - return { - type = opponent.type, - name = opponent[1], - score = opponent.score, - extradata = opponent.extradata, - match2players = players - } -end - ----@param playerData string|table? ----@return table -function StarcraftMatchGroupInput._getManuallyEnteredPlayers(playerData) - local players = {} - playerData = Json.parseIfString(playerData) or {} - for playerIndex = 1, MAX_NUM_PLAYERS do - local name = mw.ext.TeamLiquidIntegration.resolve_redirect(Logic.emptyOr( - playerData['p' .. playerIndex .. 'link'], - playerData['p' .. playerIndex] - ) or ''):gsub(' ', '_') - if String.isNotEmpty(name) then - players[playerIndex] = { - name = name, - displayname = playerData['p' .. playerIndex], - flag = Flags.CountryName(playerData['p' .. playerIndex .. 'flag']), - extradata = { - faction = playerData['p' .. playerIndex .. 'race'], - position = playerIndex - } - } - else - break - end - end - - return players -end - ----@param teamName string ----@return table -function StarcraftMatchGroupInput._getPlayersFromVariables(teamName) - local players = {} - for playerIndex = 1, MAX_NUM_PLAYERS do - local prefix = teamName .. '_p' .. playerIndex - local name = Variables.varDefault(prefix) - if Logic.isNotEmpty(name) then - ---@cast name -nil - local player = { - name = name:gsub(' ', '_'), - displayname = Variables.varDefault(prefix .. 'dn'), - flag = Flags.CountryName(Variables.varDefault(prefix .. 'flag')), - extradata = {faction = Variables.varDefault(prefix .. 'faction')} - } - if player.displayname then - Variables.varDefine(player.displayname .. '_page', player.name) - end - table.insert(players, player) - else - break - end - end - return players -end - ----@param opponent table ----@param date string? ----@return table -function StarcraftMatchGroupInput.ProcessTeamOpponentInput(opponent, date) - local name, icon, iconDark - - opponent.template = string.lower(Logic.emptyOr(opponent.template, opponent[1], 'tbd')--[[@as string]]) - - name, icon, iconDark, opponent.template = StarcraftMatchGroupInput._processTeamTemplateInput(opponent.template, date) - - local players = StarcraftMatchGroupInput._getManuallyEnteredPlayers(opponent.players) - if Logic.isEmpty(players) then - players = StarcraftMatchGroupInput._getPlayersFromVariables(name) - end - - return { - icon = icon, - icondark = iconDark, - template = opponent.template, - type = opponent.type, - name = name:gsub(' ', '_'), - score = opponent.score, - extradata = opponent.extradata, - match2players = players - } -end - ----@param template string? ----@param date string? ----@return string? ----@return string? ----@return string? ----@return string -function StarcraftMatchGroupInput._processTeamTemplateInput(template, date) - local icon, name, iconDark - template = string.lower(template or ''):gsub('_', ' ') - if String.isNotEmpty(template) and template ~= 'noteam' and - mw.ext.TeamTemplate.teamexists(template) then - - local templateData = mw.ext.TeamTemplate.raw(template, date) - icon = templateData.image - iconDark = templateData.imagedark - if String.isEmpty(icon) then - icon = templateData.legacyimage - end - if String.isEmpty(iconDark) then - iconDark = templateData.legacyimagedark - end - name = templateData.page - template = templateData.templatename or template - end - - return name, icon, iconDark, template -end - ---MapInput functions - ----@param match table ----@param mapIndex integer ----@param subGroupIndex integer ----@return table ----@return integer -function StarcraftMatchGroupInput._mapInput(match, mapIndex, subGroupIndex) - local map = Json.parseIfString(match['map' .. mapIndex]) - --redirect maps - if map.map ~= 'TBD' then - map.map = mw.ext.TeamLiquidIntegration.resolve_redirect(map.map or '') - end - - -- set initial extradata for maps - map.extradata = { - comment = map.comment, - displayname = map.mapDisplayName, - header = map.header, - server = map.server, - } - - -- determine score, resulttype, walkover and winner - map = StarcraftMatchGroupInput._mapWinnerProcessing(map) - - -- get participants data for the map + get map mode + winnerfaction and loserfaction - --(w/l faction stuff only for 1v1 maps) - map = StarcraftMatchGroupInput.ProcessPlayerMapData(map, match, 2) - - -- set sumscore to 0 if it isn't a number - if Logic.isEmpty(match.opponent1.sumscore) then - match.opponent1.sumscore = 0 - end - if Logic.isEmpty(match.opponent2.sumscore) then - match.opponent2.sumscore = 0 - end - - --adjust sumscore for winner opponent - if (tonumber(map.winner or 0) or 0) > 0 then - match['opponent' .. map.winner].sumscore = - match['opponent' .. map.winner].sumscore + 1 - end - - -- handle subgroup stuff if team match - if string.find(match.mode, 'team') then - map.subgroup = tonumber(map.subgroup or '') - if map.subgroup then - subGroupIndex = map.subgroup - else - subGroupIndex = subGroupIndex + 1 - map.subgroup = subGroupIndex - end - end - - match['map' .. mapIndex] = map - - return match, subGroupIndex -end - ----@param map table ----@return table -function StarcraftMatchGroupInput._mapWinnerProcessing(map) - map.scores = {} - local hasManualScores = false - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] - local obj = {} - if Logic.isNotEmpty(score) then - hasManualScores = true - score = CONVERT_STATUS_INPUT[score] or score - if Logic.isNumeric(score) then - obj.status = 'S' - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - - if hasManualScores then - for scoreIndex, _ in Table.iter.spairs(indexedScores, StarcraftMatchGroupInput._placementSortFunction) do - if not tonumber(map.winner or '') then - map.winner = scoreIndex - else - break - end - end - else - local winnerInput = tonumber(map.winner) - if Logic.isNotEmpty(map.walkover) then - local walkoverInput = tonumber(map.walkover) - if walkoverInput == 1 then - map.winner = 1 - elseif walkoverInput == 2 then - map.winner = 2 - elseif walkoverInput == 0 then - map.winner = 0 - end - map.walkover = Table.includes(ALLOWED_STATUSES, map.walkover) and map.walkover or 'L' - map.scores = {-1, -1} - map.resulttype = 'default' - elseif map.winner == 'skip' then - map.scores = {0, 0} - map.scores = {-1, -1} - map.resulttype = 'np' - elseif winnerInput == 1 then - map.scores = {1, 0} - elseif winnerInput == 2 then - map.scores = {0, 1} - elseif winnerInput == 0 or map.winner == 'draw' then - map.scores = {0.5, 0.5} - map.resulttype = 'draw' - end - end - - return map -end - ----@param map table ----@param match table ----@param numberOfOpponents integer ----@return table -function StarcraftMatchGroupInput.ProcessPlayerMapData(map, match, numberOfOpponents) - local participants = {} - local mapMode = '' - - for opponentIndex = 1, numberOfOpponents do - local opponentMapMode - if match['opponent' .. opponentIndex].type == Opponent.team then - local players = match['opponent' .. opponentIndex].match2players - participants, opponentMapMode = StarcraftMatchGroupInput._processTeamPlayerMapData( - players or {}, - map, - opponentIndex, - participants - ) - elseif match['opponent' .. opponentIndex].type == Opponent.literal then - opponentMapMode = 'Literal' - elseif - match['opponent' .. opponentIndex].type == Opponent.duo and - Logic.readBool(match['opponent' .. opponentIndex].extradata.isarchon) - then - opponentMapMode = 'Archon' - local players = match['opponent' .. opponentIndex].match2players - if Table.isEmpty(players) then - break - else - participants = StarcraftMatchGroupInput._processArchonPlayerMapData( - players, - map, - opponentIndex, - participants - ) - end - else - opponentMapMode = tonumber(OPPONENT_MODE_TO_PARTIAL_MATCH_MODE[match['opponent' .. opponentIndex].type]) - local players = match['opponent' .. opponentIndex].match2players - if Table.isEmpty(players) then - break - else - participants = StarcraftMatchGroupInput._processDefaultPlayerMapData( - players, - map, - opponentIndex, - participants - ) - end - end - mapMode = mapMode .. (opponentIndex ~= 1 and 'v' or '') .. opponentMapMode - - if mapMode == '1v1' and numberOfOpponents == 2 then - local opponentFactions, playerNameArray = StarcraftMatchGroupInput._fetchOpponentMapFactionsAndNames(participants) - if tonumber(map.winner or 0) == 1 then - map.extradata.winnerfaction = opponentFactions[1] - map.extradata.loserfaction = opponentFactions[2] - elseif tonumber(map.winner or 0) == 2 then - map.extradata.winnerfaction = opponentFactions[2] - map.extradata.loserfaction = opponentFactions[1] - end - map.extradata.opponent1 = playerNameArray[1] - map.extradata.opponent2 = playerNameArray[2] - end - map.patch = Variables.varDefault('tournament_patch', '') - end - - map.mode = mapMode - - map.participants = participants - return map -end - ----@param participants table ----@return table ----@return table -function StarcraftMatchGroupInput._fetchOpponentMapFactionsAndNames(participants) - local opponentFactions, playerNameArray = {}, {} - for participantKey, participantData in pairs(participants) do - local opponentIndex = tonumber(string.sub(participantKey, 1, 1)) - -- opponentIx can not be nil due to the format of the participants keys - ---@cast opponentIndex -nil - opponentFactions[opponentIndex] = participantData.faction - playerNameArray[opponentIndex] = participantData.player - end - - return opponentFactions, playerNameArray -end - ----@param players table ----@param map table ----@param opponentIndex integer ----@param participants table ----@return table -function StarcraftMatchGroupInput._processDefaultPlayerMapData(players, map, opponentIndex, participants) - map['t' .. opponentIndex .. 'p1race'] = Logic.emptyOr( - map['t' .. opponentIndex .. 'p1race'], - map['race' .. opponentIndex] - ) - - for playerIndex = 1, #players do - local faction = map['t' .. opponentIndex .. 'p' .. playerIndex .. 'race'] - participants[opponentIndex .. '_' .. playerIndex] = { - faction = Faction.read(faction or players[playerIndex].extradata.faction) or Faction.defaultFaction, - player = players[playerIndex].name - } - end - - return participants -end - ----@param players table ----@param map table ----@param opponentIndex integer ----@param participants table ----@return table -function StarcraftMatchGroupInput._processArchonPlayerMapData(players, map, opponentIndex, participants) - local faction = Logic.emptyOr( - map['opponent' .. opponentIndex .. 'race'], - map['race' .. opponentIndex], - players[1].extradata.faction - ) - participants[opponentIndex .. '_1'] = { - faction = Faction.read(faction) or Faction.defaultFaction, - player = players[1].name - } - - participants[opponentIndex .. '_2'] = { - faction = Faction.read(faction) or Faction.defaultFaction, - player = players[2].name - } - - return participants -end - ----@param players table ----@param map table ----@param opponentIndex integer ----@param participants table ----@return table ----@return string|integer -function StarcraftMatchGroupInput._processTeamPlayerMapData(players, map, opponentIndex, participants) - local amountOfTbds = 0 - local playerData = {} - - local numberOfPlayers = 0 - for prefix, playerInput, playerIndex in Table.iter.pairsByPrefix(map, 't' .. opponentIndex .. 'p') do - numberOfPlayers = numberOfPlayers + 1 - if playerInput:lower() == TBD or playerInput:lower() == TBA then - amountOfTbds = amountOfTbds + 1 - else - local link = Logic.emptyOr(map[prefix .. 'link'], Variables.varDefault(playerInput .. '_page')) or playerInput - link = mw.ext.TeamLiquidIntegration.resolve_redirect(link):gsub(' ', '_') - - local faction = Logic.readBool(map['opponent' .. opponentIndex .. 'archon']) - and Logic.emptyOr(map['t' .. opponentIndex .. 'race'], map['opponent' .. opponentIndex .. 'race']) - or map[prefix .. 'race'] - - playerData[link] = { - faction = Faction.read(faction), - position = playerIndex, - displayName = playerInput, - } - end - end - - local addToParticipants = function(currentPlayer, player, playerIndex) - local faction = currentPlayer.faction or (player.extradata or {}).faction or Faction.defaultFaction - - participants[opponentIndex .. '_' .. playerIndex] = { - faction = faction, - player = player.name, - position = currentPlayer.position, - flag = Flags.CountryName(player.flag), - } - end - - Array.forEach(players, function(player, playerIndex) - local currentPlayer = playerData[player.name] - if not currentPlayer then return end - - addToParticipants(currentPlayer, player, playerIndex) - playerData[player.name] = nil - end) - - -- if we have players not already in the match2players insert them - -- this is to break conditional data loops between match2 and teamCard/HDB - Table.iter.forEachPair(playerData, function(playerLink, player) - local faction = player.faction or Faction.defaultFaction - table.insert(players, { - name = playerLink, - displayname = player.displayName, - extradata = {faction = faction}, - }) - addToParticipants(player, players[#players], #players) - end) - - Array.forEach(Array.range(1, amountOfTbds), function(tbdIndex) - participants[opponentIndex .. '_' .. (#players + tbdIndex)] = { - faction = Faction.defaultFaction, - player = TBD:upper(), - } - end) - - if numberOfPlayers == 2 and Logic.readBool(map['opponent' .. opponentIndex .. 'archon']) then - return participants, 'Archon' - elseif numberOfPlayers == 2 and Logic.readBool(map['opponent' .. opponentIndex .. 'duoSpecial']) then - return participants, '2S' - elseif numberOfPlayers == 4 and Logic.readBool(map['opponent' .. opponentIndex .. 'quadSpecial']) then - return participants, '4S' - end - - return participants, numberOfPlayers -end - --- function to sort out winner/placements ----@param tbl table ----@param key1 string|integer ----@param key2 string|integer ----@return boolean -function StarcraftMatchGroupInput._placementSortFunction(tbl, key1, key2) - local opponent1 = tbl[key1] - local opponent2 = tbl[key2] - local opponent1Norm = opponent1.status == 'S' - local opponent2Norm = opponent2.status == 'S' - if opponent1Norm then - if opponent2Norm then - return tonumber(opponent1.score) > tonumber(opponent2.score) - else return true end - else - if opponent2Norm then return false - elseif opponent1.status == 'W' then return true - elseif Table.includes(DEFAULT_LOSS_STATUSES, opponent1.status) then return false - elseif opponent2.status == 'W' then return false - elseif Table.includes(DEFAULT_LOSS_STATUSES, opponent2.status) then return true - else return true end - end -end - -return StarcraftMatchGroupInput diff --git a/components/match2/commons/starcraft_starcraft2/match_group_input_starcraft_ffa.lua b/components/match2/commons/starcraft_starcraft2/match_group_input_starcraft_ffa.lua index 8f033448b9e..adbb63257cb 100644 --- a/components/match2/commons/starcraft_starcraft2/match_group_input_starcraft_ffa.lua +++ b/components/match2/commons/starcraft_starcraft2/match_group_input_starcraft_ffa.lua @@ -6,547 +6,414 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- +local Array = require('Module:Array') +local DateExt = require('Module:Date/Ext') +local FnUtil = require('Module:FnUtil') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') local Table = require('Module:Table') - -local StarcraftMatchGroupInput = Lua.import('Module:MatchGroup/Input/Starcraft/deprecated') -local Opponent = Lua.import('Module:Opponent') - -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L'} -local ALLOWED_STATUSES2 = {W = 'W', FF = 'FF', L = 'L', DQ = 'DQ', ['-'] = 'L'} -local MAX_NUM_VODGAMES = 9 -local MODES2 = { - solo = '1', - duo = '2', - trio = '3', - quad = '4', - team = 'team', - literal = 'literal' -} -local ALLOWED_BG = { - up = 'up', - down = 'down', - stayup = 'stayup', - staydown = 'staydown', - stay = 'stay', - mid = 'stay', - drop = 'down', - proceed = 'up', -} -local _TBD_STRINGS = { - 'definitions', - 'tbd' +local Variables = require('Module:Variables') + +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') +local Streams = Lua.import('Module:Links/Stream') + +local StarcraftMatchGroupInput = Lua.import('Module:MatchGroup/Input/Starcraft') +local BaseMatchFunctions = StarcraftMatchGroupInput.MatchFunctions +local BaseMapFunctions = StarcraftMatchGroupInput.MapFunctions + +local ADVANCE_BACKGROUND = 'up' +local DEFUALT_BACKGROUND = 'down' +local VALID_BACKGROUNDS = { + ADVANCE_BACKGROUND, + DEFUALT_BACKGROUND, + 'stayup', + 'staydown', + 'stay', } -local _BESTOF_DUMMY = 9999 -local _DEFAULT_WIN_SCORE_VALUE = 9999 -local _PLACEMENT_DUMMY = 99 - -local StarcraftFfaInput = {} - -function StarcraftFfaInput.adjustData(match) - local noscore = Logic.readBool(match.noscore) or Logic.readBool(match.nopoints) - match.noscore = noscore - - --process pbg entries and set them into match.pbg (will get merged into extradata later on) - match = StarcraftFfaInput._getPbg(match) - - --parse opponents + determine match mode + set initial stuff - match.mode = '' - local numberOfOpponents - match, numberOfOpponents = StarcraftFfaInput._opponentInput(match, noscore) - - --indicate it is an FFA match - match.mode = match.mode .. 'ffa' - - --main processing done here - local subgroup = 0 - for mapKey, map in Table.iter.pairsByPrefix(match, 'map') do - if - Logic.isNotEmpty(map.opponent1placement) or Logic.isNotEmpty(map.placement1) - or Logic.isNotEmpty(map.points1) or Logic.isNotEmpty(map.opponent1points) - or Logic.isNotEmpty(map.score1) or Logic.isNotEmpty(map.opponent1score) - or String.isNotEmpty(map.map) - then - match, subgroup = StarcraftFfaInput._mapInput(match, mapKey, subgroup, noscore, numberOfOpponents) - else - match[mapKey] = nil - break - end +local MODE_FFA = 'FFA' +local TBD = 'TBD' +local ASSUME_FINISHED_AFTER = MatchGroupInputUtil.ASSUME_FINISHED_AFTER +local NOW = os.time() + +local StarcraftFfaMatchGroupInput = {} +local MatchFunctions = {} +local MapFunctions = {} + +---@param match table +---@param options table? +---@return table +function StarcraftFfaMatchGroupInput.processMatch(match, options) + Table.mergeInto(match, BaseMatchFunctions.readDate(match.date)) + + match.stream = Streams.processStreams(match) + match.vod = Logic.nilIfEmpty(match.vod) + match.links = BaseMatchFunctions.getLinks(match) + + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) + + local opponents = BaseMatchFunctions.readOpponents(match) + + local games = MatchFunctions.extractMaps(match, opponents) + + local finishedInput = match.finished --[[@as string?]] + match.bestof = tonumber(match.firstto) or tonumber(match.bestof) + + match.finished = MatchFunctions.isFinished(match, opponents) + match.mode = MODE_FFA + + if MatchGroupInputUtil.isNotPlayed(match.winner, finishedInput) then + match.finished = true + match.resulttype = MatchGroupInputUtil.RESULT_TYPE.NOT_PLAYED + match.extradata = {ffa = 'true'} + return match end - --apply vodgames - for index = 1, MAX_NUM_VODGAMES do - local vodgame = match['vodgame' .. index] - if Logic.isNotEmpty(vodgame) and Logic.isNotEmpty(match['map' .. index]) then - match['map' .. index].vod = match['map' .. index].vod or vodgame - end + match.pbg = MatchFunctions.getPBG(match) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and not Logic.readBool(match.noscore) + and MatchFunctions.calculateMatchScore(games, opponents) + or nil + + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) + + Array.forEach(opponents, function(opponent) + opponent.placement = tonumber(opponent.placement) + end) + + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(match.winner, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + StarcraftFfaMatchGroupInput._setPlacements(opponents) + match.winner = StarcraftFfaMatchGroupInput._getWinner(opponents, match.winner, match.resulttype) end - match = StarcraftFfaInput._matchWinnerProcessing(match, numberOfOpponents, noscore) + Array.forEach(opponents, function(opponent) + opponent.extradata = opponent.extradata or {} + opponent.extradata.noscore = Logic.readBool(match.noscore) + opponent.extradata.bg = MatchFunctions.readBg(opponent.bg) + or match.pbg[opponent.placement] + or DEFUALT_BACKGROUND + + opponent.extradata.advances = Logic.readBool(opponent.advances) + or (match.bestof and (opponent.score or 0) >= match.bestof) + or opponent.extradata.bg == ADVANCE_BACKGROUND + or opponent.placement == 1 + end) + + match.opponents = opponents + match.games = games + + match.extradata = MatchFunctions.getExtraData(match) return match end +---@param match table +---@param opponents {score: integer?}[] +---@return boolean +function MatchFunctions.isFinished(match, opponents) + if MatchGroupInputUtil.isNotPlayed(match.winner, match.finished) then + return true + end -function StarcraftFfaInput._getPbg(match) - local pbg = {} + local finished = Logic.readBoolOrNil(match.finished) + if finished ~= nil then + return finished + end - local advancecount = tonumber(match.advancecount or 0) or 0 - if advancecount > 0 then - for index = 1, advancecount do - pbg[index] = 'up' - end + -- If a winner has been set + if Logic.isNotEmpty(match.winner) then + return true end - local index = 1 - while StarcraftFfaInput._bgClean(match['pbg' .. index]) ~= '' do - pbg[index] = StarcraftFfaInput._bgClean(match['pbg' .. index]) - match['pbg' .. index] = nil - index = index + 1 + -- If enough time has passed since match started, it should be marked as finished + local threshold = match.dateexact and ASSUME_FINISHED_AFTER.EXACT or ASSUME_FINISHED_AFTER.ESTIMATE + if match.timestamp ~= DateExt.defaultTimestamp and (match.timestamp + threshold) < NOW then + return true end - match.pbg = pbg + return MatchFunctions.placementHasBeenSet(opponents) +end - return match +---@param opponents table[] +---@return boolean +function MatchFunctions.placementHasBeenSet(opponents) + return Array.any(opponents, function(opponent) return Logic.isNumeric(opponent.placement) end) end ---helper function -function StarcraftFfaInput._bgClean(pbg) - local pbgInput = pbg - pbg = string.lower(pbg or '') - if pbg == '' then - return '' - else - pbg = ALLOWED_BG[pbg] - - if not pbg then - error('Bad bg/pbg entry "' .. pbgInput .. '"') - end +---@param maps table[] +---@param opponents table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps, opponents) + return function(opponentIndex) + local opponent = opponents[opponentIndex] + local sum = (opponent.extradata.advantage or 0) - (opponent.extradata.penalty or 0) + Array.forEach(maps, function(map) + sum = sum + ((map.scores or {})[opponentIndex] or 0) + end) + return sum + end +end - return pbg +---@param match table +---@param opponents table[] +---@return table[] +function MatchFunctions.extractMaps(match, opponents) + local hasScores = not Logic.readBool(match.noscore) + local maps = {} + for mapKey, mapInput in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local map = MapFunctions.readMap(mapInput, #opponents, hasScores) + + map.participants = BaseMapFunctions.getParticipants(mapInput, opponents) + + map.mode = BaseMapFunctions.getMode(mapInput, map.participants, opponents) + + table.insert(maps, map) + match[mapKey] = nil end + + return maps end ---function to get extradata for storage -function StarcraftFfaInput.getExtraData(match) +---@param match table +---@return table +function MatchFunctions.getExtraData(match) local extradata = { - featured = match.featured, - veto1by = String.nilIfEmpty(match.vetoplayer1) or match.vetoopponent1, - veto1 = match.veto1, - veto2by = String.nilIfEmpty(match.vetoplayer2) or match.vetoopponent2, - veto2 = match.veto2, - veto3by = String.nilIfEmpty(match.vetoplayer3) or match.vetoopponent3, - veto3 = match.veto3, - veto4by = String.nilIfEmpty(match.vetoplayer4) or match.vetoopponent4, - veto4 = match.veto4, - veto5by = String.nilIfEmpty(match.vetoplayer5) or match.vetoopponent5, - veto5 = match.veto5, - veto6by = String.nilIfEmpty(match.vetoplayer6) or match.vetoopponent6, - veto6 = match.veto6, + casters = MatchGroupInputUtil.readCasters(match, {noSort = true}), ffa = 'true', - noscore = match.noscore, - showplacement = match.showplacement, + noscore = tostring(Logic.readBool(match.noscore)), + showplacement = Logic.readBoolOrNil(match.showplacement), } - --add the pbg stuff - for key, item in pairs(match.pbg) do - extradata['pbg' .. key] = item + for prefix, vetoMap, vetoIndex in Table.iter.pairsByPrefix(match, 'veto') do + BaseMatchFunctions.getVeto(extradata, vetoMap, match, prefix, vetoIndex) end - match.pbg = nil + + Array.forEach(match.pbg, function(value, key) extradata['pbg' .. key] = value end) return extradata end --- function to sort out placements -function StarcraftFfaInput._placementSortFunction(tbl, key1, key2) - local op1 = tbl[key1] - local op2 = tbl[key2] - return tonumber(op1) > tonumber(op2) +---@param match table +---@return table +function MatchFunctions.getPBG(match) + local advanceCount = tonumber(match.advancecount) or 0 + + return Array.mapIndexes(function(pbgIndex) + return MatchFunctions.readBg(match['pbg' .. pbgIndex]) + or (pbgIndex <= advanceCount and ADVANCE_BACKGROUND) + or nil + end) end ---[[ - -Match Winner, Walkover, Placement, Resulttype, Status functions - -]]-- -function StarcraftFfaInput._matchWinnerProcessing(match, numberOfOpponents, noscore) - local bestof = tonumber(match.firstto) or tonumber(match.bestof) or _BESTOF_DUMMY - match.bestof = bestof - local walkover = match.walkover - local IndScore = {} - for opponentIndex = 1, numberOfOpponents do - local opponent = match['opponent' .. opponentIndex] - --determine opponent scores, status - --determine MATCH winner, resulttype and walkover - if walkover then - if Logic.isNumeric(walkover) then - local numericWalkover = tonumber(walkover) - if numericWalkover == opponentIndex then - match.winner = opponentIndex - match.walkover = 'L' - opponent.status = 'W' - IndScore[opponentIndex] = _DEFAULT_WIN_SCORE_VALUE - elseif numericWalkover == 0 then - match.winner = 0 - match.walkover = 'L' - opponent.status = 'L' - IndScore[opponentIndex] = -1 - else - opponent.status = - ALLOWED_STATUSES2[string.upper(opponent.score or '')] or 'L' - IndScore[opponentIndex] = -1 - end - elseif Table.includes(ALLOWED_STATUSES, string.upper(walkover)) then - if tonumber(match.winner or 0) == opponentIndex then - IndScore[opponentIndex] = _DEFAULT_WIN_SCORE_VALUE - opponent.status = 'W' - else - IndScore[opponentIndex] = -1 - opponent.status = ALLOWED_STATUSES2[string.upper(walkover)] or 'L' - end - else - opponent.status = - ALLOWED_STATUSES2[string.upper(opponent.score or '')] or 'L' - match.walkover = 'L' - if ALLOWED_STATUSES2[string.upper(opponent.score or '')] == 'W' then - IndScore[opponentIndex] = _DEFAULT_WIN_SCORE_VALUE - else - IndScore[opponentIndex] = -1 - end - end - opponent.score = -1 - match.finished = 'true' - match.resulttype = 'default' - elseif Logic.readBool(match.cancelled) then - match.resulttype = 'np' - match.finished = 'true' - opponent.score = -1 - IndScore[opponentIndex] = -1 - elseif ALLOWED_STATUSES2[string.upper(opponent.score or '')] then - if string.upper(opponent.score) == 'W' then - match.winner = opponentIndex - match.resulttype = 'default' - match.finished = 'true' - opponent.score = -1 - opponent.status = 'W' - IndScore[opponentIndex] = _DEFAULT_WIN_SCORE_VALUE - else - match.resulttype = 'default' - match.finished = 'true' - match.walkover = ALLOWED_STATUSES2[string.upper(opponent.score)] - opponent.status = - ALLOWED_STATUSES2[string.upper(opponent.score)] - opponent.score = -1 - IndScore[opponentIndex] = -1 - end - else - opponent.status = 'S' - opponent.score = tonumber(opponent.score or '') - or tonumber(opponent.sumscore) or -1 - IndScore[opponentIndex] = opponent.score - end - end +---@param input string? +---@return string? +function MatchFunctions.readBg(input) + if Logic.isEmpty(input) then return nil end + ---@cast input -nil - match = StarcraftFfaInput._matchPlacements(match, numberOfOpponents, noscore, IndScore) + input = string.lower(input) + assert(Table.includes(VALID_BACKGROUNDS, input), 'Bad bg/pbg entry "' .. input .. '"') - return match + return input end ---determine placements and winner (if not already set) -function StarcraftFfaInput._matchPlacements(match, numberOfOpponents, noscore, IndScore) - local counter = 0 - local temp = {} - match.finished = Logic.isNotEmpty(match.finished) - and match.finished ~= 'false' and match.finished ~= '0' - and 'true' or nil - - if not noscore then - for scoreIndex, score in Table.iter.spairs(IndScore, StarcraftFfaInput._placementSortFunction) do - local opponent = match['opponent' .. scoreIndex] - counter = counter + 1 - if counter == 1 and Logic.isEmpty(match.winner) then - if match.finished or score >= match.bestof then - match.winner = scoreIndex - match.finished = 'true' - opponent.placement = tonumber(opponent.placement or '') or counter - opponent.extradata.advances = true - opponent.extradata.bg = String.nilIfEmpty(opponent.extradata.bg) - or match.pbg[opponent.placement] - or 'down' - temp.place = counter - temp.score = IndScore[scoreIndex] - else - break - end - elseif match.finished then - if temp.score == score then - opponent.placement = tonumber(opponent.placement or '') or temp.place - else - opponent.placement = tonumber(opponent.placement or '') or counter - temp.place = counter - temp.score = IndScore[scoreIndex] - end - opponent.extradata.bg = String.nilIfEmpty(opponent.extradata.bg) - or match.pbg[opponent.placement] - or 'down' - if opponent.extradata.bg == 'up' then - opponent.extradata.advances = true - end - else - break - end - end - elseif tonumber(match.winner or '') then - for oppIndex = 1, numberOfOpponents do - local opponent = match['opponent' .. oppIndex] - opponent.placement = tonumber(opponent.placement or '') or _PLACEMENT_DUMMY - if opponent.placement == _PLACEMENT_DUMMY and tonumber(match.winner) == oppIndex then - opponent.placement = 1 - end - end - else - for oppIndex = 1, numberOfOpponents do - local opponent = match['opponent' .. oppIndex] - opponent.placement = tonumber(opponent.placement or '') or _PLACEMENT_DUMMY - if opponent.placement == 1 then - match.winner = oppIndex - end - end +---@param mapInput table +---@param opponentCount integer +---@param hasScores boolean +---@return table +function MapFunctions.readMap(mapInput, opponentCount, hasScores) + local mapName = mapInput.map + if mapName and mapName:upper() ~= TBD then + mapName = mw.ext.TeamLiquidIntegration.resolve_redirect(mapInput.map) + elseif mapName then + mapName = TBD end - if match.finished then - for oppIndex = 1, numberOfOpponents do - local opponent = match['opponent' .. oppIndex] - opponent.extradata.bg = String.nilIfEmpty(opponent.extradata.bg) - or match.pbg[opponent.placement] - or 'down' - end - end + local map = { + map = mapName, + patch = Variables.varDefault('tournament_patch', ''), + vod = mapInput.vod, + extradata = { + comment = mapInput.comment, + displayname = mapInput.mapDisplayName, + } + } - return match -end + if MatchGroupInputUtil.isNotPlayed(mapInput.winner, mapInput.finished) then + map.finished = true + map.resulttype = MatchGroupInputUtil.RESULT_TYPE.NOT_PLAYED + map.scores = {} + map.statuses = {} + return map + end ---[[ + local opponentsInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + return MapFunctions.getOpponentInfo(mapInput, opponentIndex, hasScores) + end) -OpponentInput functions + map.scores = Array.map(opponentsInfo, Operator.property('score')) -]]-- -function StarcraftFfaInput._opponentInput(match, noscore) - local numberOfOpponents - for opponentKey, opponent, opponentIndex in Table.iter.pairsByPrefix(match, 'opponent') do - numberOfOpponents = opponentIndex + map.finished = MapFunctions.isFinished(mapInput, opponentCount, hasScores) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(mapInput.winner, mapInput.finished, opponentsInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentsInfo) + StarcraftFfaMatchGroupInput._setPlacements(opponentsInfo, not hasScores) + map.winner = StarcraftFfaMatchGroupInput._getWinner(opponentsInfo, mapInput.winner, map.resulttype) + end - -- Convert byes to literals - if string.lower(opponent.template or '') == 'bye' or string.lower(opponent.name or '') == 'bye' then - opponent = {type = Opponent.literal, name = 'BYE'} - end + Array.forEach(opponentsInfo, function(opponentInfo, opponentIndex) + map.extradata['placement' .. opponentIndex] = opponentInfo.placement + end) - local bg = StarcraftFfaInput._bgClean(opponent.bg) - opponent.bg = nil - local advances = opponent.advance - if String.isEmpty(advances) then - advances = opponent.win - if String.isEmpty(advances) then - advances = opponent.advances - end - end - advances = Logic.isNotEmpty(advances) and advances ~= 'false' and advances ~= '0' - - --opponent processing (first part) - --sort out extradata - opponent.extradata = { - advances = advances, - advantage = opponent.advantage, - bg = bg, - isarchon = opponent.isarchon, - noscore = noscore, - score2 = opponent.score2, - } + return map +end - --set initial opponent sumscore - opponent.sumscore = - tonumber(opponent.extradata.advantage or '') or '' - - -- read placement input for the opponent to overwrite - -- the one set by the default opponent input processing - -- as that sets placements assuming it is a non ffa match - local inputPlace = opponent.placement - --process input depending on type - if opponent.type == Opponent.solo then - opponent = StarcraftMatchGroupInput.ProcessSoloOpponentInput(opponent) - elseif opponent.type == Opponent.duo then - opponent = StarcraftMatchGroupInput.ProcessDuoOpponentInput(opponent) - elseif opponent.type == Opponent.trio then - opponent = StarcraftMatchGroupInput.ProcessOpponentInput(opponent, 3) - elseif opponent.type == Opponent.quad then - opponent = StarcraftMatchGroupInput.ProcessOpponentInput(opponent, 4) - elseif opponent.type == Opponent.team then - opponent = StarcraftMatchGroupInput.ProcessTeamOpponentInput(opponent, match.date) - elseif opponent.type == Opponent.literal then - opponent = StarcraftMatchGroupInput.ProcessLiteralOpponentInput(opponent) - else - error('Unsupported Opponent Type') - end - match[opponentKey] = opponent - - opponent.placement = inputPlace - - --mark match as noQuery if it contains TBD or Literal opponents - local opponentName = string.lower(opponent.name or '') - local playerName = string.lower(((opponent.match2players or {})[1] or {}).name or '') - if - opponent.type == Opponent.literal or - Table.includes(_TBD_STRINGS, opponentName) or - Table.includes(_TBD_STRINGS, playerName) - then - match.noQuery = 'true' - end +---@param mapInput table +---@param opponentCount integer +---@param hasScores boolean +---@return boolean +function MapFunctions.isFinished(mapInput, opponentCount, hasScores) + local finished = Logic.readBoolOrNil(mapInput.finished) + if finished ~= nil then + return finished + end - local mode = MODES2[opponent.type] - if mode == '2' and opponent.extradata.isarchon == 'true' then - mode = 'Archon' - end + return Array.any(Array.range(1, opponentCount), function(opponentIndex) + return Logic.isNotEmpty(mapInput['placement' .. opponentIndex]) or + (hasScores and Logic.isNotEmpty(mapInput['score' .. opponentIndex])) + end) +end - match.mode = match.mode .. mode .. '_' - end +---@param mapInput any +---@param opponentIndex any +---@param hasScores any +---@return {placement: integer?, score: integer?, status: string} +function MapFunctions.getOpponentInfo(mapInput, opponentIndex, hasScores) + local score, status = MatchGroupInputUtil.computeOpponentScore{ + walkover = mapInput.walkover, + winner = mapInput.winner, + opponentIndex = opponentIndex, + score = mapInput['score' .. opponentIndex], + } - return match, numberOfOpponents + return { + placement = tonumber(mapInput['placement' .. opponentIndex]), + score = hasScores and score or nil, + status = status, + } end ---[[ +--- helper fucntions applicable for both map and match -MapInput functions +---@param opponents {placement: integer?, score: integer?, status: string} +---@param noScores boolean? +function StarcraftFfaMatchGroupInput._setPlacements(opponents, noScores) + if noScores then return end -]]-- -function StarcraftFfaInput._mapInput(match, mapKey, subgroup, noscore, numberOfOpponents) - local map = match[mapKey] + if Array.all(opponents, function(opponent) + return Logic.isNotEmpty(opponent.placement) + end) then return end - --redirect maps - if map.map ~= 'TBD' then - map.map = mw.ext.TeamLiquidIntegration.resolve_redirect(map.map or '') + ---@param status string + ---@return string + local toSortStatus = function(status) + if status == MatchGroupInputUtil.STATUS.DEFAULT_WIN or status == MatchGroupInputUtil.STATUS.SCORE or not status then + return status + end + return MatchGroupInputUtil.STATUS.DEFAULT_LOSS end - --set initial extradata for maps - map.extradata = { - comment = map.comment or '', - header = map.header or '', - noQuery = match.noQuery, - } + local cache = {} + + ---@param status string + ---@param score integer? + ---@param manualPlacement integer? + ---@return boolean + local isNewPlacement = function(status, score, manualPlacement) + if manualPlacement then + return true + elseif cache.manualPlacement and not manualPlacement then + return true + elseif status ~= cache.status then + return true + elseif status == MatchGroupInputUtil.STATUS.SCORE and score ~= cache.score then + return true + end + return false + end - --inherit stuff from match data - map.type = match.type - map.liquipediatier = match.liquipediatier - map.liquipediatiertype = match.liquipediatiertype - map.game = match.game - map.date = match.date - - --get participants data for the map + get map mode - map = StarcraftMatchGroupInput.ProcessPlayerMapData(map, match, numberOfOpponents) - - --determine scores, resulttype, walkover and winner - map = StarcraftFfaInput._mapScoreProcessing(map, numberOfOpponents, noscore) - - --adjust sumscores if scores/points are used - if not noscore then - for j = 1, numberOfOpponents do - --set sumscore to 0 if it isn't a number - if String.isEmpty(match['opponent' .. j].sumscore) then - match['opponent' .. j].sumscore = 0 - end - match['opponent' .. j].sumscore = match['opponent' .. j].sumscore + (tonumber(map.scores[j] or 0) or 0) + cache.placement = 1 + cache.skipped = 0 + for _, opponent in Table.iter.spairs(opponents, StarcraftFfaMatchGroupInput._placementSortFunction) do + local currentStatus = toSortStatus(opponent.status) + local currentScore = opponent.score or 0 + if isNewPlacement(currentStatus, currentScore, opponent.placement) then + cache.manualPlacement = opponent.placement + cache.placement = opponent.placement or (cache.placement + cache.skipped) + cache.skipped = 0 + cache.score = currentScore + cache.status = currentStatus end + opponent.placement = cache.placement + cache.skipped = cache.skipped + 1 end +end - --subgroup handling - subgroup = tonumber(map.subgroup) or subgroup + 1 +---@param opponents {placement: integer?, score: integer?, status: string} +---@param winnerInput integer|string|nil +---@param resultType string? +---@return integer? +function StarcraftFfaMatchGroupInput._getWinner(opponents, winnerInput, resultType) + if Logic.isNumeric(winnerInput) then + return tonumber(winnerInput) + elseif resultType == MatchGroupInputUtil.RESULT_TYPE.DRAW then + return MatchGroupInputUtil.WINNER_DRAW + end + + local placements = Array.map(opponents, Operator.property('placement')) + local bestPlace = Array.min(placements) - match[mapKey] = map + local calculatedWinner = Array.indexOf(placements, FnUtil.curry(Operator.eq, bestPlace)) - return match, subgroup + return calculatedWinner ~= 0 and calculatedWinner or nil end +---@param opponents {placement: integer?, score: integer?, status: string}[] +---@param index1 integer +---@param index2 integer +---@return boolean +function StarcraftFfaMatchGroupInput._placementSortFunction(opponents, index1, index2) + local opponent1 = opponents[index1] + local opponent2 = opponents[index2] + + if opponent1.status == MatchGroupInputUtil.STATUS_INPUTS.DEFAULT_WIN then + return true + elseif Table.includes(MatchGroupInputUtil.STATUS_INPUTS, opponent1.status) then + return false + end -function StarcraftFfaInput._mapScoreProcessing(map, numberOfOpponents, noscore) - map.scores = {} - local indexedScores = {} - local hasScoreSet = false - --read scores - if not noscore then - for scoreIndex = 1, numberOfOpponents do - local score = String.nilIfEmpty(map['score' .. scoreIndex]) - or String.nilIfEmpty(map['points' .. scoreIndex]) - or '' - score = ALLOWED_STATUSES2[score] or tonumber(score) or 0 - indexedScores[scoreIndex] = score - if not Logic.isNumeric(score) then - map.resulttype = 'default' - if String.isEmpty(map.walkover) or map.walkover == 'L' then - if score == 'DQ' then - map.walkover = 'DQ' - elseif score == 'FF' then - map.walkover = 'FF' - else - map.walkover = 'L' - end - end - if score == 'W' then - indexedScores[scoreIndex] = _DEFAULT_WIN_SCORE_VALUE - else - indexedScores[scoreIndex] = -1 - end - end - - --check if any score is not 0, i.e. a score has been actually entered - if score ~= 0 then - hasScoreSet = true - end - - map.scores[scoreIndex] = score - end + if (opponent1.score or -1) ~= (opponent2.score or -1) then + return (opponent1.score or -1) > (opponent2.score or -1) + end - --determine map winner and placements from scores if not set manually - if hasScoreSet then - local counter = 0 - local temp = {} - for scoreIndex, score in Table.iter.spairs(indexedScores, StarcraftFfaInput._placementSortFunction) do - counter = counter + 1 - if counter == 1 and Logic.isEmpty(map.winner) then - map.winner = scoreIndex - map.extradata['placement' .. scoreIndex] = tonumber(map['placement' .. scoreIndex] or '') or - tonumber(map['opponent' .. scoreIndex .. 'placement'] or '') or counter - temp.place = counter - temp.score = indexedScores[scoreIndex] - elseif temp.score == score then - map.extradata['placement' .. scoreIndex] = tonumber(map['placement' .. scoreIndex] or '') or - tonumber(map['opponent' .. scoreIndex .. 'placement'] or '') or temp.place - else - map.extradata['placement' .. scoreIndex] = tonumber(map['placement' .. scoreIndex] or '') or - tonumber(map['opponent' .. scoreIndex .. 'placement'] or '') or counter - temp.place = counter - temp.score = indexedScores[scoreIndex] - end - end - end - elseif tonumber(map.winner or '') then - for oppIndex = 1, numberOfOpponents do - map.extradata['placement' .. oppIndex] = tonumber(map['placement' .. oppIndex] or '') or - tonumber(map['opponent' .. oppIndex .. 'placement'] or '') or _PLACEMENT_DUMMY - if map.extradata['placement' .. oppIndex] == _PLACEMENT_DUMMY and tonumber(map.winner) == oppIndex then - map.extradata['placement' .. oppIndex] = 1 - end - end - else - for oppIndex = 1, numberOfOpponents do - map.extradata['placement' .. oppIndex] = tonumber(map['placement' .. oppIndex] or '') or - tonumber(map['opponent' .. oppIndex .. 'placement'] or '') or _PLACEMENT_DUMMY - if map.extradata['placement' .. oppIndex] == 1 then - map.winner = oppIndex - end - end + if opponent1.placement and opponent2.placement then + return opponent1.placement < opponent2.placement + elseif opponent1.placement and not opponent2.placement then + return true + elseif opponent2.placement and not opponent1.placement then + return false end - return map + return index1 < index2 end -return StarcraftFfaInput +return StarcraftFfaMatchGroupInput diff --git a/components/match2/commons/starcraft_starcraft2/match_summary_ffa_starcraft.lua b/components/match2/commons/starcraft_starcraft2/match_summary_ffa_starcraft.lua index 95b57750bec..200fb4c79f5 100644 --- a/components/match2/commons/starcraft_starcraft2/match_summary_ffa_starcraft.lua +++ b/components/match2/commons/starcraft_starcraft2/match_summary_ffa_starcraft.lua @@ -8,9 +8,9 @@ local Lua = require('Module:Lua') local Placement = require('Module:Placement') -local StarcraftMatchExternalLinks = require('Module:MatchExternalLinks/Starcraft') local Table = require('Module:Table') +local MatchSummary = Lua.import('Module:MatchSummary/Base') local FfaMatchSummary = Lua.import('Module:MatchSummary/Ffa') local StarcraftMatchGroupUtil = Lua.import('Module:MatchGroup/Util/Starcraft') local StarcraftMatchSummary = Lua.import('Module:MatchSummary/Starcraft') @@ -69,14 +69,15 @@ function CustomFfaMatchSummary.GamePlacement(props) end function CustomFfaMatchSummary.Footer(props) - local links = StarcraftMatchExternalLinks.extractFromMatch(props.match) - if #links > 0 then - local linksNode = StarcraftMatchExternalLinks.MatchExternalLinks({links = links}) - :addClass('brkts-popup-sc-footer-links vodlink') - return mw.html.create('div'):addClass('ffa-match-summary-footer') - :node(linksNode) - else - return nil + local match = props.match + + local footer = MatchSummary.addVodsToFooter(match, MatchSummary.Footer()) + + footer:addLinks(StarcraftMatchSummary.LINKS_DATA, match.links) + + local footerDisplay = footer:create() + if footerDisplay then + return footerDisplay:addClass('ffa-match-summary-footer') end end diff --git a/components/match2/commons/starcraft_starcraft2/match_summary_starcraft.lua b/components/match2/commons/starcraft_starcraft2/match_summary_starcraft.lua index bb48e683ab7..28a1cd26fca 100644 --- a/components/match2/commons/starcraft_starcraft2/match_summary_starcraft.lua +++ b/components/match2/commons/starcraft_starcraft2/match_summary_starcraft.lua @@ -68,6 +68,9 @@ end local StarcraftMatchSummary = {} +-- make these available in FFA Matchsummary too +StarcraftMatchSummary.LINKS_DATA = LINKS_DATA + ---@param args {bracketId: string, matchId: string, config: table?} ---@return Html function StarcraftMatchSummary.MatchSummaryContainer(args) diff --git a/components/match2/wikis/ageofempires/legacy/legacy_bracket_match_summary.lua b/components/match2/wikis/ageofempires/legacy/legacy_bracket_match_summary.lua new file mode 100644 index 00000000000..b1b1933bc44 --- /dev/null +++ b/components/match2/wikis/ageofempires/legacy/legacy_bracket_match_summary.lua @@ -0,0 +1,61 @@ +--- +-- @Liquipedia +-- wiki=ageofempires +-- page=Module:LegacyBracketMatchSummary +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Arguments = require('Module:Arguments') +local Array = require('Module:Array') +local Json = require('Module:Json') +local Table = require('Module:Table') + +local LegacyBracketMatchSummary = {} + +---@param args table +---@return table +function LegacyBracketMatchSummary._handleMaps(args) + local isValidMap = true + local mapIndex = 1 + + while isValidMap do + local prefix = 'map' .. mapIndex + + --Template:MatchTeam + if args['match' .. mapIndex] then + local match = Json.parse(Table.extract(args, 'match' .. mapIndex)) --[[@as table]] + Table.mergeInto(args, + Table.map(match, function(subKey, value) + return prefix .. subKey, value + end) + ) + args[prefix] = Table.extract(args, prefix .. 'map') + args[prefix .. 'mode'] = Table.extract(args, prefix .. 'mapmode') + args['date' .. mapIndex] = args['date' .. mapIndex] or Table.extract(args, prefix .. 'date') + end + + if args[prefix] then + local mapInfo = Array.parseCommaSeparatedString(args[prefix], '|') + args[prefix] = mapInfo[1] + end + + isValidMap = args[prefix] or args[prefix .. 'win'] + mapIndex = mapIndex + 1 + end + + return args +end + +-- invoked by BracketMatchSummary +---@param frame Frame +---@return string +function LegacyBracketMatchSummary.convert(frame) + local args = Arguments.getArgs(frame) + args = LegacyBracketMatchSummary._handleMaps(args) + args['civdraft'] = Table.extract(args, 'draft') + + return Json.stringify(args) +end + +return LegacyBracketMatchSummary diff --git a/components/match2/wikis/ageofempires/legacy/match_group_legacy_default.lua b/components/match2/wikis/ageofempires/legacy/match_group_legacy_default.lua new file mode 100644 index 00000000000..e086577ac9c --- /dev/null +++ b/components/match2/wikis/ageofempires/legacy/match_group_legacy_default.lua @@ -0,0 +1,64 @@ +--- +-- @Liquipedia +-- wiki=ageofempires +-- page=Module:MatchGroup/Legacy/Default +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Class = require('Module:Class') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') +local Table = require('Module:Table') + +local MatchGroupLegacy = Lua.import('Module:MatchGroup/Legacy') + +---@class AgeofEmpiresMatchGroupLegacyDefault: MatchGroupLegacy +local MatchGroupLegacyDefault = Class.new(MatchGroupLegacy) + +---@return table +function MatchGroupLegacyDefault:getMap() + local map = { + ['$notEmpty$'] = 'map$1$', + map = 'map$1$', + mode = 'map$1$mode', + winner = 'map$1$win', + civs1 = 'map$1$p1civ', + civs2 = 'map$1$p2civ', + vod = 'vodgame$1$', + date = 'date$1$', + } + + if self.bracketType == 'team' then + Table.mergeInto(map, { + players1 = 'map$1$t1players', + players2 = 'map$1$t2players', + civs1 = 'map$1$t1civs', + civs2 = 'map$1$t2civs', + }) + end + + return map +end + +---@param details table +---@param mapIndex number +---@return table? +function MatchGroupLegacyDefault:handleMap(details, mapIndex) + local blueprint = self:_getMap(mapIndex) + if Logic.isEmpty(details[blueprint['$notEmpty$']]) and Logic.isEmpty(details[blueprint['winner']]) then + return nil + end + + blueprint = Table.copy(blueprint) + blueprint['$notEmpty$'] = nil + + return self:_copyAndReplace(blueprint, details, true) +end + +---@param frame Frame +function MatchGroupLegacyDefault.run(frame) + return MatchGroupLegacyDefault(frame):build() +end + +return MatchGroupLegacyDefault diff --git a/components/match2/wikis/ageofempires/match_group_input_custom.lua b/components/match2/wikis/ageofempires/match_group_input_custom.lua index 55a935835c8..103a5e0b4bd 100644 --- a/components/match2/wikis/ageofempires/match_group_input_custom.lua +++ b/components/match2/wikis/ageofempires/match_group_input_custom.lua @@ -12,64 +12,199 @@ local Game = require('Module:Game') local Json = require('Module:Json') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Operator = require('Module:Operator') +local Page = require('Module:Page') local String = require('Module:StringUtils') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') local Opponent = Lua.import('Module:Opponent') local Streams = Lua.import('Module:Links/Stream') -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L'} -local CONVERT_STATUS_INPUT = {W = 'W', FF = 'FF', L = 'L', DQ = 'DQ', ['-'] = 'L'} -local DEFAULT_LOSS_STATUSES = {'FF', 'L', 'DQ'} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_PLAYERS = 10 -local DEFAULT_BESTOF = 99 - local CustomMatchGroupInput = {} ---- called from Module:MatchGroup +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, +} + ---@param match table ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) assert(not Logic.readBool(match.ffa), 'FFA is not yet supported in AoE match2.') - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - CustomMatchGroupInput._getOpponents(match) - CustomMatchGroupInput._getTournamentVars(match) - CustomMatchGroupInput._processMaps(match) - CustomMatchGroupInput._calculateWinner(match) - CustomMatchGroupInput._updateFinished(match) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) + match.game, match.mapsInfo = CustomMatchGroupInput._getMapsAndGame(match) + + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return CustomMatchGroupInput.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) + + local games = CustomMatchGroupInput.extractMaps(match, opponents) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and CustomMatchGroupInput.calculateMatchScore(games) + or nil + + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) + match.bestof = CustomMatchGroupInput.getBestOf(match.bestof) + + local winnerInput = match.winner --[[@as string?]] + local finishedInput = match.finished --[[@as string?]] + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) + end + + match.mode = Opponent.toLegacyMode(opponents[1].type, opponents[2].type) match.stream = Streams.processStreams(match) - CustomMatchGroupInput._getLinks(match) - CustomMatchGroupInput._getVod(match) - CustomMatchGroupInput._getExtraData(match) + match.links = CustomMatchGroupInput._getLinks(match) + + match.games = games + match.opponents = opponents + + match.extradata = CustomMatchGroupInput._getExtraData(match) + return match end ---@param match table -function CustomMatchGroupInput._getTournamentVars(match) - match = MatchGroupInput.getCommonTournamentVars(match) +---@param opponentIndex integer +---@param options readOpponentOptions +---@return table? +function CustomMatchGroupInput.readOpponent(match, opponentIndex, options) + options = options or {} + local opponentInput = Json.parseIfString(Table.extract(match, 'opponent' .. opponentIndex)) + if not opponentInput then + return opponentIndex <= 2 and MatchGroupInputUtil.mergeRecordWithOpponent({}, Opponent.blank()) or nil + end + + --- or Opponent.blank() is only needed because readOpponentArg can return nil for team opponents + local opponent = Opponent.readOpponentArgs(opponentInput) or Opponent.blank() + if Opponent.isBye(opponent) then + local byeOpponent = Opponent.blank() + byeOpponent.name = 'BYE' + return MatchGroupInputUtil.mergeRecordWithOpponent({}, byeOpponent) + end + + ---@type number|string? + local resolveDate = match.timestamp + -- If date is default date, resolve using tournament dates instead + -- default date indicates that the match is missing a date + -- In order to get correct child team template, we will use an approximately date and not the default date + if resolveDate == DateExt.defaultTimestamp then + resolveDate = DateExt.getContextualDate() + end + + Opponent.resolve(opponent, resolveDate, {syncPlayer = true}) + + local substitutions + if opponent.type == Opponent.team then + local manualPlayersInput = MatchGroupInputUtil.extractManualPlayersInput(match, opponentIndex, opponentInput) + substitutions = manualPlayersInput.substitutions + -- Change compared to commons MatchGroupInputUtil.readOpponent + local template = mw.ext.TeamTemplate.raw(opponent.template or '') or {} + opponent.players = MatchGroupInputUtil.readPlayersOfTeam( + template.page or '', + manualPlayersInput, + options, + {timestamp = match.timestamp, timezoneOffset = match.timezoneOffset} + ) + end + + Array.forEach(opponent.players or {}, function(player) + player.pageName = Page.pageifyLink(player.pageName) + end) + + local record = MatchGroupInputUtil.mergeRecordWithOpponent(opponentInput, opponent, substitutions) + + -- no need to pagify non opponent names as for literals it is irrelevant + -- and for party opponents it comes down to pagifying player names + if options.pagifyTeamNames and opponent.type == Opponent.team then + record.name = Page.pageifyLink(record.name) + end + + return record +end + +---@param match table +---@param opponents table[] +---@return table[] +function CustomMatchGroupInput.extractMaps(match, opponents) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = {} + map.map, map.extradata.displayname = CustomMatchGroupInput._getMapName(map, match.mapsInfo) + map.extradata.mapmode = Table.extract(map, 'mode') + + Table.mergeInto(map, MatchGroupInputUtil.getTournamentContext(map, match)) + + map.opponents = CustomMatchGroupInput.processPlayerMapData(map, opponents) + + map.finished = MatchGroupInputUtil.mapIsFinished(map) + local opponentInfo = Array.map(opponents, function(_, opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }, CustomMatchGroupInput.calculateMapScore(map.winner, map.finished)) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) + end - match = CustomMatchGroupInput._getMapsAndGame(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof')) - match.mode = Opponent.toLegacyMode(match.opponent1.type, match.opponent2.type) - match.headtohead = Logic.emptyOr(match.headtohead, Variables.varDefault('tournament_headtohead')) + table.insert(maps, map) + match[key] = nil + end - Variables.varDefine('bestof', match.bestof) + return maps +end + +---@param bestofInput string|integer? +---@return integer? +function CustomMatchGroupInput.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) or tonumber(Variables.varDefault('bestof')) + + if bestof then + Variables.varDefine('bestof', bestof) + end + + return bestof end ---@param match table ----@return table +---@return string?, table? function CustomMatchGroupInput._getMapsAndGame(match) - match.mapsInfo = Json.parse(Variables.varDefault('tournament_maps')) + local mapsInfo = Json.parseIfString(Variables.varDefault('tournament_maps')) - if Logic.isNotEmpty(match.mapsInfo) and match.game then - return match + if Logic.isNotEmpty(mapsInfo) and match.game then + return match.game, mapsInfo end - -- likely in preview w/o Infobox/HDB. Fetch from LPDB + -- likely in section preview, fetch from LPDB local title = mw.title.getCurrentTitle() local pages = { title.text:gsub(' ', '_'), @@ -86,446 +221,126 @@ function CustomMatchGroupInput._getMapsAndGame(match) Variables.varDefine('tournament_game', data.game) Variables.varDefine('tournament_maps', data.maps) - match.game = match.game or data.game - match.mapsInfo = Logic.emptyOr(match.mapsInfo, (Json.parse(data.maps))) - - return match -end - ----@param match table -function CustomMatchGroupInput._updateFinished(match) - match.finished = Logic.nilOr(Logic.readBoolOrNil(match.finished), Logic.isNotEmpty(match.winner)) - if match.finished or match.timestamp == DateExt.defaultTimestamp then - return - end - - -- Match is automatically marked finished upon page edit after a - -- certain amount of time (depending on whether the date is exact) - local currentUnixTime = os.time(os.date('!*t') --[[@as osdateparam]]) - local threshold = match.dateexact and 30800 or 86400 - match.finished = match.timestamp + threshold < currentUnixTime + return match.game or data.game, Logic.emptyOr(mapsInfo, (Json.parse(data.maps))) end ---@param match table function CustomMatchGroupInput._getLinks(match) - match.links = {} + local links = {} + match.civdraft1 = match.civdraft1 or match.civdraft for key, value in Table.iter.pairsByPrefix(match, 'civdraft') do - match.links[key] = 'https://aoe2cm.net/draft/' .. value + links[key] = 'https://aoe2cm.net/draft/' .. value end + match.mapdraft1 = match.mapdraft1 or match.mapdraft for key, value in Table.iter.pairsByPrefix(match, 'mapdraft') do - match.links[key] = 'https://aoe2cm.net/draft/' .. value + links[key] = 'https://aoe2cm.net/draft/' .. value end -end ----@param match table -function CustomMatchGroupInput._getVod(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod) + return links end ----@param match table -function CustomMatchGroupInput._getExtraData(match) - match.extradata = { - headtohead = match.headtohead, - civdraft = match.civdraft, - mapdraft = match.mapdraft, - casters = MatchGroupInput.readCasters(match, {noSort = true}), - } -end - ----@param match table -function CustomMatchGroupInput._processMaps(match) - for _, _, mapIndex in Table.iter.pairsByPrefix(match, 'map') do - CustomMatchGroupInput._mapInput(match, mapIndex) - end -end - ----@param match table -function CustomMatchGroupInput._calculateWinner(match) - local bestof = match.bestof or DEFAULT_BESTOF - local numberOfOpponents = 0 - - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local opponent = match['opponent' .. opponentIndex] - if Logic.isEmpty(opponent) then - break - end - - numberOfOpponents = numberOfOpponents + 1 - - if Logic.isNotEmpty(match.walkover) then - if Logic.isNumeric(match.walkover) then - local walkover = tonumber(match.walkover) - if walkover == opponentIndex then - match.winner = opponentIndex - match.walkover = 'FF' - opponent.status = 'W' - elseif walkover == 0 then - match.winner = 0 - match.walkover = 'FF' - opponent.status = 'FF' - else - local score = string.upper(opponent.score or '') - opponent.status = CONVERT_STATUS_INPUT[score] or 'FF' - end - elseif Table.includes(ALLOWED_STATUSES, string.upper(match.walkover)) then - if tonumber(match.winner or 0) == opponentIndex then - opponent.status = 'W' - else - opponent.status = CONVERT_STATUS_INPUT[string.upper(match.walkover)] or 'L' - end - else - local score = string.upper(opponent.score or '') - opponent.status = CONVERT_STATUS_INPUT[score] or 'L' - match.walkover = 'L' - end - opponent.score = -1 - match.finished = true - match.resulttype = 'default' - elseif CONVERT_STATUS_INPUT[string.upper(opponent.score or '')] then - if string.upper(opponent.score) == 'W' then - match.winner = opponentIndex - match.finished = true - opponent.score = -1 - opponent.status = 'W' - else - local score = string.upper(opponent.score) - match.finished = true - match.walkover = CONVERT_STATUS_INPUT[score] - opponent.status = CONVERT_STATUS_INPUT[score] - opponent.score = -1 - end - match.resulttype = 'default' - else - opponent.status = 'S' - opponent.score = tonumber(opponent.score) or tonumber(opponent.autoscore) or -1 - if opponent.score > bestof / 2 then - match.finished = Logic.emptyOr(match.finished, true) - match.winner = tonumber(match.winner) or opponentIndex - end - end - end - - CustomMatchGroupInput._determineWinnerIfMissing(match) - - for opponentIndex = 1, numberOfOpponents do - local opponent = match['opponent' .. opponentIndex] - if match.winner == 'draw' or tonumber(match.winner) == 0 or - (match.opponent1.score == bestof / 2 and match.opponent1.score == match.opponent2.score) then - match.finished = true - match.winner = 0 - match.resulttype = 'draw' - end - - if tonumber(match.winner) == opponentIndex or - match.resulttype == 'draw' then - opponent.placement = 1 - elseif Logic.isNumeric(match.winner) then - opponent.placement = 2 - end - end -end - ----@param match table -function CustomMatchGroupInput._determineWinnerIfMissing(match) - if not Logic.readBool(match.finished) or Logic.isNotEmpty(match.winner) then - return - end - - local scores = Array.mapIndexes(function(opponentIndex) - local opponent = match['opponent' .. opponentIndex] - if not opponent then - return nil - end - return match['opponent' .. opponentIndex].score or -1 - end - ) - local maxScore = math.max(unpack(scores) or 0) - -- if we have a positive score and the match is finished we also have a winner - if maxScore > 0 then - local maxIndexFound = false - for opponentIndex, score in pairs(scores) do - if maxIndexFound and score == maxScore then - match.winner = 0 - break - elseif score == maxScore then - maxIndexFound = true - match.winner = opponentIndex - end - end +---@param maps table[] +---@return fun(opponentIndex: integer): integer +function CustomMatchGroupInput.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end end ---@param match table ---@return table -function CustomMatchGroupInput._getOpponents(match) - -- read opponents and ignore empty ones - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if Logic.isNotEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - end - match['opponent' .. opponentIndex] = opponent - - if opponent.type == Opponent.team and Logic.isNotEmpty(opponent.template) then - local template = mw.ext.TeamTemplate.raw(opponent.template) - if template then - MatchGroupInput.readPlayersOfTeam(match, opponentIndex, template.page, { - resolveRedirect = true, - applyUnderScores = true, - maxNumPlayers = MAX_NUM_PLAYERS, - }) - end - end - end - - return match -end - ----@param record table ----@param timestamp number -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) or Opponent.blank() - - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = DateExt.getContextualDateOrNow() - end - - Opponent.resolve(opponent, teamTemplateDate, {syncPlayer = true}) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end - ----@param match table ----@param mapIndex integer -function CustomMatchGroupInput._mapInput(match, mapIndex) - local map = Json.parseIfString(match['map' .. mapIndex]) - if String.isNotEmpty(map.map) and map.map ~= 'TBD' then - if Logic.isNotEmpty(match.mapsInfo) then - local info = Array.find(match.mapsInfo, function(m) - return m.name == map.map or m.link == map.map - end) - if info then - map.map = info.link - map.mapDisplayName = info.name - end - else - map.mapDisplayName = map.map - map.map = mw.ext.TeamLiquidIntegration.resolve_redirect(map.map or '') - end - end - - -- set initial extradata for maps - map.extradata = { - comment = map.comment, - header = map.header, - displayname = map.mapDisplayName, - mapmode = map.mode +function CustomMatchGroupInput._getExtraData(match) + return { + headtohead = Logic.emptyOr(match.headtohead, Variables.varDefault('tournament_headtohead')), + casters = MatchGroupInputUtil.readCasters(match, {noSort = true}), } - map.game = match.game - map.mode = match.mode - - -- determine score, resulttype, walkover and winner - map = CustomMatchGroupInput._mapWinnerProcessing(map) - - -- Init score if match started and map info is present - if not match.opponent1.autoscore and not match.opponent2.autoscore - and map.map and map.map ~= 'TBD' - and match.timestamp < os.time(os.date('!*t') --[[@as osdateparam]]) - and String.isNotEmpty(map.civs1) and String.isNotEmpty(map.civs2) then - match.opponent1.autoscore = 0 - match.opponent2.autoscore = 0 - end - - if Logic.isEmpty(map.resulttype) and map.scores[1] and map.scores[2] then - match.opponent1.autoscore = (match.opponent1.autoscore or 0) + map.scores[1] - match.opponent2.autoscore = (match.opponent2.autoscore or 0) + map.scores[2] - end - - -- get participants data for the map + get map mode + winnerfaction and loserfaction - --(w/l faction stuff only for 1v1 maps) - CustomMatchGroupInput.processPlayerMapData(map, match, 2) - - match['map' .. mapIndex] = map end ---@param map table ----@return table -function CustomMatchGroupInput._mapWinnerProcessing(map) - map.scores = {} - local hasManualScores = false - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] - local obj = {} - if Logic.isNotEmpty(score) then - hasManualScores = true - score = CONVERT_STATUS_INPUT[score] or score - if Logic.isNumeric(score) then - obj.status = 'S' - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - - local winnerInput = tonumber(map.winner) - if Logic.isNotEmpty(map.walkover) then - local walkoverInput = tonumber(map.walkover) - if walkoverInput == 1 or walkoverInput == 2 or walkoverInput == 0 then - map.winner = walkoverInput - end - map.walkover = Table.includes(ALLOWED_STATUSES, map.walkover) and map.walkover or 'L' - map.scores = {-1, -1} - map.resulttype = 'default' - - return map - end - - if hasManualScores then - for scoreIndex, _ in Table.iter.spairs(indexedScores, CustomMatchGroupInput._placementSortFunction) do - if not tonumber(map.winner) then - map.winner = scoreIndex - else - break - end - end - - return map +---@param mapsInfo {name: string, link: string}[]? +---@return string? +---@return string? +function CustomMatchGroupInput._getMapName(map, mapsInfo) + if String.isEmpty(map.map) or map.map == 'TBD' then + return end - - if map.winner == 'skip' then - map.scores = {-1, -1} - map.resulttype = 'np' - elseif winnerInput == 1 then - map.scores = {1, 0} - elseif winnerInput == 2 then - map.scores = {0, 1} - elseif winnerInput == 0 or map.winner == 'draw' then - map.scores = {0.5, 0.5} - map.resulttype = 'draw' + if Logic.isEmpty(mapsInfo) then + return mw.ext.TeamLiquidIntegration.resolve_redirect(map.map or ''), map.map end - - return map + ---@cast mapsInfo -nil + local info = Array.find(mapsInfo, function(m) + return m.name == map.map or m.link == map.map + end) or {} + return info.link, info.name end ---@param map table ----@param match table ----@param numberOfOpponents integer -function CustomMatchGroupInput.processPlayerMapData(map, match, numberOfOpponents) - local participants = {} - for opponentIndex = 1, numberOfOpponents do - local opponent = match['opponent' .. opponentIndex] - if Opponent.typeIsParty(opponent.type) then - CustomMatchGroupInput._processPartyMapData(opponent.match2players, map, opponentIndex, participants) - elseif opponent.type == Opponent.team then - CustomMatchGroupInput._processTeamMapData(opponent.match2players, map, opponentIndex, participants) - end - end - - map.participants = participants -end - ----@param players table[] ----@param map table ----@param opponentIndex integer ----@param participants table ----@return table -function CustomMatchGroupInput._processPartyMapData(players, map, opponentIndex, participants) - local civs = Array.parseCommaSeparatedString(map['civs' .. opponentIndex]) - - for playerIndex, player in pairs(players) do - local civ = Logic.emptyOr(civs[playerIndex], Faction.defaultFaction) - civ = Faction.read(civ, {game = Game.abbreviation{game = map.game}:lower()}) - - participants[opponentIndex .. '_' .. playerIndex] = { - civ = civ, - player = player.name, - } - end - - return participants +---@param opponents table[] +---@return {players: table[]}[] +function CustomMatchGroupInput.processPlayerMapData(map, opponents) + return Array.map(opponents, function(opponent, opponentIndex) + return {players = CustomMatchGroupInput._participants( + opponent.match2players, + map, + opponentIndex, + opponent.type + )} + end) end ---@param opponentPlayers table[] ---@param map table ---@param opponentIndex integer ----@param participants table ----@return table -function CustomMatchGroupInput._processTeamMapData(opponentPlayers, map, opponentIndex, participants) - local players = Array.parseCommaSeparatedString(map['players' .. opponentIndex]) +---@param opponentType OpponentType +---@return {civ: string?, flag: string?, displayName: string?, pageName: string?}[] +function CustomMatchGroupInput._participants(opponentPlayers, map, opponentIndex, opponentType) + local players + if opponentType == Opponent.team then + players = Array.parseCommaSeparatedString(map['players' .. opponentIndex]) + else + players = Array.map(opponentPlayers, Operator.property('name')) + end local civs = Array.parseCommaSeparatedString(map['civs' .. opponentIndex]) - local function findPlayer(name) - return Table.filter(opponentPlayers or {}, function(player) - return player.displayName == name or player.pageName == name - end)[1] or {pageName = name, displayName = name} - end + local participants, unattachedParticipants = MatchGroupInputUtil.parseParticipants( + opponentPlayers, + players, + function(playerIndex) + local player = players[playerIndex] + return player and {name = player} or nil + end, + function(playerIndex, playerIdData, playerInputData) + local civ = Logic.emptyOr(civs[playerIndex], Faction.defaultFaction) + civ = Faction.read(civ, {game = Game.abbreviation{game = map.game}:lower()}) + return { + civ = civ, + displayName = playerIdData.displayname or playerInputData.name, + pageName = playerIdData.name or playerInputData.name, + flag = playerIdData.flag, + index = playerIndex, + } + end + ) + Array.forEach(unattachedParticipants, function(participant) + table.insert(participants, participant) + end) - for playerIndex, player in pairs(players) do - local civ = Logic.emptyOr(civs[playerIndex], Faction.defaultFaction) - civ = Faction.read(civ, {game = Game.abbreviation{game = map.game}:lower()}) - local playerData = findPlayer(player) - - participants[opponentIndex .. '_' .. playerIndex] = { - civ = civ, - displayName = playerData.displayName, - pageName = playerData.pageName, - flag = playerData.flag, - } - end return participants end --- function to sort out winner/placements ----@param tbl table ----@param key1 string ----@param key2 string ----@return boolean -function CustomMatchGroupInput._placementSortFunction(tbl, key1, key2) - local opponent1 = tbl[key1] - local opponent2 = tbl[key2] - local opponent1Norm = opponent1.status == 'S' - local opponent2Norm = opponent2.status == 'S' - if opponent1Norm then - if opponent2Norm then - return tonumber(opponent1.score) > tonumber(opponent2.score) - else - return true - end - else - if opponent2Norm then - return false - elseif opponent1.status == 'W' then - return true - elseif Table.includes(DEFAULT_LOSS_STATUSES, opponent1.status) then - return false - elseif opponent2.status == 'W' then - return false - elseif Table.includes(DEFAULT_LOSS_STATUSES, opponent2.status) then - return true - else - return true +---@param winnerInput string|integer|nil +---@param finished boolean +---@return fun(opponentIndex: integer): integer? +function CustomMatchGroupInput.calculateMapScore(winnerInput, finished) + local winner = tonumber(winnerInput) + return function(opponentIndex) + -- TODO Better to check if map has started, rather than finished, for a more correct handling + if not winner and not finished then + return end + return winner == opponentIndex and 1 or 0 end end diff --git a/components/match2/wikis/ageofempires/match_legacy.lua b/components/match2/wikis/ageofempires/match_legacy.lua index 880d1ad59c7..d60f5225dd4 100644 --- a/components/match2/wikis/ageofempires/match_legacy.lua +++ b/components/match2/wikis/ageofempires/match_legacy.lua @@ -8,6 +8,7 @@ local MatchLegacy = {} +local Array = require('Module:Array') local Json = require('Module:Json') local Lua = require('Module:Lua') local String = require('Module:StringUtils') @@ -31,6 +32,7 @@ function MatchLegacy.storeGames(match, match2) for gameIndex, game2 in ipairs(match2.match2games or {}) do local game = Table.deepCopy(game2) local participants = Json.parseIfString(game2.participants) or {} + local opponents = Json.parseIfString(game2.opponents) or {} -- Extradata game.extradata = {} @@ -39,17 +41,16 @@ function MatchLegacy.storeGames(match, match2) game.extradata.tournament = match.tournament game.extradata.vodmatch = match.vod if game.mode == 'team' then - local function processOpponent(opponentIndex) - for _, player, playerId in Table.iter.pairsByPrefix(participants, opponentIndex .. '_') do - local prefix = 'o' .. opponentIndex .. 'p' .. playerId + Array.forEach(opponents, function(opponent, opponentIndex) + -- opponent.players can have gaps + for _, player in pairs(opponent.players) do + local prefix = 'o' .. opponentIndex .. 'p' .. player.index game.extradata[prefix] = player.pageName game.extradata[prefix .. 'faction'] = player.civ game.extradata[prefix .. 'name'] = player.displayname game.extradata[prefix .. 'flag'] = player.flag end - end - processOpponent(1) - processOpponent(2) + end) elseif game.mode == '1v1' then local player1 = participants['1_1'] or {} local player2 = participants['2_1'] or {} @@ -63,8 +64,8 @@ function MatchLegacy.storeGames(match, match2) (tonumber(game.winner) == 2 and game.extradata.opponent1civ) or (tonumber(game.winner) == 1 and game.extradata.opponent2civ) or '' - game.extradata.opponent1name = player1.player - game.extradata.opponent2name = player2.player + game.extradata.opponent1name = player1.displayName + game.extradata.opponent2name = player2.displayName end -- Other stuff game.opponent1 = match.opponent1 @@ -122,7 +123,7 @@ function MatchLegacy._convertParameters(match2) local opponentmatch2players = opponent.match2players or {} if opponent.type == Opponent.solo then local player = opponentmatch2players[1] or {} - match[prefix] = player.name:gsub(' ', '_') + match[prefix] = (player.name or 'TBD'):gsub(' ', '_') match[prefix .. 'score'] = (tonumber(opponent.score) or 0) > 0 and opponent.score or 0 match[prefix .. 'flag'] = player.flag match.extradata[prefix .. 'name'] = player.displayname diff --git a/components/match2/wikis/ageofempires/match_summary.lua b/components/match2/wikis/ageofempires/match_summary.lua index d3487051099..014f9a0ce25 100644 --- a/components/match2/wikis/ageofempires/match_summary.lua +++ b/components/match2/wikis/ageofempires/match_summary.lua @@ -8,12 +8,13 @@ local Array = require('Module:Array') local DateExt = require('Module:Date/Ext') -local Game = require('Module:Game') -local MapMode = require('Module:MapMode') local Faction = require('Module:Faction') +local Game = require('Module:Game') local Icon = require('Module:Icon') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local MapMode = require('Module:MapMode') +local Operator = require('Module:Operator') local Table = require('Module:Table') local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper') @@ -112,15 +113,19 @@ function CustomMatchSummary.addToFooter(match, footer) return footer end - local player1, player2 = string.gsub(match.opponents[1].name, ' ', '_'), + if not Opponent.isEmpty(match.opponents[1]) and not Opponent.isEmpty(match.opponents[2]) then + local player1, player2 = string.gsub(match.opponents[1].name, ' ', '_'), string.gsub(match.opponents[2].name, ' ', '_') - return footer:addElement( - '[[File:Match Info Stats.png|link=' .. - tostring(mw.uri.fullUrl('Special:RunQuery/Match_history')) .. - '?pfRunQueryFormName=Match+history&Head_to_head_query%5Bplayer%5D=' .. - player1 .. - '&Head_to_head_query%5Bopponent%5D=' .. player2 .. '&wpRunQuery=Run+query|Head-to-head statistics]]' - ) + footer:addElement( + '[[File:Match Info Stats.png|link=' .. + tostring(mw.uri.fullUrl('Special:RunQuery/Match_history')) .. + '?pfRunQueryFormName=Match+history&Head_to_head_query%5Bplayer%5D=' .. + player1 .. + '&Head_to_head_query%5Bopponent%5D=' .. player2 .. '&wpRunQuery=Run+query|Head-to-head statistics]]' + ) + end + + return footer end ---@param match MatchGroupUtilMatch @@ -160,8 +165,7 @@ function CustomMatchSummary._createGame(row, game, props) faction1 = CustomMatchSummary._createFactionIcon(CustomMatchSummary._getPlayerData(game, '1_1').civ, normGame) faction2 = CustomMatchSummary._createFactionIcon(CustomMatchSummary._getPlayerData(game, '2_1').civ, normGame) else - local function createParticipant(participantId, flipped) - local player = CustomMatchSummary._getPlayerData(game, participantId) + local function createParticipant(player, flipped) local playerNode = PlayerDisplay.BlockPlayer{player = player, flip = flipped} local factionNode = CustomMatchSummary._createFactionIcon(player.civ, normGame) return mw.html.create('div'):css('display', 'flex'):css('align-self', flipped and 'end' or 'start') @@ -171,9 +175,12 @@ function CustomMatchSummary._createGame(row, game, props) end local function createOpponentDisplay(opponentId) local display = mw.html.create('div'):css('display', 'flex'):css('flex-direction', 'column'):css('width', '35%') - for participantId in Table.iter.pairsByPrefix(game.participants, opponentId .. '_') do - display:node(createParticipant(participantId, opponentId == 1)) - end + Array.forEach( + Array.sortBy(game.opponents[opponentId].players, Operator.property('index')), + function(player) + display:node(createParticipant(player, opponentId == 1)) + end + ) return display end diff --git a/components/match2/wikis/apexlegends/get_match_group_copy_paste_wiki.lua b/components/match2/wikis/apexlegends/get_match_group_copy_paste_wiki.lua index 2d0cd0eae2d..cffff4da882 100644 --- a/components/match2/wikis/apexlegends/get_match_group_copy_paste_wiki.lua +++ b/components/match2/wikis/apexlegends/get_match_group_copy_paste_wiki.lua @@ -35,7 +35,7 @@ function WikiCopyPaste.getMatchCode(bestof, mode, index, opponents, args) '|p_kill=1 |p1=12 |p2=9 |p3=7 |p4=5 |p5=4 |p6=3 |p7=3 |p8=2 |p9=2 |p10=2 |p11=1 |p12=1 |p13=1 |p14=1 |p15=1', {INDENT .. '|twitch=|youtube='}, Array.map(Array.range(1, bestof), function(mapIndex) - return INDENT .. '|map' .. mapIndex .. '={{Map|date=|finished=|map=|vod=|stats=}}' + return INDENT .. '|map' .. mapIndex .. '={{Map|date=|finished=|map=|vod=}}' end), Array.map(Array.range(1, opponents), function(opponentIndex) return INDENT .. '|opponent' .. opponentIndex .. '=' .. WikiCopyPaste._getOpponent(mode, bestof) diff --git a/components/match2/wikis/apexlegends/match_group_input_custom.lua b/components/match2/wikis/apexlegends/match_group_input_custom.lua index e81bb12672f..e6ce590965e 100644 --- a/components/match2/wikis/apexlegends/match_group_input_custom.lua +++ b/components/match2/wikis/apexlegends/match_group_input_custom.lua @@ -7,43 +7,24 @@ -- local Array = require('Module:Array') -local DateExt = require('Module:Date/Ext') -local FnUtil = require('Module:FnUtil') local Json = require('Module:Json') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Operator = require('Module:Operator') local Streams = require('Module:Links/Stream') -local String = require('Module:StringUtils') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') - -local STATUS_SCORE = 'S' -local STATUS_DRAW = 'D' -local STATUS_DEFAULT_WIN = 'W' -local STATUS_FORFEIT = 'FF' -local STATUS_DISQUALIFIED = 'DQ' -local STATUS_DEFAULT_LOSS = 'L' -local ALLOWED_STATUSES = { - STATUS_DRAW, - STATUS_DEFAULT_WIN, - STATUS_FORFEIT, - STATUS_DISQUALIFIED, - STATUS_DEFAULT_LOSS, -} -local MAX_NUM_OPPONENTS = 60 -local MAX_NUM_PLAYERS = 3 +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') + local DEFAULT_MODE = 'team' -local NO_SCORE = -99 -local NP_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local NOT_PLAYED_SCORE = -1 -local SECONDS_UNTIL_FINISHED_EXACT = 30800 -local SECONDS_UNTIL_FINISHED_NOT_EXACT = 86400 -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) local DUMMY_MAP_NAME = 'null' -- Is set in Template:Map when |map= is empty. +local OPPONENT_CONFIG = { + resolveRedirect = true, + applyUnderScores = true, + maxNumPlayers = 3, +} local MatchFunctions = {} local MapFunctions = {} @@ -54,101 +35,165 @@ local CustomMatchGroupInput = {} ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - match = MatchFunctions.parseSetting(match) - -- Adjust map data, especially set participants data - match = MatchFunctions.adjustMapData(match) - match = MatchFunctions.getScoreFromMaps(match) - - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = MatchFunctions.getOpponents(match) - match = MatchFunctions.getTournamentVars(match) - match = MatchFunctions.getVodStuff(match) - match = MatchFunctions.getExtraData(match) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - return match -end + local settings = MatchFunctions.parseSetting(match) + + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) + + local games = MatchFunctions.extractMaps(match, opponents, settings.score) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(opponents, games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.extradata = opponent.extradata or {} + opponent.extradata.startingpoints = tonumber(opponent.pointmodifier) + opponent.placement = tonumber(opponent.placement) + + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) -CustomMatchGroupInput.processMap = FnUtil.identity - ----@param record table ----@param timestamp number -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() - - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + if match.finished then + match.resulttype = + MatchGroupInputUtil.isNotPlayed(winnerInput, finishedInput) + and MatchGroupInputUtil.RESULT_TYPE.NOT_PLAYED + or nil + match.walkover = nil + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + CustomMatchGroupInput.setPlacements(opponents) + MatchFunctions.setBgForOpponents(opponents, settings.status) end - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) + + match.stream = Streams.processStreams(match) + + match.games = games + match.opponents = opponents + + match.extradata = MatchFunctions.getExtraData(settings) + + return match end ----@param data table ----@param indexedScores table[] ----@return table +---@param match table +---@param opponents table[] +---@param scoreSettings table ---@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - if Table.includes(NP_STATUSES, data.finished) then - -- Map or Match wasn't played, set not played - data.resulttype = 'np' - data.finished = true - elseif Logic.readBool(data.finished) then - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, data.finished) - data.winner = data.winner or winner - end +function MatchFunctions.extractMaps(match, opponents, scoreSettings) + local maps = {} + for key, map, mapIndex in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + if map.map == DUMMY_MAP_NAME then + map.map = '' + end + + Table.mergeInto(map, MatchGroupInputUtil.readDate(map.date)) + map.finished = MatchGroupInputUtil.mapIsFinished(map) - --set it as finished if we have a winner - if Logic.isNotEmpty(data.winner) then - data.finished = true + local opponentInfo = Array.map(opponents, function(matchOpponent) + local opponentMapInput = Json.parseIfString(matchOpponent['m' .. mapIndex]) + return MapFunctions.makeMapOpponentDetails(opponentMapInput, scoreSettings) + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.isNotPlayed(winnerInput, finishedInput) + and MatchGroupInputUtil.RESULT_TYPE.NOT_PLAYED + or nil + map.walkover = nil + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) + end + + map.extradata = MapFunctions.getExtraData(map, opponentInfo) + + table.insert(maps, map) + match[key] = nil end - return data, indexedScores + return maps end +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(opponents, maps) + return function(opponentIndex) + return Array.reduce(Array.map(maps, function(map) + return map.scores[opponentIndex] or 0 + end), Operator.add, 0) + (opponents[opponentIndex].startingpoints or 0) + end +end + +---Warning mutates the placement field in each opponent ---@param opponents table[] ----@param winner string|number ----@param finished boolean ---@return table[] ----@return string|number -function CustomMatchGroupInput.setPlacement(opponents, winner, finished) - local lastScore = NO_SCORE - local lastPlacement = NO_SCORE - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and Logic.isEmpty(winner) and finished then - winner = scoreIndex +function CustomMatchGroupInput.setPlacements(opponents) + local usedPlacements = Array.map(opponents, function() + return 0 + end) + Array.forEach(opponents, function(opponent) + if opponent.placement then + usedPlacements[opponent.placement] = usedPlacements[opponent.placement] + 1 + end + end) + -- Spread out placements if there are duplicates placements + -- For example 2 placement at 4 means 5 is also taken and the next available is 6 + Array.forEach(usedPlacements, function(count, placement) + if count > 1 then + usedPlacements[placement+1] = usedPlacements[placement + 1] + (count - 1) + usedPlacements[placement] = 1 + end + end) + + local function findNextSlot(placement) + if usedPlacements[placement] == 0 then + return placement end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or counter - lastPlacement = counter - lastScore = score or NO_SCORE + return findNextSlot(placement + 1) + end + + local lastScore + local lastPlacement = 0 + for _, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.scoreSorter) do + if not opp.placement then + local thisPlacement = findNextSlot(lastPlacement) + usedPlacements[thisPlacement] = 1 + if lastScore and opp.score == lastScore then + opp.placement = lastPlacement + else + opp.placement = thisPlacement + lastPlacement = opp.placement + lastScore = opp.score + end end end - return opponents, winner + return opponents end ---@param tbl table ---@param key1 string|number ---@param key2 string|number ---@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score) or NO_SCORE - local value2 = tonumber(tbl[key2].score) or NO_SCORE +function CustomMatchGroupInput.scoreSorter(tbl, key1, key2) + local value1 = tonumber(tbl[key1].score) or -math.huge + local value2 = tonumber(tbl[key2].score) or -math.huge return value1 > value2 end @@ -156,51 +201,21 @@ end -- match related functions -- ---@param match table ----@return table -function MatchFunctions.adjustMapData(match) - local opponents = Array.mapIndexes(function(idx) return match['opponent' .. idx] end) - for key, map, mapIndex in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do - local scores - Table.mergeInto(map, MatchGroupInput.readDate(map.date)) - map = MapFunctions.getParticipants(map, opponents) - map = MapFunctions.getOpponentStats(map, opponents, mapIndex) - map, scores = MapFunctions.getScoresAndWinner(map, match.scoreSettings) - map = MapFunctions.getExtraData(map, scores) - - if map.map == DUMMY_MAP_NAME then - map.map = '' - end - - match[key] = map - end - - return match -end - ----@param match table ----@return table +---@return {score: table, status: table} function MatchFunctions.parseSetting(match) -- Score Settings - match.scoreSettings = { + local scoreSettings = { kill = tonumber(match.p_kill) or 1, matchPointThreadhold = tonumber(match.matchpoint), + placement = Array.mapIndexes(function(idx) + return match['opponent' .. idx] and (tonumber(match['p' .. idx]) or 0) or nil + end) } - Table.mergeInto(match.scoreSettings, Array.mapIndexes(function(idx) - return match['opponent' .. idx] and (tonumber(match['p' .. idx]) or 0) or nil - end)) - -- Up/Down colors - local function splitAndTrim(s, pattern) - if not s then - return {} - end - return Array.map(mw.text.split(s, pattern), String.trim) - end - - match.statusSettings = Array.flatMap(splitAndTrim(match.bg, ','), function (status) - local placements, color = unpack(splitAndTrim(status, '=')) - local pStart, pEnd = unpack(splitAndTrim(placements, '-')) + local statusSettings = Array.flatMap(Array.parseCommaSeparatedString(match.bg, ','), function (status) + local placements, color = unpack(Array.parseCommaSeparatedString(status, '=')) + local pStart, pEnd = unpack(Array.parseCommaSeparatedString(placements, '-')) local pStartNumber = tonumber(pStart) --[[@as integer]] local pEndNumber = tonumber(pEnd) or pStartNumber return Array.map(Array.range(pStartNumber, pEndNumber), function() @@ -208,140 +223,27 @@ function MatchFunctions.parseSetting(match) end) end) - return match -end - ---- Calculate the points based on the map results ----@param match table ----@return table -function MatchFunctions.getScoreFromMaps(match) - local newScores = {} - - local mapIndex = 1 - while match['map' .. mapIndex] do - for index = 1, MAX_NUM_OPPONENTS do - newScores[index] = (newScores[index] or 0) + (match['map' .. mapIndex].scores[index] or 0) - end - mapIndex = mapIndex + 1 - end - - for index = 1, MAX_NUM_OPPONENTS do - if match['opponent' .. index] and not match['opponent' .. index].score then - match['opponent' .. index].score = (newScores[index] or 0) + (match['opponent' .. index].pointmodifier or 0) - end - end - - return match -end - ----@param match table ----@return table -function MatchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - return MatchGroupInput.getCommonTournamentVars(match) -end - ----@param match table ----@return table -function MatchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - - match.links = { - stats = match.stats, + return { + score = scoreSettings, + status = statusSettings, } - - return match end ----@param match table +---@param settings table ---@return table -function MatchFunctions.getExtraData(match) - match.extradata = { - scoring = match.scoreSettings, - status = match.statusSettings, +function MatchFunctions.getExtraData(settings) + return { + scoring = settings.score, + status = settings.status, } - - return match -end - ----@param match table ----@return table -function MatchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if Logic.isNotEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - opponent.score = string.upper(opponent.score or '') - if Logic.isNumeric(opponent.score) then - opponent.score = tonumber(opponent.score) - opponent.status = STATUS_SCORE - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = NOT_PLAYED_SCORE - end - - -- get players from vars for teams - assert(Opponent.isType(opponent.type), 'Unsupported Opponent Type "' .. (opponent.type or '') .. '"') - if opponent.type == Opponent.team then - if Logic.isNotEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name, { - resolveRedirect = true, - applyUnderScores = true, - maxNumPlayers = MAX_NUM_PLAYERS, - }) - end - end - - opponent.extradata = opponent.extradata or {} - opponent.extradata.startingpoints = tonumber(opponent.pointmodifier) - - opponents[opponentIndex] = opponent - end - end - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and SECONDS_UNTIL_FINISHED_EXACT or SECONDS_UNTIL_FINISHED_NOT_EXACT - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if Logic.isNotEmpty(match.winner) or Logic.readBool(match.finished) then - match.finished = true - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - - if match.finished then - opponents = MatchFunctions.setBgForOpponents(opponents, match.statusSettings) - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - - return match end ---@param opponents table ---@param statusSettings table ----@return table function MatchFunctions.setBgForOpponents(opponents, statusSettings) Array.forEach(opponents, function(opponent) opponent.extradata.bg = statusSettings[opponent.placement] end) - return opponents end -- @@ -350,112 +252,47 @@ end -- Parse extradata information ---@param map table ----@param scores table[] +---@param opponents table[] ---@return table -function MapFunctions.getExtraData(map, scores) - map.extradata = { +function MapFunctions.getExtraData(map, opponents) + return { dateexact = map.dateexact, comment = map.comment, - opponents = scores, + opponents = opponents, } - - return map end --- Parse participant information ----@param map table ----@param opponents table[] ----@return table -function MapFunctions.getParticipants(map, opponents) - local participants = {} - for opponentIndex, opponent in ipairs(opponents) do - for playerIndex = 1, MAX_NUM_PLAYERS do - local player = Json.parseIfString(opponent['p'.. playerIndex]) - if player then - participants = MapFunctions.attachToParticipant( - player, - opponentIndex, - opponent.match2players, - participants - ) - end - end - end - - map.participants = participants - return map -end - ----@param player table ----@param opponentIndex integer ----@param players table[]? ----@param participants table +---Calculate Score and Winner of the map +---@param scoreDataInput table? +---@param scoreSettings table ---@return table -function MapFunctions.attachToParticipant(player, opponentIndex, players, participants) - player.player = mw.ext.TeamLiquidIntegration.resolve_redirect(player.player or ''):gsub(' ', '_') - for playerIndex, item in pairs(players or {}) do - if player.player == item.name then - participants[opponentIndex .. '_' .. playerIndex] = player - break - end +function MapFunctions.makeMapOpponentDetails(scoreDataInput, scoreSettings) + if not scoreDataInput then + return {} end - return participants -end + local scoreBreakdown = {} ----@param map table ----@param opponents table[] ----@param idx integer ----@return table -function MapFunctions.getOpponentStats(map, opponents, idx) - for oppIdx, opponent in pairs(opponents) do - map['t'.. oppIdx ..'data'] = Json.parseIfString(opponent['m' .. idx]) + local placement, kills = tonumber(scoreDataInput[1]), tonumber(scoreDataInput[2]) + if placement and kills then + scoreBreakdown.placePoints = scoreSettings.placement[placement] or 0 + scoreBreakdown.killPoints = kills * scoreSettings.kill + scoreBreakdown.kills = kills + scoreBreakdown.totalPoints = scoreBreakdown.placePoints + scoreBreakdown.killPoints end + local opponent = { + status = MatchGroupInputUtil.STATUS.SCORE, + scoreBreakdown = scoreBreakdown, + placement = placement, + score = scoreBreakdown.totalPoints, + } - return map -end - ----Calculate Score and Winner of the map ----@param map table ----@param scoreSettings table ----@return table ----@return table -function MapFunctions.getScoresAndWinner(map, scoreSettings) - map.scores = {} - local indexedScores = {} - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local opponentData = map['t' .. opponentIndex .. 'data'] - if not opponentData then - break - end - local scoreBreakdown = {} - - local placement, kills = tonumber(opponentData[1]), tonumber(opponentData[2]) - if placement and kills then - scoreBreakdown.placePoints = scoreSettings[placement] or 0 - scoreBreakdown.killPoints = kills * scoreSettings.kill - scoreBreakdown.kills = kills - scoreBreakdown.totalPoints = scoreBreakdown.placePoints + scoreBreakdown.killPoints - end - local opponent = { - status = STATUS_SCORE, - scoreBreakdown = scoreBreakdown, - placement = placement, - score = scoreBreakdown.totalPoints, - } - - if opponentData[1] == '-' then - opponent.placement = NO_SCORE - end - - table.insert(map.scores, opponent.score or 0) - indexedScores[opponentIndex] = opponent + if scoreDataInput[1] == '-' then + opponent.status = MatchGroupInputUtil.STATUS.FORFIET + opponent.score = 0 end - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map, indexedScores + return opponent end return CustomMatchGroupInput diff --git a/components/match2/wikis/apexlegends/match_summary.lua b/components/match2/wikis/apexlegends/match_summary.lua index c5d3a1d3e3b..3d2a683381a 100644 --- a/components/match2/wikis/apexlegends/match_summary.lua +++ b/components/match2/wikis/apexlegends/match_summary.lua @@ -31,8 +31,6 @@ local OpponentDisplay = OpponentLibraries.OpponentDisplay ---@field scoringTable {kill: number, placement: {rangeStart: integer, rangeEnd: integer, score: number}[]} ---@field stream table -local NO_PLACEMENT = -99 - local PHASE_ICONS = { finished = {iconName = 'concluded', color = 'icon--green'}, ongoing = {iconName = 'live', color = 'icon--red'}, @@ -166,6 +164,12 @@ local MATCH_STANDING_COLUMNS = { return PLACEMENT_BG[opponent.placement] end, value = function (opponent) + local placementDisplay + if opponent.status and opponent.status ~= 'S' then + placementDisplay = '-' + else + placementDisplay = CustomMatchSummary._displayRank(opponent.placement) + end local icon, color = CustomMatchSummary._getTrophy(opponent.placement) return mw.html.create() :tag('i') @@ -174,7 +178,7 @@ local MATCH_STANDING_COLUMNS = { :addClass(color) :done() :tag('span'):addClass('panel-table__cell-game__text') - :wikitext(CustomMatchSummary._displayRank(opponent.placement)):done() + :wikitext(placementDisplay):done() end, }, }, @@ -204,7 +208,7 @@ local GAME_STANDINGS_COLUMNS = { }, sortVal = { value = function (opponent, idx) - if opponent.placement == -1 or opponent.placement == NO_PLACEMENT then + if opponent.placement == -1 or opponent.status ~= 'S' then return idx end return opponent.placement @@ -213,10 +217,16 @@ local GAME_STANDINGS_COLUMNS = { row = { value = function (opponent, idx) local place = opponent.placement ~= -1 and opponent.placement or idx + local placementDisplay + if opponent.status and opponent.status ~= 'S' then + placementDisplay = '-' + else + placementDisplay = CustomMatchSummary._displayRank(place) + end local icon, color = CustomMatchSummary._getTrophy(place) return mw.html.create() :tag('i'):addClass('panel-table__cell-icon'):addClass(icon):addClass(color):done() - :tag('span'):wikitext(CustomMatchSummary._displayRank(place)):done() + :tag('span'):wikitext(CustomMatchSummary._displayRank(placementDisplay)):done() end, }, }, @@ -359,20 +369,19 @@ function CustomMatchSummary._opponents(match) end local placementSortFunction = function(opponent1, opponent2) - if opponent1.placement == opponent2.placement or not opponent1.placement or not opponent2.placement then - if opponent1.score and opponent2.score and opponent1.score ~= opponent2.score then - return opponent1.score > opponent2.score - else - return (opponent1.name or '') < (opponent2.name or '') - end + if opponent1.placement and opponent2.placement and opponent1.placement ~= opponent2.placement then + return opponent1.placement < opponent2.placement end - if opponent1.placement == NO_PLACEMENT then + if opponent1.status ~= 'S' and opponent2.status == 'S' then return false end - if opponent2.placement == NO_PLACEMENT then + if opponent2.status ~= 'S' and opponent1.status == 'S' then return true end - return opponent1.placement < opponent2.placement + if opponent1.score and opponent2.score and opponent1.score ~= opponent2.score then + return opponent1.score > opponent2.score + end + return (opponent1.name or '') < (opponent2.name or '') end -- Sort match level based on final placement & score @@ -391,11 +400,9 @@ end function CustomMatchSummary._createScoringData(match) local scoreSettings = match.extradata.scoring - local scoreKill = Table.extract(scoreSettings, 'kill') - local matchPointThreadhold = Table.extract(scoreSettings, 'matchPointThreadhold') local scorePlacement = {} - local points = Table.groupBy(scoreSettings, function (_, value) + local points = Table.groupBy(scoreSettings.placement, function (_, value) return value end) @@ -411,9 +418,9 @@ function CustomMatchSummary._createScoringData(match) end return { - kill = scoreKill, + kill = scoreSettings.kill, placement = scorePlacement, - matchPointThreadhold = matchPointThreadhold, + matchPointThreadhold = scoreSettings.matchPointThreadhold, } end @@ -797,10 +804,6 @@ end ---@param placementEnd string|number|nil ---@return string function CustomMatchSummary._displayRank(placementStart, placementEnd) - if NO_PLACEMENT == placementStart then - return '-' - end - local places = {} if placementStart then diff --git a/components/match2/wikis/arenafps/legacy/legacy_bracket_match_summary.lua b/components/match2/wikis/arenafps/legacy/legacy_bracket_match_summary.lua new file mode 100644 index 00000000000..6aef9ef2905 --- /dev/null +++ b/components/match2/wikis/arenafps/legacy/legacy_bracket_match_summary.lua @@ -0,0 +1,60 @@ +--- +-- @Liquipedia +-- wiki=arenafps +-- page=Module:LegacyBracketMatchSummary +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Arguments = require('Module:Arguments') +local Array = require('Module:Array') +local Logic = require('Module:Logic') +local Json = require('Module:Json') +local Table = require('Module:Table') + +local DRAW = 'draw' +local SKIP = 'skip' + +local LegacyBracketMatchSummary = {} + +---@param args table +---@return table +function LegacyBracketMatchSummary._handleMaps(args) + Array.mapIndexes(function(index) + local prefix = 'map' .. index + local map = args[prefix] + local winner = Table.extract(args, prefix .. 'win') + local score = Table.extract(args, prefix .. 'score') + if Logic.isEmpty(map) and Logic.isEmpty(winner) then + return false + end + + if Logic.isNotEmpty(score) then + local splitedScore = Array.parseCommaSeparatedString(score, '-') + args[prefix .. 'score1'] = mw.text.decode(splitedScore[1]) + args[prefix .. 'score2'] = mw.text.decode(splitedScore[2]) + end + + args[prefix .. 'finished'] = (winner == SKIP and SKIP) or + (not Logic.isEmpty(winner) and 'true') or 'false' + + if Logic.isNumeric(winner) or winner == DRAW then + args[prefix .. 'winner'] = winner == DRAW and 0 or winner + end + return true + end) + + return args +end + +-- invoked by BracketMatchSummary +---@param frame Frame +---@return string +function LegacyBracketMatchSummary.convert(frame) + local args = Arguments.getArgs(frame) + args = LegacyBracketMatchSummary._handleMaps(args) + + return Json.stringify(args) +end + +return LegacyBracketMatchSummary diff --git a/components/match2/wikis/arenafps/legacy/match_group_legacy_default.lua b/components/match2/wikis/arenafps/legacy/match_group_legacy_default.lua new file mode 100644 index 00000000000..be2eea3cc9f --- /dev/null +++ b/components/match2/wikis/arenafps/legacy/match_group_legacy_default.lua @@ -0,0 +1,36 @@ +--- +-- @Liquipedia +-- wiki=arenafps +-- page=Module:MatchGroup/Legacy/Default +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Class = require('Module:Class') +local Lua = require('Module:Lua') + +local MatchGroupLegacy = Lua.import('Module:MatchGroup/Legacy') + + +---@class ArenafpsMatchGroupLegacyDefault: MatchGroupLegacy +local MatchGroupLegacyDefault = Class.new(MatchGroupLegacy) + +---@return table +function MatchGroupLegacyDefault:getMap() + return { + ['$notEmpty$'] = 'map$1$', + map = 'map$1$', + finished = 'map$1$finished', + winner = 'map$1$winner', + score1 = 'map$1$score1', + score2 = 'map$1$score2', + vod = 'vodgame$1$' + } +end + +---@param frame Frame +function MatchGroupLegacyDefault.run(frame) + return MatchGroupLegacyDefault(frame):build() +end + +return MatchGroupLegacyDefault diff --git a/components/match2/wikis/arenafps/legacy/match_maps_legacy.lua b/components/match2/wikis/arenafps/legacy/match_maps_legacy.lua new file mode 100644 index 00000000000..9ab586e0be0 --- /dev/null +++ b/components/match2/wikis/arenafps/legacy/match_maps_legacy.lua @@ -0,0 +1,156 @@ +--- +-- @Liquipedia +-- wiki=arenafps +-- page=Module:MatchMaps/Legacy +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Arguments = require('Module:Arguments') +local Array = require('Module:Array') +local Json = require('Module:Json') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') +local MatchGroup = require('Module:MatchGroup') +local PageVariableNamespace = require('Module:PageVariableNamespace') +local Table = require('Module:Table') + +local MatchGroupBase = Lua.import('Module:MatchGroup/Base') +local MatchSubobjects = Lua.import('Module:Match/Subobjects') + +local globalVars = PageVariableNamespace() + +local OpponentLibraries = require('Module:OpponentLibraries') +local Opponent = OpponentLibraries.Opponent + +local MAX_NUMBER_OF_OPPONENTS = 2 +local MAX_NUM_MAPS = 9 + +local MatchMapsLegacy = {} + +---@param args table +---@return table +function MatchMapsLegacy._mergeDetailsIntoArgs(args) + local details = Json.parseIfTable(Table.extract(args, 'details')) or {} + + return Table.merge(details, args, { + date = details.date or args.date, + dateheader = Logic.isNotEmpty(args.date), + header = Table.extract(args, 'header') + }) +end + +---@param matchArgs table +function MatchMapsLegacy._readOpponents(matchArgs) + local matchMapsType + Array.forEach(Array.range(1, MAX_NUMBER_OF_OPPONENTS), function (opponentIndex) + matchMapsType = matchMapsType or (matchArgs['player' .. opponentIndex] and Opponent.solo or + matchArgs['team' .. opponentIndex] and Opponent.team) or nil + matchArgs['opponent' .. opponentIndex] = { + score = Table.extract(matchArgs, 'score' .. opponentIndex) or + Table.extract(matchArgs, 'games' .. opponentIndex), + template = Table.extract(matchArgs, 'team' .. opponentIndex), + name = Table.extract(matchArgs, 'player' .. opponentIndex), + link = Table.extract(matchArgs, 'playerlink' .. opponentIndex), + flag = Table.extract(matchArgs, 'player' .. opponentIndex .. 'flag'), + } + end) + matchMapsType = matchMapsType or Opponent.literal + matchArgs['opponent1'].type = matchMapsType + matchArgs['opponent2'].type = matchMapsType +end + +---@param matchArgs table +function MatchMapsLegacy._readMaps(matchArgs) + Array.forEach(Array.range(1, MAX_NUM_MAPS), function(mapIndex) + local prefix = 'map' .. mapIndex + local mapArgs = { + map = Table.extract(matchArgs, prefix), + finished = Table.extract(matchArgs, prefix .. 'finished'), + winner = Table.extract(matchArgs, prefix .. 'winner'), + score1 = Table.extract(matchArgs, prefix .. 'score1'), + score2 = Table.extract(matchArgs, prefix .. 'score2'), + vod = Table.extract(matchArgs, 'vodgame' .. mapIndex) + } + matchArgs[prefix] = Logic.isNotEmpty(mapArgs) and MatchSubobjects.luaGetMap(mapArgs) or nil + end) +end + +-- invoked by Template:MatchMaps +---@param frame Frame +---@return string +function MatchMapsLegacy.convertMatch(frame) + local matchArgs = MatchMapsLegacy._mergeDetailsIntoArgs(Arguments.getArgs(frame)) + MatchMapsLegacy._readOpponents(matchArgs) + MatchMapsLegacy._readMaps(matchArgs) + matchArgs.winner = matchArgs.winner or Table.extract(matchArgs, 'win') + + return Json.stringify(matchArgs) +end + +---@param store boolean? +---@return boolean +function MatchMapsLegacy._shouldStore(store) + return Logic.nilOr( + Logic.readBoolOrNil(store), + not Logic.readBool(globalVars:get('disable_LPDB_storage')) + ) +end + +-- invoked by Template:LegacySingleMatch +---@param frame Frame +---@return Html +function MatchMapsLegacy.showmatch(frame) + local match = Json.parseIfString(MatchMapsLegacy.convertMatch(frame)) + local id = Table.extract(match, 'id') + assert(id, 'Missing id') + + local store = MatchMapsLegacy._shouldStore(Table.extract(match, 'store')) + MatchGroup.Bracket({ + 'Bracket/2', + isLegacy = true, + id = id, + hide = true, + store = store, + noDuplicateCheck = not store, + R1M1 = match + }) + + return MatchGroup.MatchByMatchId({ + id = MatchGroupBase.getBracketIdPrefix() .. id, + matchid = 'R1M1', + }) +end + +-- invoked by Template:LegacyMatchList +---@param frame Frame +---@return string +function MatchMapsLegacy.matchList(frame) + globalVars:set('islegacy', 'true') + local args = Arguments.getArgs(frame) + assert(args.id, 'Missing id') + local store = MatchMapsLegacy._shouldStore(args.store) + local hide = Logic.nilOr(Logic.readBoolOrNil(args.hide), true) + + Table.mergeInto(args, { + isLegacy = true, + store = store, + noDuplicateCheck = not store, + collapsed = hide, + attached = hide, + title = Logic.nilOr(Table.extract(args, 'title'), args[1]), + width = Table.extract(args, 'width') or '300px', + }) + args[1] = nil + + for matchKey, _, matchIndex in Table.iter.pairsByPrefix(args, 'match') do + local match = Json.parse(Table.extract(args, matchKey)) --[[@as table]] + args['M' .. matchIndex .. 'header'] = Table.extract(match, 'header') + args['M' .. matchIndex] = Json.stringify(match) + end + + globalVars:delete('islegacy') + return MatchGroup.MatchList(args) +end + +return MatchMapsLegacy diff --git a/components/match2/wikis/arenafps/match_group_input_custom.lua b/components/match2/wikis/arenafps/match_group_input_custom.lua index c88cca037cd..b261b433d68 100644 --- a/components/match2/wikis/arenafps/match_group_input_custom.lua +++ b/components/match2/wikis/arenafps/match_group_input_custom.lua @@ -7,30 +7,21 @@ -- local Array = require('Module:Array') -local DateExt = require('Module:Date/Ext') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Operator = require('Module:Operator') local Streams = require('Module:Links/Stream') -local String = require('Module:StringUtils') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L', 'D'} -local FINISHED_INDICATORS = {'skip', 'np', 'cancelled', 'canceled'} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_MAPS = 9 local DEFAULT_BESTOF = 3 -local NO_SCORE = -99 -local MATCH_BYE = 'bye' - -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_MODE = 'Duel' -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} @@ -39,148 +30,50 @@ local CustomMatchGroupInput = {} ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - -- Count number of maps, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) - - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - - return match -end - --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) - map = mapFunctions.getTournamentVars(map) - - return map -end - ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() - - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end - - Opponent.resolve(opponent, teamTemplateDate, {syncPlayer = true}) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end - ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if Table.includes(FINISHED_INDICATORS, data.finished) or Table.includes(FINISHED_INDICATORS, data.winner) then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = 'default' - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'default') - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end - end - - --set it as finished if we have a winner - if not Logic.isEmpty(data.winner) then - data.finished = true + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) + + local games = MatchFunctions.extractMaps(match, #opponents) + + match.bestof = MatchFunctions.getBestOf(match.bestof) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - return data, indexedScores -end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) ----@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ----@return table[] ----@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == 'default' then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local lastScore = NO_SCORE - local lastPlacement = NO_SCORE - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and (winner or '') == '' then - if finished then - winner = scoreIndex - end - end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or counter - lastPlacement = counter - lastScore = score or NO_SCORE - end - end - end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) - return opponents, winner -end + match.games = games + match.opponents = opponents ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score or NO_SCORE) or NO_SCORE - local value2 = tonumber(tbl[key2].score or NO_SCORE) or NO_SCORE - return value1 > value2 + return match end -- @@ -188,200 +81,87 @@ end -- ---@param match table ----@return table -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match -end +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) --- Calculate the match scores based on the map results (counting map wins) --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local opponentNumber = 0 - for index = 1, MAX_NUM_OPPONENTS do - if String.isEmpty(match['opponent' .. index]) then - break - end - opponentNumber = index - end - local newScores = {} - local foundScores = false - - for i = 1, MAX_NUM_MAPS do - if match['map'..i] then - local winner = tonumber(match['map'..i].winner) - foundScores = true - if winner and winner > 0 and winner <= opponentNumber then - newScores[winner] = (newScores[winner] or 0) + 1 - end - else - break + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - for index = 1, opponentNumber do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end + table.insert(maps, map) + match[key] = nil end - return match -end - ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'Duel')) - return MatchGroupInput.getCommonTournamentVars(match) -end - ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - - match.links = {} - local links = match.links - if match.preview then links.preview = match.preview end - if match.quakehistory then links.quakehistory = 'http://www.quakehistory.com/en/matches/' .. match.quakehistory end - if match.dbstats then links.dbstats = 'https://quakelife.ru/diabotical/stats/matches/?matches=' .. match.dbstats end - if match.qrindr then links.qrindr = 'https://qrindr.com/match/' .. match.qrindr end - if match.esl then links.esl = 'https://play.eslgaming.com/match/' .. match.esl end - if match.stats then links.stats = match.stats end - - return match + return maps end ----@param match table ----@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if Logic.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == Opponent.team and not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name) - end - end - end - - -- see if match should actually be finished if bestof limit was reached - if isScoreSet and not Logic.readBool(match.finished) then - local firstTo = math.ceil(match.bestof/2) - match.finished = Array.any(opponents, function(opponent) - return (tonumber(opponent.score) or 0) >= firstTo - end) - end - - -- check if match should actually be finished due to a non score status - if not Logic.readBool(match.finished) then - for _, opponent in pairs(opponents) do - if String.isNotEmpty(opponent.status) and opponent.status ~= 'S' then - match.finished = true - break - end - end - end +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end + if bestof then + Variables.varDefine('bestof', bestof) + return bestof end - -- apply placements and winner if finshed - if not Logic.isEmpty(match.winner) or Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF +end - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - return match end --- Get Playerdata for non-team opponents ----@param player table ----@return boolean -function CustomMatchGroupInput._playerIsBye(player) - return (player.name or ''):lower() == MATCH_BYE or (player.displayname or ''):lower() == MATCH_BYE +---@param match table +---@return table +function MatchFunctions.getLinks(match) + return { + preview = match.preview, + quakehistory = match.quakehistory and ('http://www.quakehistory.com/en/matches/' .. match.quakehistory) or nil, + dbstats = match.dbstats and ('https://quakelife.ru/diabotical/stats/matches/?matches=' .. match.dbstats) or nil, + qrindr = match.qrindr and ('https://qrindr.com/match/' .. match.qrindr) or nil, + esl = match.esl and ('https://play.eslgaming.com/match/' .. match.esl) or nil, + pf = match.pf and ('https://www.plusforward.net/quake/post/' .. match.pf) or nil, + stats = match.stats, + } end -- -- map related functions -- --- Parse extradata information ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { +function MapFunctions.getExtraData(map, opponentCount) + return { comment = map.comment, } - return map -end - --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - if Logic.isNumeric(score) then - obj.status = 'S' - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map -end - ----@param map table ----@return table -function mapFunctions.getTournamentVars(map) - map.mode = Logic.emptyOr(map.mode, Variables.varDefault('tournament_mode', 'team')) - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/arenafps/match_summary.lua b/components/match2/wikis/arenafps/match_summary.lua index 1f591585b4c..bfdaa81fa54 100644 --- a/components/match2/wikis/arenafps/match_summary.lua +++ b/components/match2/wikis/arenafps/match_summary.lua @@ -123,12 +123,12 @@ function CustomMatchSummary._createMapRow(game) local leftNode = mw.html.create('div') :addClass('brkts-popup-spaced') :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 1, Icons.CHECK)) - :node(CustomMatchSummary._gameScore(game, 1)) + :node(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) :css('width', '20%') local rightNode = mw.html.create('div') :addClass('brkts-popup-spaced') - :node(CustomMatchSummary._gameScore(game, 2)) + :node(DisplayHelper.MapScore(game.scores[2], 2, game.resultType, game.walkover, game.winner)) :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 2, Icons.CHECK)) :css('width', '20%') diff --git a/components/match2/wikis/battlerite/match_group_input_custom.lua b/components/match2/wikis/battlerite/match_group_input_custom.lua index f62ac04d8ca..712d0b903a5 100644 --- a/components/match2/wikis/battlerite/match_group_input_custom.lua +++ b/components/match2/wikis/battlerite/match_group_input_custom.lua @@ -6,43 +6,24 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- -local DateExt = require('Module:Date/Ext') -local Logic = require('Module:Logic') +local Array = require('Module:Array') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') -local Table = require('Module:Table') -local Variables = require('Module:Variables') +local Operator = require('Module:Operator') local Streams = require('Module:Links/Stream') +local Table = require('Module:Table') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') - -local STATUS_SCORE = 'S' -local STATUS_DRAW = 'D' -local STATUS_DEFAULT_WIN = 'W' -local STATUS_FORFEIT = 'FF' -local STATUS_DISQUALIFIED = 'DQ' -local STATUS_DEFAULT_LOSS = 'L' -local ALLOWED_STATUSES = { - STATUS_DRAW, - STATUS_DEFAULT_WIN, - STATUS_FORFEIT, - STATUS_DISQUALIFIED, - STATUS_DEFAULT_LOSS, -} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_PLAYERS = 15 -local NO_SCORE = -99 -local DUMMY_MAP = 'default' -local NP_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local DEFAULT_RESULT_TYPE = 'default' -local NOT_PLAYED_SCORE = -1 +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DUMMY_MAP = 'default' -- Is set in Template:Map when |map= is empty. +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, + maxNumPlayers = 15, +} -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} @@ -51,378 +32,143 @@ local CustomMatchGroupInput = {} ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getScoreFromMapWinners(match) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getLinks(match) - match = matchFunctions.getExtraData(match) - - -- Adjust map data, especially set participants data - match = matchFunctions.adjustMapData(match) - - return match -end - ----@param match table ----@return table -function matchFunctions.adjustMapData(match) - for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do - match[key] = mapFunctions.getExtraData(map) - end + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - return match -end + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - if map.map == DUMMY_MAP then - map.map = nil - end - map = mapFunctions.getScoresAndWinner(map) + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) - return map -end + local games = MatchFunctions.extractMaps(match, #opponents) + match.bestof = MatchGroupInputUtil.getBestOf(nil, games) + games = MatchFunctions.removeUnsetMaps(games) ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if - Table.includes(NP_STATUSES, data.finished) or - Table.includes(NP_STATUSES, data.winner) - then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif CustomMatchGroupInput.placementCheckSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = DEFAULT_RESULT_TYPE - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, DEFAULT_RESULT_TYPE) - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end - end - - -- set it as finished if we have a winner - if not Logic.isEmpty(data.winner) then - data.finished = true - end - - return data, indexedScores -end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) ----@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ----@return table[] ----@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == DEFAULT_RESULT_TYPE then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local lastScore = NO_SCORE - local lastPlacement = NO_SCORE - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and (winner or '') == '' then - if finished then - winner = scoreIndex - end - end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or counter - lastPlacement = counter - lastScore = score or NO_SCORE - end - end - end + match.games = games + match.opponents = opponents - return opponents, winner -end + match.extradata = MatchFunctions.getExtraData(match) ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score or NO_SCORE) or NO_SCORE - local value2 = tonumber(tbl[key2].score or NO_SCORE) or NO_SCORE - return value1 > value2 -end - --- Check if any opponent has a none-standard status ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckSpecialStatus(tbl) - return Table.any(tbl, - function (_, scoreinfo) - return scoreinfo.status ~= STATUS_SCORE and String.isNotEmpty(scoreinfo.status) - end - ) + return match end -- -- match related functions -- --- Calculate the match scores based on the map results (counting map wins) --- Only update an opponents result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local newScores = {} - local setScores = false - - -- If the match has started, we want to use the automatic calculations - if match.dateexact and match.timestamp <= NOW then - setScores = true - end +-- Template:Map sets a default map name so we can count the number of maps. +-- These maps however shouldn't be stored +-- The keepMap function will check if a map should be kept +---@param games table[] +---@return table[] +function MatchFunctions.removeUnsetMaps(games) + return Array.filter(games, MapFunctions.keepMap) +end - local mapIndex = 1 - while match['map'..mapIndex] do - local winner = tonumber(match['map'..mapIndex].winner) - if winner and winner > 0 and winner <= MAX_NUM_OPPONENTS then - setScores = true - newScores[winner] = (newScores[winner] or 0) + 1 +---@param match table +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - mapIndex = mapIndex + 1 - end - for index = 1, MAX_NUM_OPPONENTS do - if not match['opponent' .. index].score and setScores then - match['opponent' .. index].score = newScores[index] or 0 - end + table.insert(maps, map) + match[key] = nil end - return match -end - ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - return MatchGroupInput.getCommonTournamentVars(match) + return maps end ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - - return match +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + end end ---@param match table ---@return table -function matchFunctions.getLinks(match) - match.links = {} - return match +function MatchFunctions.getLinks(match) + return {} end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mvp = MatchGroupInput.readMvp(match), +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), } - return match -end - ----@param match table ----@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - opponent.score = string.upper(opponent.score or '') - if Logic.isNumeric(opponent.score) then - opponent.score = tonumber(opponent.score) - opponent.status = STATUS_SCORE - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = NOT_PLAYED_SCORE - end - - -- get players from vars for teams - if opponent.type == Opponent.team then - if not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name, { - resolveRedirect = true, - applyUnderScores = true, - maxNumPlayers = MAX_NUM_PLAYERS, - }) - end - elseif opponent.type ~= Opponent.solo and opponent.type ~= Opponent.literal then - error('Unsupported Opponent Type "' .. (opponent.type or '') .. '"') - end - - opponents[opponentIndex] = opponent - end - end - - --apply walkover input - match.walkover = string.upper(match.walkover or '') - if Logic.isNumeric(match.walkover) then - local winnerIndex = tonumber(match.walkover) - opponents = matchFunctions._makeAllOpponentsLoseByWalkover(opponents, STATUS_DEFAULT_LOSS) - opponents[winnerIndex].status = STATUS_DEFAULT_WIN - match.finished = true - elseif Logic.isNumeric(match.winner) and Table.includes(ALLOWED_STATUSES, match.walkover) then - local winnerIndex = tonumber(match.winner) - opponents = matchFunctions._makeAllOpponentsLoseByWalkover(opponents, match.walkover) - opponents[winnerIndex].status = STATUS_DEFAULT_WIN - match.finished = true - end - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if - not Logic.isEmpty(match.winner) or - Logic.readBool(match.finished) or - CustomMatchGroupInput.placementCheckSpecialStatus(opponents) - then - match.finished = true - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match -end - ----@param opponents table[] ----@param walkoverType string? ----@return table[] -function matchFunctions._makeAllOpponentsLoseByWalkover(opponents, walkoverType) - for index, _ in pairs(opponents) do - opponents[index].score = NOT_PLAYED_SCORE - opponents[index].status = walkoverType - end - return opponents end -- -- map related functions -- --- Parse extradata information +-- Check if a map should be discarded due to being redundant +-- DUMMY_MAP_NAME needs the match the default value in Template:Map ---@param map table ----@return table -function mapFunctions.getExtraData(map) - map.extradata.comment = map.comment - - return map +---@return boolean +function MapFunctions.keepMap(map) + return map.map ~= DUMMY_MAP end --- Calculate Score and Winner of the map ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - if Logic.isNumeric(score) then - obj.status = STATUS_SCORE - score = tonumber(score) - map['score' .. scoreIndex] = score - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = NOT_PLAYED_SCORE - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map +function MapFunctions.getExtraData(map, opponentCount) + return { + comment = map.comment, + } end return CustomMatchGroupInput diff --git a/components/match2/wikis/brawlhalla/match_group_input_custom.lua b/components/match2/wikis/brawlhalla/match_group_input_custom.lua index 42d75668b9a..4cf0b03840e 100644 --- a/components/match2/wikis/brawlhalla/match_group_input_custom.lua +++ b/components/match2/wikis/brawlhalla/match_group_input_custom.lua @@ -8,406 +8,162 @@ local Array = require('Module:Array') local CharacterStandardization = mw.loadData('Module:CharacterStandardization') -local FnUtil = require('Module:FnUtil') -local Json = require('Module:Json') -local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Operator = require('Module:Operator') local String = require('Module:StringUtils') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') local Opponent = Lua.import('Module:Opponent') local Streams = Lua.import('Module:Links/Stream') -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L'} -local CONVERT_STATUS_INPUT = {W = 'W', FF = 'FF', L = 'L', DQ = 'DQ', ['-'] = 'L'} -local DEFAULT_LOSS_STATUSES = {'FF', 'L', 'DQ'} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_PLAYERS = 10 -local DEFAULT_BESTOF = 99 - local CustomMatchGroupInput = {} ---- called from Module:MatchGroup + +-- called from Module:MatchGroup ---@param match table ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - Table.mergeInto(match, MatchGroupInput.readDate(match.date, { - 'match_date', + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + + Table.mergeInto(match, Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date, { 'tournament_enddate', 'tournament_startdate', - })) - CustomMatchGroupInput._getOpponents(match) - CustomMatchGroupInput._getTournamentVars(match) - CustomMatchGroupInput._processMaps(match) - CustomMatchGroupInput._calculateWinner(match) - CustomMatchGroupInput._updateFinished(match) - match.stream = Streams.processStreams(match) - CustomMatchGroupInput._getVod(match) - return match -end + }))) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) + local games = CustomMatchGroupInput.extractMaps(match, opponents) + match.bestof = MatchGroupInputUtil.getBestOf(nil, games) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and CustomMatchGroupInput.calculateMatchScore(games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) -CustomMatchGroupInput.processMap = FnUtil.identity - ----@param match table -function CustomMatchGroupInput._getTournamentVars(match) - match = MatchGroupInput.getCommonTournamentVars(match) - - match.bestof = match.bestof - match.mode = Variables.varDefault('tournament_mode', 'singles') -end - ----@param match table -function CustomMatchGroupInput._updateFinished(match) - match.finished = Logic.nilOr(Logic.readBoolOrNil(match.finished), Logic.isNotEmpty(match.winner)) if match.finished then - return + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - -- Match is automatically marked finished upon page edit after a - -- certain amount of time (depending on whether the date is exact) - local currentUnixTime = os.time(os.date('!*t') --[[@as osdateparam]]) - local threshold = match.dateexact and 30800 or 86400 - match.finished = match.timestamp + threshold < currentUnixTime -end + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) + match.mode = Variables.varDefault('tournament_mode', 'singles') ----@param match table -function CustomMatchGroupInput._getVod(match) match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod) -end - ----@param match table -function CustomMatchGroupInput._processMaps(match) - for _, _, mapIndex in Table.iter.pairsByPrefix(match, 'map') do - CustomMatchGroupInput._mapInput(match, mapIndex) - end -end - ----@param match table -function CustomMatchGroupInput._calculateWinner(match) - local bestof = match.bestof or DEFAULT_BESTOF - local numberOfOpponents = 0 - - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local opponent = match['opponent' .. opponentIndex] - if Logic.isEmpty(opponent) then - break - end - - numberOfOpponents = numberOfOpponents + 1 - - if Logic.isNotEmpty(match.walkover) then - if Logic.isNumeric(match.walkover) then - local walkover = tonumber(match.walkover) - if walkover == opponentIndex then - match.winner = opponentIndex - match.walkover = 'FF' - opponent.status = 'W' - elseif walkover == 0 then - match.winner = 0 - match.walkover = 'FF' - opponent.status = 'FF' - else - local score = string.upper(opponent.score or '') - opponent.status = CONVERT_STATUS_INPUT[score] or 'FF' - end - elseif Table.includes(ALLOWED_STATUSES, string.upper(match.walkover)) then - if tonumber(match.winner or 0) == opponentIndex then - opponent.status = 'W' - else - opponent.status = CONVERT_STATUS_INPUT[string.upper(match.walkover)] or 'L' - end - else - local score = string.upper(opponent.score or '') - opponent.status = CONVERT_STATUS_INPUT[score] or 'L' - match.walkover = 'L' - end - opponent.score = -1 - match.finished = true - match.resulttype = 'default' - elseif CONVERT_STATUS_INPUT[string.upper(opponent.score or '')] then - if string.upper(opponent.score) == 'W' then - match.winner = opponentIndex - match.finished = true - opponent.score = -1 - opponent.status = 'W' - else - local score = string.upper(opponent.score) - match.finished = true - match.walkover = CONVERT_STATUS_INPUT[score] - opponent.status = CONVERT_STATUS_INPUT[score] - opponent.score = -1 - end - match.resulttype = 'default' - else - opponent.status = 'S' - opponent.score = tonumber(opponent.score) or tonumber(opponent.autoscore) or -1 - if opponent.score > bestof / 2 then - match.finished = Logic.emptyOr(match.finished, true) - match.winner = tonumber(match.winner) or opponentIndex - end - end - end - - CustomMatchGroupInput._determineWinnerIfMissing(match) - - for opponentIndex = 1, numberOfOpponents do - local opponent = match['opponent' .. opponentIndex] - if match.winner == 'draw' or tonumber(match.winner) == 0 or - (match.opponent1.score == bestof / 2 and match.opponent1.score == match.opponent2.score) then - match.finished = true - match.winner = 0 - match.resulttype = 'draw' - end - - if tonumber(match.winner) == opponentIndex or - match.resulttype == 'draw' then - opponent.placement = 1 - elseif Logic.isNumeric(match.winner) then - opponent.placement = 2 - end - end -end - ----@param match table -function CustomMatchGroupInput._determineWinnerIfMissing(match) - if not Logic.readBool(match.finished) or Logic.isNotEmpty(match.winner) then - return - end - - local scores = Array.mapIndexes(function(opponentIndex) - local opponent = match['opponent' .. opponentIndex] - if not opponent then - return nil - end - return match['opponent' .. opponentIndex].score or -1 - end - ) - local maxScore = math.max(unpack(scores) or 0) - -- if we have a positive score and the match is finished we also have a winner - if maxScore > 0 then - local maxIndexFound = false - for opponentIndex, score in pairs(scores) do - if maxIndexFound and score == maxScore then - match.winner = 0 - break - elseif score == maxScore then - maxIndexFound = true - match.winner = opponentIndex - end - end - end -end ----@param match table ----@return table -function CustomMatchGroupInput._getOpponents(match) - -- read opponents and ignore empty ones - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if Logic.isNotEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - end - match['opponent' .. opponentIndex] = opponent - - if opponent.type == Opponent.team and Logic.isNotEmpty(opponent.name) then - MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name, { - resolveRedirect = true, - applyUnderScores = true, - maxNumPlayers = MAX_NUM_PLAYERS, - }) - end - end + match.games = games + match.opponents = opponents return match end ----@param record table ----@param timestamp number -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) or Opponent.blank() - - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - Opponent.resolve(opponent, timestamp, {syncPlayer = true}) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end - ---@param match table ----@param mapIndex integer -function CustomMatchGroupInput._mapInput(match, mapIndex) - local map = Json.parseIfString(match['map' .. mapIndex]) - if String.isNotEmpty(map.map) and map.map ~= 'TBD' then - map.map = mw.ext.TeamLiquidIntegration.resolve_redirect(map.map) - end - - -- set initial extradata for maps - map.extradata = { - comment = map.comment, - header = map.header, - } - map.game = match.game - map.mode = match.mode - - -- determine score, resulttype, walkover and winner - map = CustomMatchGroupInput._mapWinnerProcessing(map) - - -- Init score if match started and map info is present - if not match.opponent1.autoscore and not match.opponent2.autoscore - and map.map and map.map ~= 'TBD' - and match.timestamp < os.time(os.date('!*t') --[[@as osdateparam]]) - and String.isNotEmpty(map.char1) and String.isNotEmpty(map.char2) then - match.opponent1.autoscore = 0 - match.opponent2.autoscore = 0 - end - - if Logic.isEmpty(map.resulttype) and map.scores[1] and map.scores[2] then - match.opponent1.autoscore = (match.opponent1.autoscore or 0) + map.scores[1] - match.opponent2.autoscore = (match.opponent2.autoscore or 0) + map.scores[2] - end - - -- get participants data for the map + get map mode + winnerfaction and loserfaction - --(w/l faction stuff only for 1v1 maps) - CustomMatchGroupInput.processPlayerMapData(map, match, 2) - - match['map' .. mapIndex] = map -end - ----@param map table ----@return table -function CustomMatchGroupInput._mapWinnerProcessing(map) - map.scores = {} - local hasManualScores = false - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] - local obj = {} - if Logic.isNotEmpty(score) then - hasManualScores = true - score = CONVERT_STATUS_INPUT[score] or score - if Logic.isNumeric(score) then - obj.status = 'S' - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break +---@param matchOpponents table[] +---@return table[] +function CustomMatchGroupInput.extractMaps(match, matchOpponents) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + if String.isNotEmpty(map.map) and map.map ~= 'TBD' then + map.map = mw.ext.TeamLiquidIntegration.resolve_redirect(map.map) end - end - local winnerInput = tonumber(map.winner) - if Logic.isNotEmpty(map.walkover) then - local walkoverInput = tonumber(map.walkover) - if walkoverInput == 1 or walkoverInput == 2 or walkoverInput == 0 then - map.winner = walkoverInput + map.extradata = { + comment = map.comment, + } + map.finished = MatchGroupInputUtil.mapIsFinished(map) + map.opponents = Array.map(matchOpponents, function(opponent, opponentIndex) + return CustomMatchGroupInput.getParticipantsOfOpponent(map, opponent, opponentIndex) + end) + + local opponentInfo = Array.map(matchOpponents, function(_, opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }, CustomMatchGroupInput.calculateMapScore(map.winner, map.finished)) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - map.walkover = Table.includes(ALLOWED_STATUSES, map.walkover) and map.walkover or 'L' - map.scores = {-1, -1} - map.resulttype = 'default' - return map + table.insert(maps, map) + match[key] = nil end - if hasManualScores then - for scoreIndex, _ in Table.iter.spairs(indexedScores, CustomMatchGroupInput._placementSortFunction) do - if not tonumber(map.winner) then - map.winner = scoreIndex - else - break - end - end - - return map - end + return maps +end - if map.winner == 'skip' then - map.scores = {-1, -1} - map.resulttype = 'np' - elseif winnerInput == 1 then - map.scores = {1, 0} - elseif winnerInput == 2 then - map.scores = {0, 1} - elseif winnerInput == 0 or map.winner == 'draw' then - map.scores = {0, 0} - map.resulttype = 'draw' +---@param maps table[] +---@return fun(opponentIndex: integer): integer +function CustomMatchGroupInput.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - - return map end ---@param map table ----@param match table ----@param numberOfOpponents integer -function CustomMatchGroupInput.processPlayerMapData(map, match, numberOfOpponents) - local participants = {} - for opponentIndex = 1, numberOfOpponents do - local opponent = match['opponent' .. opponentIndex] - if opponent.type == Opponent.solo then - CustomMatchGroupInput._processSoloMapData(opponent.match2players[1], map, opponentIndex, participants) - end +---@param opponent table +---@param opponentIndex integer +---@return table? +function CustomMatchGroupInput.getParticipantsOfOpponent(map, opponent, opponentIndex) + if opponent.type == Opponent.solo then + return CustomMatchGroupInput._processSoloMapData(opponent.match2players[1], map, opponentIndex) end - - map.participants = participants + return nil end ---@param player table ---@param map table ---@param opponentIndex integer ----@param participants table ---@return table -function CustomMatchGroupInput._processSoloMapData(player, map, opponentIndex, participants) +function CustomMatchGroupInput._processSoloMapData(player, map, opponentIndex) local char = map['char' .. opponentIndex] or '' - participants[opponentIndex .. '_1'] = { - char = MatchGroupInput.getCharacterName(CharacterStandardization, char), - player = player.name, + return { + players = { + { + char = MatchGroupInputUtil.getCharacterName(CharacterStandardization, char), + player = player.name, + } + } } - - return participants end --- function to sort out winner/placements ----@param tbl table ----@param key1 string ----@param key2 string ----@return boolean -function CustomMatchGroupInput._placementSortFunction(tbl, key1, key2) - local opponent1 = tbl[key1] - local opponent2 = tbl[key2] - local opponent1Norm = opponent1.status == 'S' - local opponent2Norm = opponent2.status == 'S' - if opponent1Norm then - if opponent2Norm then - return tonumber(opponent1.score) > tonumber(opponent2.score) - else - return true - end - else - if opponent2Norm then - return false - elseif opponent1.status == 'W' then - return true - elseif Table.includes(DEFAULT_LOSS_STATUSES, opponent1.status) then - return false - elseif opponent2.status == 'W' then - return false - elseif Table.includes(DEFAULT_LOSS_STATUSES, opponent2.status) then - return true - else - return true +---@param winnerInput string|integer|nil +---@param finished boolean +---@return fun(opponentIndex: integer): integer? +function CustomMatchGroupInput.calculateMapScore(winnerInput, finished) + local winner = tonumber(winnerInput) + return function(opponentIndex) + -- TODO Better to check if map has started, rather than finished, for a more correct handling + if not winner and not finished then + return end + return winner == opponentIndex and 1 or 0 end end diff --git a/components/match2/wikis/brawlstars/match_group_input_custom.lua b/components/match2/wikis/brawlstars/match_group_input_custom.lua index 54ee7907dc2..200165ec368 100644 --- a/components/match2/wikis/brawlstars/match_group_input_custom.lua +++ b/components/match2/wikis/brawlstars/match_group_input_custom.lua @@ -47,11 +47,11 @@ function CustomMatchGroupInput.processMatch(match, options) local opponents = Array.mapIndexes(function(opponentIndex) return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) end) - local games = CustomMatchGroupInput.extractMaps(match, #opponents) + local games = CustomMatchGroupInput.extractMaps(match, opponents) match.bestof = MatchFunctions.getBestOf(match) local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) - and MatchFunctions.calculateMatchScore(games, match.bestof) + and MatchFunctions.calculateMatchScore(games) or nil Array.forEach(opponents, function(opponent, opponentIndex) opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ @@ -71,7 +71,8 @@ function CustomMatchGroupInput.processMatch(match, options) MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) match.stream = Streams.processStreams(match) @@ -84,9 +85,9 @@ function CustomMatchGroupInput.processMatch(match, options) end ---@param match table ----@param opponentCount integer +---@param opponents table[] ---@return table[] -function CustomMatchGroupInput.extractMaps(match, opponentCount) +function CustomMatchGroupInput.extractMaps(match, opponents) local maps = {} for key, map, mapIndex in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do local finishedInput = map.finished --[[@as string?]] @@ -94,10 +95,10 @@ function CustomMatchGroupInput.extractMaps(match, opponentCount) map.vod = map.vod or String.nilIfEmpty(match['vodgame' .. mapIndex]) map.bestof = MapFunctions.getBestOf(map) - map.participants = MapFunctions.getParticipants(map, opponentCount) - map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.participants = MapFunctions.getParticipants(map, opponents) + map.extradata = MapFunctions.getExtraData(map, #opponents) - local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local opponentInfo = Array.map(opponents, function(_, opponentIndex) local score, status = MatchGroupInputUtil.computeOpponentScore({ walkover = map.walkover, winner = map.winner, @@ -123,16 +124,13 @@ function CustomMatchGroupInput.extractMaps(match, opponentCount) return maps end -CustomMatchGroupInput.processMap = FnUtil.identity - -- -- match related functions -- ---@param maps table[] ----@param bestOf integer ---@return fun(opponentIndex: integer): integer? -function MatchFunctions.calculateMatchScore(maps, bestOf) +function MatchFunctions.calculateMatchScore(maps) return function(opponentIndex) return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end @@ -146,13 +144,6 @@ function MatchFunctions.getBestOf(match) return bestof or DEFAULT_BESTOF_MATCH end ----@param match table ----@return table -function MatchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - return MatchGroupInputUtil.getCommonTournamentVars(match) -end - ---@param match table ---@return table function MatchFunctions.getExtraData(match) @@ -201,23 +192,37 @@ function MapFunctions.getExtraData(map, opponentCount) end ---@param map table ----@param opponentCount integer +---@param opponents table[] ---@return table -function MapFunctions.getParticipants(map, opponentCount) - local participants = {} +function MapFunctions.getParticipants(map, opponents) + local allParticipants = {} local getCharacterName = FnUtil.curry(MatchGroupInputUtil.getCharacterName, BrawlerNames) - for opponentIndex = 1, opponentCount do - for _, player, playerIndex in Table.iter.pairsByPrefix(map, 't' .. opponentIndex .. 'p') do - participants[opponentIndex .. '_' .. playerIndex] = {player = player} - end - - for _, brawler, pickIndex in Table.iter.pairsByPrefix(map, 't' .. opponentIndex .. 'c') do - participants[opponentIndex .. '_' .. pickIndex] = participants[opponentIndex .. '_' .. pickIndex] or {} - participants[opponentIndex .. '_' .. pickIndex].brawler = getCharacterName(brawler) - end - end + Array.forEach(opponents, function(opponent, opponentIndex) + local players = Array.mapIndexes(function(playerIndex) + return opponent.match2players[playerIndex] or Logic.nilIfEmpty(map['t' .. opponentIndex .. 'c' .. playerIndex]) + end) + local participants, unattachedParticipants = MatchGroupInputUtil.parseParticipants( + opponent.match2players, + players, + function(playerIndex) + local player = map['t' .. opponentIndex .. 'p' .. playerIndex] + return player and {name = player} or nil + end, + function(playerIndex, playerIdData) + local brawler = map['t' .. opponentIndex .. 'c' .. playerIndex] + return { + player = playerIdData.name, + brawler = getCharacterName(brawler), + } + end + ) + Array.forEach(unattachedParticipants, function(participant) + table.insert(participants, participant) + end) + Table.mergeInto(allParticipants, Table.map(participants, MatchGroupInputUtil.prefixPartcipants(opponentIndex))) + end) - return participants + return allParticipants end return CustomMatchGroupInput diff --git a/components/match2/wikis/callofduty/match_group_input_custom.lua b/components/match2/wikis/callofduty/match_group_input_custom.lua index 6876b6b77b4..9efb8ad9df7 100644 --- a/components/match2/wikis/callofduty/match_group_input_custom.lua +++ b/components/match2/wikis/callofduty/match_group_input_custom.lua @@ -7,7 +7,6 @@ -- local Array = require('Module:Array') -local FnUtil = require('Module:FnUtil') local Logic = require('Module:Logic') local Lua = require('Module:Lua') local Operator = require('Module:Operator') @@ -41,7 +40,7 @@ function CustomMatchGroupInput.processMatch(match, options) match.bestof = MatchFunctions.getBestOf(match) local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) - and MatchFunctions.calculateMatchScore(games, match.bestof) + and MatchFunctions.calculateMatchScore(games) or nil Array.forEach(opponents, function(opponent, opponentIndex) opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ @@ -61,7 +60,8 @@ function CustomMatchGroupInput.processMatch(match, options) MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) match.stream = Streams.processStreams(match) match.links = MatchFunctions.getLinks(match) @@ -110,16 +110,13 @@ function CustomMatchGroupInput.extractMaps(match, opponentCount) return maps end -CustomMatchGroupInput.processMap = FnUtil.identity - -- -- match related functions -- ---@param maps table[] ----@param bestOf integer ---@return fun(opponentIndex: integer): integer? -function MatchFunctions.calculateMatchScore(maps, bestOf) +function MatchFunctions.calculateMatchScore(maps) return function(opponentIndex) return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end @@ -133,13 +130,6 @@ function MatchFunctions.getBestOf(match) return bestof or DEFAULT_BESTOF end ----@param match table ----@return table -function MatchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - return MatchGroupInputUtil.getCommonTournamentVars(match) -end - ---@param match table ---@return table function MatchFunctions.getLinks(match) diff --git a/components/match2/wikis/clashofclans/match_group_input_custom.lua b/components/match2/wikis/clashofclans/match_group_input_custom.lua index efc8a269b54..e23dc6516c3 100644 --- a/components/match2/wikis/clashofclans/match_group_input_custom.lua +++ b/components/match2/wikis/clashofclans/match_group_input_custom.lua @@ -7,432 +7,182 @@ -- local Array = require('Module:Array') -local DateExt = require('Module:Date/Ext') -local Json = require('Module:Json') local Logic = require('Module:Logic') local Lua = require('Module:Lua') local MathUtil = require('Module:MathUtil') +local Operator = require('Module:Operator') local Streams = require('Module:Links/Stream') -local String = require('Module:StringUtils') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L', 'D'} -local FINISHED_INDICATORS = {'skip', 'np', 'cancelled', 'canceled'} -local MAX_NUM_OPPONENTS = 8 -local MAX_NUM_PLAYERS = 10 -local MAX_NUM_MAPS = 9 local DEFAULT_BESTOF = 3 -local NO_SCORE = -99 -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_MODE = 'team' +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, +} -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} -- called from Module:MatchGroup +---@param match table +---@param options table? +---@return table function CustomMatchGroupInput.processMatch(match, options) - -- Count number of maps, check for empty maps to remove, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) - CustomMatchGroupInput._underScoreAdjusts(match) + local games = MatchFunctions.extractMaps(match, #opponents) - return match -end - -function CustomMatchGroupInput._underScoreAdjusts(match) - local fixUnderscore = function(page) - return page and page:gsub(' ', '_') or page - end - - for opponentKey, opponent in Table.iter.pairsByPrefix(match, 'opponent') do - opponent.name = fixUnderscore(opponent.name) - - for _, player in Table.iter.pairsByPrefix(match, opponentKey .. '_p') do - player.name = fixUnderscore(player.name) - end - end -end - --- called from Module:Match/Subobjects -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) - - map.map = nil - - return map -end - -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() - - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end - - Opponent.resolve(opponent, teamTemplateDate, {syncPlayer=true}) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end - -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if Table.includes(FINISHED_INDICATORS, data.finished) or Table.includes(FINISHED_INDICATORS, data.winner) then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if CustomMatchGroupInput.isDraw(indexedScores, tonumber(data.winner)) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif CustomMatchGroupInput.placementCheckSpecialStatus(indexedScores) then - data.winner = CustomMatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = 'default' - if CustomMatchGroupInput.placementCheckFF(indexedScores) then - data.walkover = 'ff' - elseif CustomMatchGroupInput.placementCheckDQ(indexedScores) then - data.walkover = 'dq' - elseif CustomMatchGroupInput.placementCheckWL(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'default') - else - - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, tonumber(data.winner), nil, data.finished) - data.winner = tonumber(data.winner) or winner - end - end - - --set it as finished if we have a winner - if not Logic.isEmpty(data.winner) then - data.finished = true - end - - return data, indexedScores -end - ----@param indexedScores table[] ----@param winner integer? ----@return boolean -function CustomMatchGroupInput.isDraw(indexedScores, winner) - if winner == 0 then return true end - if winner then return false end - return MatchGroupInput.isDraw(indexedScores) -end - -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == 'default' or winner then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local last = {score = NO_SCORE, placement = NO_SCORE} - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and Logic.isEmpty(winner) then - if finished then - winner = scoreIndex - end - end - if last.score == score and last.time == opp.time and last.percentage == opp.percentage then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or last.placement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or counter - last = { - score = score or NO_SCORE, - placement = counter, - time = opp.time, - percentage = opp.percentage, - } - end - end - end - - return opponents, winner -end - -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local score1 = tonumber(tbl[key1].score or NO_SCORE) or NO_SCORE - local score2 = tonumber(tbl[key2].score or NO_SCORE) or NO_SCORE + match.bestof = MatchFunctions.getBestOf(match.bestof) - if score1 ~= score2 then - return score1 > score2 - end + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) - local percentage1 = tbl[key1].percentage - local percentage2 = tbl[key2].percentage + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) - if percentage1 ~= percentage2 then - return percentage1 > percentage2 + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - local time1 = tbl[key1].time - local time2 = tbl[key2].time - - if time1 == time2 or time2 and not time1 then - return false - elseif not time2 then - return true - end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) - return time1 < time2 -end - --- Check if any team has a none-standard status -function CustomMatchGroupInput.placementCheckSpecialStatus(tbl) - return Table.any(tbl, function (_, scoreinfo) return scoreinfo.status ~= 'S' end) -end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) --- function to check for forfeits -function CustomMatchGroupInput.placementCheckFF(tbl) - return Table.any(tbl, function (_, scoreinfo) return scoreinfo.status == 'FF' end) -end + match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) --- function to check for DQ's -function CustomMatchGroupInput.placementCheckDQ(tbl) - return Table.any(tbl, function (_, scoreinfo) return scoreinfo.status == 'DQ' end) -end + match.games = games + match.opponents = opponents --- function to check for W/L -function CustomMatchGroupInput.placementCheckWL(tbl) - return Table.any(tbl, function (_, scoreinfo) return scoreinfo.status == 'L' end) -end + match.extradata = MatchFunctions.getExtraData(match) --- Get the winner when resulttype=default -function CustomMatchGroupInput.getDefaultWinner(tbl) - for index, scoreInfo in pairs(tbl) do - if scoreInfo.status == 'W' then - return index - end - end - return -1 + return match end -- -- match related functions -- -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match -end - --- Calculate the match scores based on the map results (counting map wins) --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner -function matchFunctions.getScoreFromMapWinners(match) - local opponentNumber = 0 - for index = 1, MAX_NUM_OPPONENTS do - if String.isEmpty(match['opponent' .. index]) then - break - end - opponentNumber = index - end - local newScores = {} - local foundScores = false - - for i = 1, MAX_NUM_MAPS do - if match['map'..i] then - local winner = tonumber(match['map'..i].winner) - foundScores = true - if winner and winner > 0 and winner <= opponentNumber then - newScores[winner] = (newScores[winner] or 0) + 1 - end - else - break - end - end - - for index = 1, opponentNumber do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end - end - - return match -end -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - return MatchGroupInput.getCommonTournamentVars(match) -end - -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - - match.links = {} - - return match -end - -function matchFunctions.getExtraData(match) - match.extradata = { - mvp = MatchGroupInput.readMvp(match), - mvpteam = match.mvpteam or match.winner, +---@param match table +---@return table +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), } - - return match -end - --- Parse MVP input -function matchFunctions.getMVP(match) - if not match.mvp then return {} end - local mvppoints = match.mvppoints or 1 - - -- Split the input - local players = mw.text.split(match.mvp, ',') - - -- Trim the input - for index, player in pairs(players) do - players[index] = mw.text.trim(player) - end - - return {players = players, points = mvppoints} end -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if Logic.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == Opponent.team and not Logic.isEmpty(opponent.name) then - match = matchFunctions.getTeamPlayers(match, opponentIndex, opponent.name) - end - end - end - - -- see if match should actually be finished if bestof limit was reached - if isScoreSet and not Logic.readBool(match.finished) then - local firstTo = math.ceil(match.bestof / 2) - for _, item in pairs(opponents) do - if (tonumber(item.score or 0) or 0) >= firstTo then - match.finished = true - break - end +---@param match table +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.map = nil + map.extradata = MapFunctions.getExtraData(map) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return { + score = score, + status = status, + time = map.extradata.times[opponentIndex], + percentage = map.extradata.percentages[opponentIndex] or 0 + } + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - -- check if match should actually be finished due to a non score status - if not Logic.readBool(match.finished) then - for _, opponent in pairs(opponents) do - if String.isNotEmpty(opponent.status) and opponent.status ~= 'S' then - match.finished = true - break - end - end + table.insert(maps, map) + match[key] = nil end - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end + return maps +end - -- apply placements and winner if finshed - if not Logic.isEmpty(match.winner) or Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent + if bestof then + Variables.varDefine('bestof', bestof) + return bestof end - return match + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF end --- Get Playerdata from Vars (get's set in TeamCards) for team opponents -function matchFunctions.getTeamPlayers(match, opponentIndex, teamName) - -- let's make sure we don't leave any gaps. - match['opponent' .. opponentIndex].match2players = {} - for playerIndex = 1, MAX_NUM_PLAYERS do - -- parse player - local player = Json.parseIfString(match['opponent' .. opponentIndex .. '_p' .. playerIndex]) or {} - player.name = player.name or Variables.varDefault(teamName .. '_p' .. playerIndex) - player.flag = player.flag or Variables.varDefault(teamName .. '_p' .. playerIndex .. 'flag') - player.displayname = player.displayname or Variables.varDefault(teamName .. '_p' .. playerIndex .. 'dn') - if not Table.isEmpty(player) then - table.insert(match['opponent' .. opponentIndex].match2players, player) - end +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end +end - return match +---@param match table +---@return table +function MatchFunctions.getLinks(match) + return {} end -- -- map related functions -- --- Parse extradata information -function mapFunctions.getExtraData(map) - map.extradata = { +---@param map table +---@return table +function MapFunctions.getExtraData(map) + return { comment = map.comment, - times = mapFunctions.readTimes(map), - percentages = mapFunctions.readPercentages(map), + times = MapFunctions.readTimes(map), + percentages = MapFunctions.readPercentages(map), } - return map end -function mapFunctions.readPercentages(map) +---@param map table +---@return table +function MapFunctions.readPercentages(map) local percentages = {} for _, percentage in Table.iter.pairsByPrefix(map, 'percent') do @@ -442,7 +192,9 @@ function mapFunctions.readPercentages(map) return percentages end -function mapFunctions.readTimes(map) +---@param map table +---@return table +function MapFunctions.readTimes(map) local timesInSeconds = {} for _, timeInput in Table.iter.pairsByPrefix(map, 'time') do @@ -460,34 +212,4 @@ function mapFunctions.readTimes(map) return timesInSeconds end --- Calculate Score and Winner of the map -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = tonumber(map['score' .. scoreIndex]) or map['score' .. scoreIndex] - local obj = {} - if not Logic.isEmpty(score) then - if Logic.isNumeric(score) then - obj.status = 'S' - obj.score = tonumber(score) - obj.time = map.extradata.times[scoreIndex] - obj.percentage = map.extradata.percentages[scoreIndex] or 0 - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map -end - return CustomMatchGroupInput diff --git a/components/match2/wikis/clashofclans/match_summary.lua b/components/match2/wikis/clashofclans/match_summary.lua index 41e6c3a14a3..46b7527bf17 100644 --- a/components/match2/wikis/clashofclans/match_summary.lua +++ b/components/match2/wikis/clashofclans/match_summary.lua @@ -94,10 +94,15 @@ function CustomMatchSummary.createBody(match) end function CustomMatchSummary._gameScore(game, opponentIndex) - local score = game.scores[opponentIndex] or '' return mw.html.create('div') :css('width', '16px') - :wikitext(score) + :wikitext(DisplayHelper.MapScore( + game.scores[opponentIndex], + opponentIndex, + game.resultType, + game.walkover, + game.winner + )) end function CustomMatchSummary._percentage(game, opponentIndex) diff --git a/components/match2/wikis/clashroyale/match_group_input_custom.lua b/components/match2/wikis/clashroyale/match_group_input_custom.lua new file mode 100644 index 00000000000..ed24a5cfe4b --- /dev/null +++ b/components/match2/wikis/clashroyale/match_group_input_custom.lua @@ -0,0 +1,344 @@ +--- +-- @Liquipedia +-- wiki=clashroyale +-- page=Module:MatchGroup/Input/Custom +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local CardNames = mw.loadData('Module:CardNames') +local FnUtil = require('Module:FnUtil') +local Json = require('Module:Json') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') +local Operator = require('Module:Operator') +local Table = require('Module:Table') +local Variables = require('Module:Variables') + +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') +local OpponentLibraries = require('Module:OpponentLibraries') +local Opponent = OpponentLibraries.Opponent +local Streams = Lua.import('Module:Links/Stream') + +local DEFAULT_MODE = 'solo' +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, +} +local ROYALE_API_PREFIX = 'https://royaleapi.com/' + +local CustomMatchGroupInput = {} +local MatchFunctions = {} +local MapFunctions = {} + +---@param match table +---@param options table? +---@return table +function CustomMatchGroupInput.processMatch(match, options) + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) + + local games = MatchFunctions.extractMaps(match, opponents) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games, opponents) + or nil + + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) + + match.bestof = MatchFunctions.getBestOf(match.bestof) + + local winnerInput = match.winner --[[@as string?]] + local finishedInput = match.finished --[[@as string?]] + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) + end + + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) + + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) + match.vod = Logic.nilIfEmpty(match.vod) + match.extradata = MatchFunctions.getExtraData(match, #games) + + match.games = games + match.opponents = opponents + + return match +end + +---@param match table +---@return table +function MatchFunctions.getLinks(match) + return { + royaleapi = match.royaleapi and (ROYALE_API_PREFIX .. match.royaleapi) or nil, + } +end + +---@param match table +---@param opponents table[] +---@return table[] +function MatchFunctions.extractMaps(match, opponents) + local maps = {} + local subGroup = 0 + for mapKey, mapInput, mapIndex in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local map + map, subGroup = MapFunctions.readMap(mapInput, mapIndex, subGroup, #opponents) + + map.participants = MapFunctions.getParticipants(mapInput, opponents) + map.extradata = MapFunctions.getExtraData(mapInput, map.participants) + + map.vod = Logic.emptyOr(mapInput.vod, match['vodgame' .. mapIndex]) + + table.insert(maps, map) + match[mapKey] = nil + end + + return maps +end + +---@param maps table[] +---@param opponents table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps, opponents) + return function(opponentIndex) + local calculatedScore = MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + if not calculatedScore then return end + local opponent = opponents[opponentIndex] + return calculatedScore + (opponent.extradata.advantage or 0) - (opponent.extradata.penalty or 0) + end +end + +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) or tonumber(Variables.varDefault('match_bestof')) + + if bestof then + Variables.varDefine('match_bestof', bestof) + end + + return bestof +end + +---@param match table +---@param numberOfGames integer +---@return table +function MatchFunctions.getExtraData(match, numberOfGames) + local extradata = { + t1bans = MatchFunctions.readBans(match.t1bans), + t2bans = MatchFunctions.readBans(match.t2bans), + mvp = MatchGroupInputUtil.readMvp(match), + } + + local prefix = 'subgroup%d+' + Table.mergeInto(extradata, Table.filterByKey(match, function(key) return key:match(prefix .. 'header') end)) + Table.mergeInto(extradata, Table.filterByKey(match, function(key) return key:match(prefix .. 'iskoth') end)) + + local t1Bans = Table.filterByKey(match, function(key) return key:match(prefix .. 't1bans') end) + Table.mergeInto(extradata, Table.mapValues(t1Bans, MatchFunctions.readBans)) + + local t2Bans = Table.filterByKey(match, function(key) return key:match(prefix .. 't2bans') end) + Table.mergeInto(extradata, Table.mapValues(t2Bans, MatchFunctions.readBans)) + + return extradata +end + +---@param bansInput string +---@return table? +function MatchFunctions.readBans(bansInput) + local bans = CustomMatchGroupInput._readCards(bansInput) + + return Logic.nilIfEmpty(bans) +end + +---@param mapInput table +---@param mapIndex integer +---@param subGroup integer +---@param opponentCount integer +---@return table +---@return integer +function MapFunctions.readMap(mapInput, mapIndex, subGroup, opponentCount) + subGroup = tonumber(mapInput.subgroup) or (subGroup + 1) + + local map = { + subgroup = subGroup, + } + + map.finished = MatchGroupInputUtil.mapIsFinished(mapInput) + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = mapInput.walkover, + winner = mapInput.winner, + opponentIndex = opponentIndex, + score = mapInput['score' .. opponentIndex], + }, MapFunctions.calculateMapScore(mapInput, map.finished)) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(mapInput.winner, mapInput.finished, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, mapInput.winner, opponentInfo) + end + + return map, subGroup +end + +---@param mapInput table +---@param finished boolean +---@return fun(opponentIndex: integer): integer? +function MapFunctions.calculateMapScore(mapInput, finished) + local winner = tonumber(mapInput.winner) + return function(opponentIndex) + -- TODO Better to check if map has started, rather than finished, for a more correct handling + if not winner and not finished then + return + end + + return winner == opponentIndex and 1 or 0 + end +end + +---@param mapInput table +---@param opponents table[] +---@return table +function MapFunctions.getParticipants(mapInput, opponents) + local participants = {} + Array.forEach(opponents, function(opponent, opponentIndex) + if opponent.type == Opponent.literal then + return + elseif opponent.type == Opponent.team then + Table.mergeInto(participants, MapFunctions.getTeamParticipants(mapInput, opponent, opponentIndex)) + return + end + Table.mergeInto(participants, MapFunctions.getPartyParticipants(mapInput, opponent, opponentIndex)) + end) + + return participants +end + +---@param mapInput table +---@param opponent table +---@param opponentIndex integer +---@return table +function MapFunctions.getTeamParticipants(mapInput, opponent, opponentIndex) + local players = Array.mapIndexes(function(playerIndex) + return Logic.nilIfEmpty(mapInput['t' .. opponentIndex .. 'p' .. playerIndex]) + end) + + local participants, unattachedParticipants = MatchGroupInputUtil.parseParticipants( + opponent.match2players, + players, + function(playerIndex) + local prefix = 't' .. opponentIndex .. 'p' .. playerIndex + return { + name = mapInput[prefix], + link = Logic.nilIfEmpty(mapInput[prefix .. 'link']) or Variables.varDefault(mapInput[prefix] .. '_page'), + } + end, + function(playerIndex, playerIdData, playerInputData) + local prefix = 'o' .. opponentIndex .. 'p' .. playerIndex + return { + played = true, + player = playerIdData.name or playerInputData.link, + cards = CustomMatchGroupInput._readCards(mapInput[prefix .. 'c']), + } + end + ) + + Array.forEach(unattachedParticipants, function(participant) + table.insert(opponent.match2players, { + name = participant.player, + displayname = participant.player, + }) + participants[#opponent.match2players] = participant + end) + + return Table.map(participants, MatchGroupInputUtil.prefixPartcipants(opponentIndex)) +end + +---@param mapInput table +---@param opponent table +---@param opponentIndex integer +---@return table +function MapFunctions.getPartyParticipants(mapInput, opponent, opponentIndex) + local players = opponent.match2players + + local prefix = 't' .. opponentIndex .. 'p' + + local participants = {} + + Array.forEach(players, function(player, playerIndex) + participants[opponentIndex .. '_' .. playerIndex] = { + played = true, + player = player.name, + cards = CustomMatchGroupInput._readCards(mapInput[prefix .. playerIndex .. 'c']), + } + end) + + return participants +end + +---@param mapInput table +---@param participants table +---@return table +function MapFunctions.getExtraData(mapInput, participants) + local extradata = { + comment = mapInput.comment, + header = mapInput.header, + } + + return Table.merge(extradata, MapFunctions.getCardsExtradata(participants)) +end + +--- additionally store cards info in extradata so we can condition on them +---@param participants table +---@return table +function MapFunctions.getCardsExtradata(participants) + local extradata = {} + local playerCount = {} + for participantKey, participant in Table.iter.spairs(participants) do + local opponentIndex = string.match(participantKey, '^(%d+)_') + + playerCount[opponentIndex] = (playerCount[opponentIndex] or 0) + 1 + + local prefix = 't' .. opponentIndex .. 'p' .. playerCount[opponentIndex] + extradata[prefix .. 'tower'] = participant.cards.tower + -- participant.cards is an array plus the tower value .... + for cardIndex, card in ipairs(participant.cards) do + extradata[prefix .. 'c' .. cardIndex] = card + end + end + + return extradata +end + +---@param input string +---@return table +function CustomMatchGroupInput._readCards(input) + local cleanCard = FnUtil.curry(MatchGroupInputUtil.getCharacterName, CardNames) + + return Table.mapValues(Json.parseIfString(input) or {}, cleanCard) +end + +return CustomMatchGroupInput diff --git a/components/match2/wikis/clashroyale/match_summary.lua b/components/match2/wikis/clashroyale/match_summary.lua index 451c38d8475..3e0c9c70733 100644 --- a/components/match2/wikis/clashroyale/match_summary.lua +++ b/components/match2/wikis/clashroyale/match_summary.lua @@ -14,9 +14,11 @@ local FnUtil = require('Module:FnUtil') local Icon = require('Module:Icon') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Operator = require('Module:Operator') local Table = require('Module:Table') local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') local MatchSummary = Lua.import('Module:MatchSummary/Base') local OpponentLibraries = require('Module:OpponentLibraries') @@ -217,35 +219,60 @@ end ---@param matchId string ---@return MatchSummaryBody function CustomMatchSummary._createTeamMatchBody(body, match, matchId) - local subMatches = match.extradata.submatches - for _, game in ipairs(match.games) do - local subMatch = subMatches[game.subgroup] - if not subMatch.games then - subMatch.games = {} - end - - table.insert(subMatch.games, game) - end - - for subMatchIndex, subMatch in ipairs(subMatches) do - local players = CustomMatchSummary._fetchPlayersForSubmatch(subMatchIndex, subMatch, match) + local _, subMatches = Array.groupBy(match.games, Operator.property('subgroup')) + subMatches = Array.map(subMatches, function(subMatch) + return {games = subMatch} + end) + + Array.forEach(subMatches, FnUtil.curry(CustomMatchSummary._getSubMatchOpponentsAndPlayers, match)) + Array.forEach(subMatches, CustomMatchSummary._calculateSubMatchWinner) + Array.forEach(subMatches, function(subMatch, subMatchIndex) body:addRow(CustomMatchSummary._createSubMatch( - players, + subMatch.players, subMatchIndex, subMatch, match.extradata )) - end + end) CustomMatchSummary._addMvp(match, body) return body end +---@param match MatchGroupUtilMatch +---@param subMatch table +---@param subMatchIndex integer +function CustomMatchSummary._getSubMatchOpponentsAndPlayers(match, subMatch, subMatchIndex) + subMatch.players = CustomMatchSummary._fetchPlayersForSubmatch(subMatchIndex, subMatch, match) + subMatch.opponents = Array.map(Array.range(1, #subMatch.players), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore( + {opponentIndex = opponentIndex}, + FnUtil.curry(MatchGroupInputUtil.computeMatchScoreFromMapWinners, subMatch.games) + ) + return {score = score, status = status} + end) +end + +---@param subMatch table +function CustomMatchSummary._calculateSubMatchWinner(subMatch) + subMatch.scores = Array.map(subMatch.opponents, Operator.property('score')) + + local subMatchIsFinished = Array.all(subMatch.games, function(game) + return Logic.isNotEmpty(game.winner) + or game.resulttype == MatchGroupInputUtil.RESULT_TYPE.NOT_PLAYED + + end) + if not subMatchIsFinished then return end + + subMatch.finished = true + subMatch.winner = MatchGroupInputUtil.getHighestScoringOpponent(subMatch.opponents) +end + ---@param subMatchIndex integer ---@param subMatch table ---@param match MatchGroupUtilMatch ----@return table +---@return table[] function CustomMatchSummary._fetchPlayersForSubmatch(subMatchIndex, subMatch, match) local players = {{}, {}, hash = {{}, {}}} @@ -257,6 +284,8 @@ function CustomMatchSummary._fetchPlayersForSubmatch(subMatchIndex, subMatch, ma end end + players.hash = nil + return players end @@ -387,17 +416,21 @@ function CustomMatchSummary._opponentCardsDisplay(args) local date = args.date local color = flip and CARD_COLOR_2 or CARD_COLOR_1 - local wrapper = mw.html.create('div') - :css('flex-basis', '1px') - :css('display', 'inline-flex') - :css('flex-direction', (flip and 'row' or 'row-reverse')) - :css('align-items', 'center') - local wrapperCards = mw.html.create('div') + local sideWrapper = mw.html.create('div') :css('display', 'flex') :css('flex-direction', 'column') for _, cardData in ipairs(cardDataSets) do + local wrapper = mw.html.create('div') + :css('flex-basis', '1px') + :css('display', 'inline-flex') + :css('flex-direction', (flip and 'row' or 'row-reverse')) + :css('align-items', 'center') + local wrapperCards = mw.html.create('div') + :css('display', 'flex') + :css('flex-direction', 'column') + local cardDisplays = {} for _, card in ipairs(cardData) do table.insert(cardDisplays, mw.html.create('div') @@ -437,9 +470,11 @@ function CustomMatchSummary._opponentCardsDisplay(args) }) wrapper:node(towerCardDisplay) end + + sideWrapper:node(wrapper) end - return wrapper + return sideWrapper end return CustomMatchSummary diff --git a/components/match2/wikis/counterstrike/match_external_links.lua b/components/match2/wikis/counterstrike/match_external_links.lua index e6e1331705c..beefe1d258a 100644 --- a/components/match2/wikis/counterstrike/match_external_links.lua +++ b/components/match2/wikis/counterstrike/match_external_links.lua @@ -47,7 +47,7 @@ return { { name = 'faceit', icon = 'FACEIT-icon.png', - prefixLink = 'https://www.faceit.com/en/csgo/room/', + prefixLink = 'https://www.faceit.com/en/match/room/', label = 'Match Room and Stats on FACEIT', isMapStats = true }, diff --git a/components/match2/wikis/counterstrike/match_group_input_custom.lua b/components/match2/wikis/counterstrike/match_group_input_custom.lua index 7bd8df8a933..7ea55243ab1 100644 --- a/components/match2/wikis/counterstrike/match_group_input_custom.lua +++ b/components/match2/wikis/counterstrike/match_group_input_custom.lua @@ -5,34 +5,34 @@ -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- - +local Array = require('Module:Array') local DateExt = require('Module:Date/Ext') +local EarningsOf = require('Module:Earnings of') local Logic = require('Module:Logic') -local MathUtil = require('Module:MathUtil') local Lua = require('Module:Lua') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') local Table = require('Module:Table') -local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') -local Streams = require('Module:Links/Stream') -local EarningsOf = require('Module:Earnings of') -local Opponent = Lua.import('Module:Opponent') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local OpponentLibraries = Lua.import('Module:OpponentLibraries') +local Opponent = OpponentLibraries.Opponent +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L', 'D'} -local NP_MATCH_STATUS = {'cancelled','canceled', 'postponed'} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_MAPS = 9 local DUMMY_MAP_NAME = 'null' -- Is set in Template:Map when |map= is empty. local FEATURED_TIERS = {1, 2} local MIN_EARNINGS_FOR_FEATURED = 200000 -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local OPPONENT_CONFIG = { + maxNumPlayers = 5, + resolveRedirect = true, + applyUnderScores = true +} -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} @@ -41,283 +41,175 @@ local CustomMatchGroupInput = {} ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - -- Count number of maps, check for empty maps to remove, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getLinks(match) - match = matchFunctions.removeUnsetMaps(match) - match = matchFunctions.getScoreFromMapWinners(match) - - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getExtraData(match) - - return match -end - --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) + local games = MatchFunctions.extractMaps(match, #opponents) + match.bestof = MatchGroupInputUtil.getBestOf(nil, games) + games = MatchFunctions.removeUnsetMaps(games) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) + end - return map -end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) + match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_valve_tier')) + match.status = Logic.emptyOr(match.status, Variables.varDefault('tournament_status')) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match, games) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + match.games = games + match.opponents = opponents - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end + match.extradata = MatchFunctions.getExtraData(match, opponents) - Opponent.resolve(opponent, teamTemplateDate, {syncPlayer = true}) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) + return match end -- +-- match related functions -- --- function to check for draws ----@param tbl table[] ----@return boolean -function CustomMatchGroupInput.placementCheckDraw(tbl) - if #tbl < MAX_NUM_OPPONENTS then - return false - end - - return MatchGroupInput.isDraw(tbl) -end ----@param data table ----@param indexedScores table[] ----@return table +---@param match table +---@param opponentCount integer ---@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - local winner = tonumber(data.winner) - if Logic.readBool(data.finished) then - if CustomMatchGroupInput.placementCheckDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = MatchGroupInput.setPlacement(indexedScores, data.winner, 1, 1) - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = 'default' - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = MatchGroupInput.setPlacement(indexedScores, data.winner, 1, 2) - elseif CustomMatchGroupInput.placementCheckScoresSet(indexedScores) then - --CS only has exactly 2 opponents, neither more or less - if #indexedScores == MAX_NUM_OPPONENTS then - if tonumber(indexedScores[1].score) > tonumber(indexedScores[2].score) then - data.winner = 1 - else - data.winner = 2 - end - indexedScores = MatchGroupInput.setPlacement(indexedScores, data.winner, 1, 2) - end - end - --If a manual winner is set use it - if winner and data.resulttype ~= 'default' then - if winner == 0 then - data.resulttype = 'draw' - else - data.resulttype = nil - end - data.winner = winner - indexedScores = MatchGroupInput.setPlacement(indexedScores, winner, 1, 2) +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }, MapFunctions.calculateMapScore(map)) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end + + table.insert(maps, map) + match[key] = nil end - return data, indexedScores -end ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckScoresSet(tbl) - return Table.all(tbl, function (_, scoreinfo) return scoreinfo.status == 'S' end) + return maps end -- -- match related functions -- ----@param match table ----@return table -function matchFunctions.getBestOf(match) - local mapCount = 0 - for i = 1, MAX_NUM_MAPS do - if match['map' .. i] then - mapCount = mapCount + 1 - else - break - end +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - match.bestof = mapCount - return match end -- Template:Map sets a default map name so we can count the number of maps. --- These maps however shouldn't be stored in lpdb, nor displayed --- The discardMap function will check if a map should be removed --- Remove all maps that should be removed. ----@param match table ----@return table -function matchFunctions.removeUnsetMaps(match) - for i = 1, MAX_NUM_MAPS do - if match['map' .. i] then - if mapFunctions.discardMap(match['map' .. i]) then - match['map' .. i] = nil - end - else - break - end - end - return match -end - --- Calculate the match scores based on the map results. --- If it's a Best of 1, we'll take the exact score of that map --- If it's not a Best of 1, we should count the map wins --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - -- For best of 1, display the results of the single map - local opponent1 = match.opponent1 - local opponent2 = match.opponent2 - local newScores = {} - local foundScores = false - if match.bestof == 1 then - if match.map1 then - newScores = match.map1.scores - foundScores = true - end - else -- For best of >1, disply the map wins - for i = 1, MAX_NUM_MAPS do - if match['map' .. i] then - local winner = tonumber(match['map' .. i].winner) - foundScores = true - -- Only two opponents in CS - if winner and winner > 0 and winner <= 2 then - newScores[winner] = (newScores[winner] or 0) + 1 - end - else - break - end - end - end - if not opponent1.score and foundScores then - opponent1.score = newScores[1] or 0 - end - if not opponent2.score and foundScores then - opponent2.score = newScores[2] or 0 - end - match.opponent1 = opponent1 - match.opponent2 = opponent2 - return match +-- These maps however shouldn't be stored +-- The keepMap function will check if a map should be kept +---@param games table[] +---@return table[] +function MatchFunctions.removeUnsetMaps(games) + return Array.filter(games, MapFunctions.keepMap) end ---@param match table +---@param maps table[] ---@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_valve_tier')) - match.status = Logic.emptyOr(match.status, Variables.varDefault('tournament_status')) - - return MatchGroupInput.getCommonTournamentVars(match) -end +function MatchFunctions.getLinks(match, maps) + local platforms = mw.loadData('Module:MatchExternalLinks') + table.insert(platforms, {name = 'vod2', isMapStats = true}) ----@param match table ----@return table -function matchFunctions.getLinks(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) + return Table.map(platforms, function (key, platform) + if Logic.isEmpty(platform) then + return key, nil + end - match.links = {} + local makeLink = function(name) + local linkPrefix = platform.prefixLink or '' + local linkSuffix = platform.suffixLink or '' + return linkPrefix .. name .. linkSuffix + end - local links = match.links + local linksOfPlatform = {} + local name = platform.name - local platforms = mw.loadData('Module:MatchExternalLinks') - table.insert(platforms, {name = 'vod2', isMapStats = true}) - - for _, platform in ipairs(platforms) do - -- Stat external links inserted in {{Map}} - if Logic.isNotEmpty(platform) then - local platformLinks = {} - local name = platform.name - local prefixLink = platform.prefixLink or '' - local suffixLink = platform.suffixLink or '' - - if match[name] then - table.insert(platformLinks, {prefixLink .. match[name] .. suffixLink, 0}) - match[name] = nil - end + if match[name] then + table.insert(linksOfPlatform, {makeLink(match[name]), 0}) + end - if platform.isMapStats then - for i = 1, match.bestof do - local map = match['map' .. i] - if map and map[platform.name] then - table.insert(platformLinks, {prefixLink .. match['map' .. i][name] .. suffixLink, i}) - match['map' .. i][platform.name] = nil - end + if platform.isMapStats then + Array.forEach(maps, function(map, mapIndex) + if not map[name] then + return end - else - if platform.max then - for i = 2, platform.max, 1 do - if match[platform.name .. i] then - table.insert(platformLinks, {prefixLink .. match[name .. i] .. suffixLink, i}) - match[platform.name .. i] = nil - end - end + table.insert(linksOfPlatform, {makeLink(map[name]), mapIndex}) + end) + elseif platform.max then + for i = 2, platform.max, 1 do + if match[name .. i] then + table.insert(linksOfPlatform, {makeLink(match[name .. i]), i}) end end - - if #platformLinks > 0 then - links[name] = platformLinks - end end - end - return match + if Logic.isEmpty(linksOfPlatform) then + return name, nil + end + return name, linksOfPlatform + end) end ---@param match table ---@return string? -function matchFunctions.getMatchStatus(match) +function MatchFunctions.getMatchStatus(match) if match.resulttype == 'np' then - return match.status - else - return nil + return Logic.emptyOr(match.status, Variables.varDefault('tournament_status')) end end ---@param name string? ---@param year string|osdate ---@return number -function matchFunctions.getEarnings(name, year) +function MatchFunctions.getEarnings(name, year) if Logic.isEmpty(name) then return 0 end @@ -326,8 +218,9 @@ function matchFunctions.getEarnings(name, year) end ---@param match table +---@param opponents table[] ---@return boolean -function matchFunctions.isFeatured(match) +function MatchFunctions.isFeatured(match, opponents) if Table.includes(FEATURED_TIERS, tonumber(match.liquipediatier)) then return true end @@ -339,15 +232,14 @@ function matchFunctions.isFeatured(match) return false end - local opponent1, opponent2 = match.opponent1, match.opponent2 local year = os.date('%Y') if - opponent1.type == Opponent.team and - matchFunctions.getEarnings(opponent1.name, year) >= MIN_EARNINGS_FOR_FEATURED + opponents[1].type == Opponent.team and + MatchFunctions.getEarnings(opponents[1].name, year) >= MIN_EARNINGS_FOR_FEATURED or - opponent2.type == Opponent.team and - matchFunctions.getEarnings(opponent2.name, year) >= MIN_EARNINGS_FOR_FEATURED + opponents[2].type == Opponent.team and + MatchFunctions.getEarnings(opponents[2].name, year) >= MIN_EARNINGS_FOR_FEATURED then return true end @@ -356,78 +248,16 @@ function matchFunctions.isFeatured(match) end ---@param match table +---@param opponents table[] ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mapveto = MatchGroupInput.getMapVeto(match), - status = matchFunctions.getMatchStatus(match), +function MatchFunctions.getExtraData(match, opponents) + return { + mapveto = MatchGroupInputUtil.getMapVeto(match), + status = MatchFunctions.getMatchStatus(match), overturned = Logic.isNotEmpty(match.overturned), - featured = matchFunctions.isFeatured(match), + featured = MatchFunctions.isFeatured(match, opponents), hidden = Logic.readBool(Variables.varDefault('match_hidden')) } - return match -end - ----@param match table ----@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if TypeUtil.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == Opponent.team and not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name, { - maxNumPlayers = 5, resolveRedirect = true, applyUnderScores = true - }) - end - end - end - - -- Handle tournament status for unfinished matches - if (not Logic.readBool(match.finished)) and Logic.isNotEmpty(match.status) then - match.finished = match.status - end - - if Table.includes(NP_MATCH_STATUS, match.finished) then - match.resulttype = 'np' - match.status = match.finished - match.finished = false - match.dateexact = false - else - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - if Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match end -- @@ -438,108 +268,76 @@ end -- DUMMY_MAP_NAME needs the match the default value in Template:Map ---@param map table ---@return boolean -function mapFunctions.discardMap(map) - if map.map == DUMMY_MAP_NAME then - return true - else - return false - end +function MapFunctions.keepMap(map) + return map.map ~= DUMMY_MAP_NAME end -- Parse extradata information ---@param map table ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { +function MapFunctions.getExtraData(map) + local extradata = { comment = map.comment, } - return map + + Table.mergeInto(extradata, MapFunctions._getHalfScores(map)) + + return extradata end ---@param map table ---@return table -function mapFunctions._getHalfScores(map) - map.extradata['t1sides'] = {} - map.extradata['t2sides'] = {} - map.extradata['t1halfs'] = {} - map.extradata['t2halfs'] = {} +function MapFunctions._getHalfScores(map) + local t1sides = {} + local t2sides = {} + local t1halfs = {} + local t2halfs = {} - local key = '' + local prefix = '' local overtimes = 0 - local function getOpossiteSide(side) + local function getOppositeSide(side) return side == 'ct' and 't' or 'ct' end while true do - local t1Side = map[key .. 't1firstside'] + local t1Side = map[prefix .. 't1firstside'] if Logic.isEmpty(t1Side) or (t1Side ~= 'ct' and t1Side ~= 't') then break end - local t2Side = getOpossiteSide(t1Side) + local t2Side = getOppositeSide(t1Side) -- Iterate over two Halfs (In regular time a half is 15 rounds, after that sides switch) for _ = 1, 2, 1 do - if(map[key .. 't1' .. t1Side] and map[key .. 't2' .. t2Side]) then - table.insert(map.extradata['t1sides'], t1Side) - table.insert(map.extradata['t2sides'], t2Side) - table.insert(map.extradata['t1halfs'], tonumber(map[key .. 't1' .. t1Side]) or 0) - table.insert(map.extradata['t2halfs'], tonumber(map[key .. 't2' .. t2Side]) or 0) - map[key .. 't1' .. t1Side] = nil - map[key .. 't2' .. t2Side] = nil + if(map[prefix .. 't1' .. t1Side] and map[prefix .. 't2' .. t2Side]) then + table.insert(t1sides, t1Side) + table.insert(t2sides, t2Side) + table.insert(t1halfs, tonumber(map[prefix .. 't1' .. t1Side]) or 0) + table.insert(t2halfs, tonumber(map[prefix .. 't2' .. t2Side]) or 0) -- second half (sides switch) t1Side, t2Side = t2Side, t1Side end end overtimes = overtimes + 1 - key = 'o' .. overtimes + prefix = 'o' .. overtimes end - return map + return { + t1sides = t1sides, + t2sides = t2sides, + t1halfs = t1halfs, + t2halfs = t2halfs, + } end --- Calculate Score and Winner of the map --- Use the half information if available ---@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - - map = mapFunctions._getHalfScores(map) - - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score - if Table.includes(ALLOWED_STATUSES, map['score' .. scoreIndex]) then - score = map['score' .. scoreIndex] - elseif Logic.isNotEmpty(map.extradata['t' .. scoreIndex .. 'halfs']) then - score = MathUtil.sum(map.extradata['t' .. scoreIndex .. 'halfs']) - else - score = tonumber(map['score' .. scoreIndex]) - end - local obj = {} - if not Logic.isEmpty(score) then - if TypeUtil.isNumeric(score) then - obj.status = 'S' - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - end - map.scores[scoreIndex] = score - indexedScores[scoreIndex] = obj - end +---@return fun(opponentIndex: integer): integer? +function MapFunctions.calculateMapScore(map) + local halfs = MapFunctions._getHalfScores(map) + return function(opponentIndex) + return Array.reduce(halfs['t' .. opponentIndex .. 'halfs'], Operator.add) end - - if map.finished == 'skip' then - map.resulttype = 'np' - else - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - end - - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/counterstrike/match_summary.lua b/components/match2/wikis/counterstrike/match_summary.lua index 82bbd832af5..beb87fbc98d 100644 --- a/components/match2/wikis/counterstrike/match_summary.lua +++ b/components/match2/wikis/counterstrike/match_summary.lua @@ -21,11 +21,11 @@ local MatchSummary = Lua.import('Module:MatchSummary/Base') local GREEN_CHECK = Icon.makeIcon{iconName = 'winner', color = 'forest-green-text', size = '110%'} local NO_CHECK = '[[File:NoCheck.png|link=]]' -local ARROW_LEFT = '[[File:Arrow sans left.svg|15x15px|link=|Left team starts]]' -local ARROW_RIGHT = '[[File:Arrow sans right.svg|15x15px|link=|Right team starts]]' local TBD = 'TBD' +local CustomMatchSummary = {} + -- Score Class ---@class CounterstrikeScore ---@operator call: CounterstrikeScore @@ -106,121 +106,16 @@ function Score:create() return self.root end --- Map Veto Class ----@class CounterstrikeMapVeto: MatchSummaryRowInterface ----@operator call: CounterstrikeMapVeto ----@field root Html ----@field table Html -local MapVeto = Class.new( - function(self) - self.root = mw.html.create('div'):addClass('brkts-popup-mapveto') - self.table = self.root:tag('table') - :addClass('wikitable-striped'):addClass('collapsible'):addClass('collapsed') - self:createHeader() - end -) - ----@return self -function MapVeto:createHeader() - self.table:tag('tr') - :tag('th'):css('width','33%'):done() - :tag('th'):css('width','34%'):wikitext('Map Veto'):done() - :tag('th'):css('width','33%'):done() - return self -end - ----@param firstVeto number? ----@return self -function MapVeto:vetoStart(firstVeto) - local textLeft - local textCenter - local textRight - if firstVeto == 1 then - textLeft = 'Start Map Veto' - textCenter = ARROW_LEFT - elseif firstVeto == 2 then - textCenter = ARROW_RIGHT - textRight = 'Start Map Veto' - else return self end - self.table:tag('tr'):addClass('brkts-popup-mapveto-vetostart') - :tag('th'):wikitext(textLeft or ''):done() - :tag('th'):wikitext(textCenter):done() - :tag('th'):wikitext(textRight or ''):done() - return self -end - ----@param map string? ----@return self -function MapVeto:addDecider(map) - map = Logic.emptyOr(map, TBD) - - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - self:addColumnVetoMap(row, map) - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - - self.table:node(row) - return self -end - ----@param vetotype string? ----@param map1 string? ----@param map2 string? ----@return self -function MapVeto:addRound(vetotype, map1, map2) - map1 = Logic.emptyOr(map1, TBD) - map2 = Logic.emptyOr(map2, TBD) - - local class - local vetoText - if vetotype == 'ban' then - vetoText = 'BAN' - class = 'brkts-popup-mapveto-ban' - elseif vetotype == 'pick' then - vetoText = 'PICK' - class = 'brkts-popup-mapveto-pick' - elseif vetotype == 'defaultban' then - vetoText = 'DEFAULT BAN' - class = 'brkts-popup-mapveto-defaultban' - else - return self - end - - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoMap(row, map1) - self:addColumnVetoType(row, class, vetoText) - self:addColumnVetoMap(row, map2) - - self.table:node(row) - return self -end - ----@param row Html ----@param styleClass string ----@param vetoText string ----@return self -function MapVeto:addColumnVetoType(row, styleClass, vetoText) - row:tag('td') - :tag('span') - :addClass(styleClass) - :addClass('brkts-popup-mapveto-vetotype') - :wikitext(vetoText) - return self -end +---@class CounterstrikeMapVeto: VetoDisplay +---@field game string? +local MapVeto = Class.new(MatchSummary.MapVeto, function(self, game) + self.game = game +end) ----@param row Html ---@param map string? ----@return self -function MapVeto:addColumnVetoMap(row, map) - row:tag('td'):wikitext(map):done() - return self -end - ----@return Html -function MapVeto:create() - return self.root +---@return string +function MapVeto:displayMap(map) + return Logic.nilIfEmpty(CustomMatchSummary._createMapLink(map, self.game)) or TBD end ---@class CounterstrikeMatchStatus: MatchSummaryRowInterface @@ -248,8 +143,6 @@ function MatchStatus:create() return self.root end -local CustomMatchSummary = {} - ---@param args table ---@return Html function CustomMatchSummary.getByMatchId(args) @@ -306,27 +199,7 @@ function CustomMatchSummary.createBody(match) end -- Add the Map Vetoes - if match.extradata.mapveto then - local vetoData = match.extradata.mapveto - if vetoData then - local mapVeto = MapVeto() - - for _,vetoRound in ipairs(vetoData) do - if vetoRound.vetostart then - mapVeto:vetoStart(tonumber(vetoRound.vetostart)) - end - if vetoRound.type == 'decider' then - mapVeto:addDecider(CustomMatchSummary._createMapLink(vetoRound.decider, match.game)) - else - mapVeto:addRound(vetoRound.type, - CustomMatchSummary._createMapLink(vetoRound.team1, match.game), - CustomMatchSummary._createMapLink(vetoRound.team2, match.game)) - end - end - - body:addRow(mapVeto) - end - end + body:addRow(MatchSummary.defaultMapVetoDisplay(match, MapVeto(match.game))) -- Match Status (postponed/ cancel(l)ed) if match.extradata.status then @@ -462,13 +335,17 @@ function CustomMatchSummary._createMap(game) local row = MatchSummary.Row() local extradata = game.extradata or {} + local function scoreDisplay(oppIdx) + return DisplayHelper.MapScore(game.scores[oppIdx], oppIdx, game.resultType, game.walkover, game.winner) + end + -- Score local team1Score = Score():setLeft() local team2Score = Score('rtl'):setRight() -- Teams map score - team1Score:setMapScore(game.scores[1]) - team2Score:setMapScore(game.scores[2]) + team1Score:setMapScore(scoreDisplay(1)) + team2Score:setMapScore(scoreDisplay(2)) local t1sides = extradata['t1sides'] or {} local t2sides = extradata['t2sides'] or {} diff --git a/components/match2/wikis/criticalops/match_group_input_custom.lua b/components/match2/wikis/criticalops/match_group_input_custom.lua index 6ecae1a8ddb..59ac2e1917f 100644 --- a/components/match2/wikis/criticalops/match_group_input_custom.lua +++ b/components/match2/wikis/criticalops/match_group_input_custom.lua @@ -6,369 +6,155 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- -local DateExt = require('Module:Date/Ext') -local Json = require('Module:Json') +local Array = require('Module:Array') local Logic = require('Module:Logic') -local MathUtil = require('Module:MathUtil') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') +local MathUtil = require('Module:MathUtil') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') local Table = require('Module:Table') -local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') -local Streams = require('Module:Links/Stream') -local Opponent = Lua.import('Module:Opponent') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') +local DEFAULT_MODE = 'team' +local DUMMY_MAP = 'null' -- Is set in Template:Map when |map= is empty. local SIDE_DEF = 'ct' local SIDE_ATK = 't' -local STATUS_SCORE = 'S' -local STATUS_DRAW = 'D' -local STATUS_DEFAULT_WIN = 'W' -local STATUS_FORFEIT = 'FF' -local STATUS_DISQUALIFIED = 'DQ' -local STATUS_DEFAULT_LOSS = 'L' -local ALLOWED_STATUSES = { - STATUS_DRAW, - STATUS_DEFAULT_WIN, - STATUS_FORFEIT, - STATUS_DISQUALIFIED, - STATUS_DEFAULT_LOSS, -} -local NOT_PLAYED_MATCH_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local NOT_PLAYED_RESULT_TYPE = 'np' -local DRAW_RESULT_TYPE = 'draw' -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) -local NOT_PLAYED_SCORE = -1 -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_PLAYERS = 10 -local DEFAULT_RESULT_TYPE = 'default' -local DUMMY_MAP_NAME = 'null' -- Is set in Template:Map when |map= is empty. -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} -- called from Module:MatchGroup ---@param match table +---@param options table? ---@return table -function CustomMatchGroupInput.processMatch(match) - -- Count number of maps, check for empty maps to remove, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.removeUnsetMaps(match) - match = matchFunctions.getScoreFromMapWinners(match) - - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getExtraData(match) +function CustomMatchGroupInput.processMatch(match, options) + match.finished = Logic.nilIfEmpty(match.finished) or match.status - return match -end + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) - - return map -end + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + local games = MatchFunctions.extractMaps(match, #opponents) + match.bestof = MatchGroupInputUtil.getBestOf(nil, games) + games = MatchFunctions.removeUnsetMaps(games) - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) --- function to check for draws ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckDraw(tbl) - if #tbl < MAX_NUM_OPPONENTS then - return false + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - return MatchGroupInput.isDraw(tbl) -end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - local winner = tonumber(data.winner) - if Logic.readBool(data.finished) then - if CustomMatchGroupInput.placementCheckDraw(indexedScores) then - data.winner = 0 - data.resulttype = DRAW_RESULT_TYPE - indexedScores = MatchGroupInput.setPlacement(indexedScores, data.winner, 1, 1) - elseif CustomMatchGroupInput.placementCheckSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = DEFAULT_RESULT_TYPE - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = STATUS_FORFEIT - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = STATUS_DISQUALIFIED - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = STATUS_DEFAULT_LOSS - end - indexedScores = MatchGroupInput.setPlacement(indexedScores, data.winner, 1, 2) - elseif CustomMatchGroupInput.placementCheckScoresSet(indexedScores) then - --C-OPS only has exactly 2 opponents, neither more or less - if #indexedScores == MAX_NUM_OPPONENTS then - if tonumber(indexedScores[1].score) > tonumber(indexedScores[2].score) then - data.winner = 1 - else - data.winner = 2 - end - indexedScores = MatchGroupInput.setPlacement(indexedScores, data.winner, 1, 2) - end - end - --If a manual winner is set use it - if winner and data.resulttype ~= DEFAULT_RESULT_TYPE then - if winner == 0 then - data.resulttype = DRAW_RESULT_TYPE - else - data.resulttype = nil - end - data.winner = winner - indexedScores = MatchGroupInput.setPlacement(indexedScores, winner, 1, 2) - end - end - return data, indexedScores -end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) + match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) --- Check if any team has a none-standard status ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckSpecialStatus(tbl) - return Table.any(tbl, - function (_, scoreinfo) - return scoreinfo.status ~= STATUS_SCORE and String.isNotEmpty(scoreinfo.status) - end - ) -end + match.games = games + match.opponents = opponents ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckScoresSet(tbl) - return Table.all(tbl, function (_, scoreinfo) return scoreinfo.status == STATUS_SCORE end) + match.extradata = MatchFunctions.getExtraData(match) + + return match end -- -- match related functions -- ----@param match table ----@return table -function matchFunctions.getBestOf(match) - local mapCount = 0 - for _, _, mapIndex in Table.iter.pairsByPrefix(match, 'map') do - mapCount = mapIndex - end - match.bestof = mapCount - return match -end - -- Template:Map sets a default map name so we can count the number of maps. --- These maps however shouldn't be stored in lpdb, nor displayed --- The discardMap function will check if a map should be removed --- Remove all maps that should be removed. ----@param match table ----@return table -function matchFunctions.removeUnsetMaps(match) - for mapKey, map in Table.iter.pairsByPrefix(match, 'map') do - if map.map == DUMMY_MAP_NAME then - match[mapKey] = nil - end - end - return match +-- These maps however shouldn't be stored +-- The keepMap function will check if a map should be kept +---@param games table[] +---@return table[] +function MatchFunctions.removeUnsetMaps(games) + return Array.filter(games, MapFunctions.keepMap) end --- Calculate the match scores based on the map results. --- If it's a Best of 1, we'll take the exact score of that map --- If it's not a Best of 1, we should count the map wins --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ---@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - -- For best of 1, display the results of the single map - local opponent1 = match.opponent1 - local opponent2 = match.opponent2 - local newScores = {} - local foundScores = false - if match.bestof == 1 then - if match.map1 then - newScores = match.map1.scores - foundScores = true - end - else -- For best of >1, disply the map wins - for _, map in Table.iter.pairsByPrefix(match, 'map') do - local winner = tonumber(map.winner) - foundScores = true - -- Only two opponents in C-OPS - if winner and winner > 0 and winner <= 2 then - newScores[winner] = (newScores[winner] or 0) + 1 - end +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }, MapFunctions.calculateMapScore(map)) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - if not opponent1.score and foundScores then - opponent1.score = newScores[1] or 0 - end - if not opponent2.score and foundScores then - opponent2.score = newScores[2] or 0 - end - match.opponent1 = opponent1 - match.opponent2 = opponent2 - return match -end ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - return MatchGroupInput.getCommonTournamentVars(match) -end + table.insert(maps, map) + match[key] = nil + end ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - return match + return maps end ----@param match table ----@return string? -function matchFunctions.getMatchStatus(match) - if match.resulttype == NOT_PLAYED_RESULT_TYPE then - return match.status - else - return nil +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mapveto = MatchGroupInput.getMapVeto(match), - status = matchFunctions.getMatchStatus(match), - } - return match -end - ----@param match table ----@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if TypeUtil.isNumeric(opponent.score) then - opponent.status = STATUS_SCORE - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = NOT_PLAYED_SCORE - end - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == Opponent.team and not Logic.isEmpty(opponent.name) then - match = matchFunctions.getPlayers(match, opponentIndex, opponent.name) - end - end - end - - -- Handle tournament status for unfinished matches - if (not Logic.readBool(match.finished)) and Logic.isNotEmpty(match.status) then - match.finished = match.status - end - - if Table.includes(NOT_PLAYED_MATCH_STATUSES, match.finished) then - match.resulttype = NOT_PLAYED_MATCH_STATUSES - match.status = match.finished - match.finished = false - match.dateexact = false - else - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - if Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match +function MatchFunctions.getLinks(match) + return {} end --- Get Playerdata from Vars (get's set in TeamCards) ---@param match table ----@param opponentIndex integer ----@param teamName string ---@return table -function matchFunctions.getPlayers(match, opponentIndex, teamName) - -- match._storePlayers will break after the first empty player. let's make sure we don't leave any gaps. - local count = 1 - for playerIndex = 1, MAX_NUM_PLAYERS do - -- parse player - local player = match['opponent' .. opponentIndex .. '_p' .. playerIndex] or {} - player = Json.parseIfString(player) - local playerPrefix = teamName .. '_p' .. playerIndex - player.name = player.name or Variables.varDefault(playerPrefix) - player.flag = player.flag or Variables.varDefault(playerPrefix .. 'flag') - player.displayname = player.displayname or Variables.varDefault(playerPrefix .. 'dn') - if not Table.isEmpty(player) then - match['opponent' .. opponentIndex .. '_p' .. count] = player - count = count + 1 - end - end - return match +function MatchFunctions.getExtraData(match) + return { + mapveto = MatchGroupInputUtil.getMapVeto(match), + status = match.resulttype == MatchGroupInputUtil.RESULT_TYPE.NOT_PLAYED and match.status or nil, + } end -- @@ -379,104 +165,82 @@ end -- DUMMY_MAP_NAME needs the match the default value in Template:Map ---@param map table ---@return boolean -function mapFunctions.discardMap(map) - return map.map == DUMMY_MAP_NAME +function MapFunctions.keepMap(map) + return map.map ~= DUMMY_MAP end --- Parse extradata information ---@param map table ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { - comment = map.comment, - } - return map +function MapFunctions.getExtraData(map) + local extradata = MapFunctions.getSideData(map) + + return Table.merge(extradata, {comment = map.comment}) end ---@param map table ---@return table -function mapFunctions._getHalfScores(map) - map.extradata.t1sides = {} - map.extradata.t2sides = {} - map.extradata.t1halfs = {} - map.extradata.t2halfs = {} - - local key = '' - local overtimes = 0 - - local function getOppositeSide(side) - return side == SIDE_DEF and SIDE_ATK or SIDE_DEF - end - - while true do - local t1Side = map[key .. 't1firstside'] - if Logic.isEmpty(t1Side) or (t1Side ~= SIDE_DEF and t1Side ~= SIDE_ATK) then - break - end - local t2Side = getOppositeSide(t1Side) - - -- Iterate over two Halfs (In regular time a half is 15 rounds, after that sides switch) - for _ = 1, 2, 1 do - if(map[key .. 't1' .. t1Side] and map[key .. 't2' .. t2Side]) then - table.insert(map.extradata.t1sides, t1Side) - table.insert(map.extradata.t2sides, t2Side) - table.insert(map.extradata.t1halfs, tonumber(map[key .. 't1' .. t1Side]) or 0) - table.insert(map.extradata.t2halfs, tonumber(map[key .. 't2' .. t2Side]) or 0) - map[key .. 't1' .. t1Side] = nil - map[key .. 't2' .. t2Side] = nil - -- second half (sides switch) - t1Side, t2Side = t2Side, t1Side - end - end - - overtimes = overtimes + 1 - key = 'o' .. overtimes - end - - return map +function MapFunctions.getSideData(map) + ---@param sideInput string + ---@return boolean + local isValidSide = function(sideInput) + return Logic.isNotEmpty(sideInput) and (sideInput == SIDE_DEF or sideInput == SIDE_ATK) + end + + ---@param prefix string + ---@param t1Side string + ---@param t2Side string + ---@return {t1Side: string, t2Side: string, t1Half: integer, t2Half: integer}? + local getDataFor = function(prefix, t1Side, t2Side) + local half1 = tonumber(map[prefix .. 't1' .. t1Side]) + local half2 = tonumber(map[prefix .. 't2' .. t2Side]) + if not half1 or not half2 then return end + return { + t1Side = t1Side, + t2Side = t2Side, + t1Half = half1, + t2Half = half2, + } + end + + ---@param prefix string + ---@return {t1Side: string, t2Side: string, t1Half: integer, t2Half: integer}[]? + local getSideData = function(prefix) + local t1Side = map[prefix .. 't1firstside'] + if not isValidSide(t1Side) then return end + local t2Side = t1Side == SIDE_DEF and SIDE_ATK or SIDE_DEF + + return Array.append({}, + getDataFor(prefix, t1Side, t2Side), + getDataFor(prefix, t2Side, t1Side) + ) + end + + local sideData = getSideData('') or {} + + Array.extendWith(sideData, Array.mapIndexes(function(overtimeIndex) + return getSideData('o' .. overtimeIndex) + end)) + + return { + t1sides = Array.map(sideData, Operator.property('t1Side')), + t2sides = Array.map(sideData, Operator.property('t2Side')), + t1halfs = Array.map(sideData, Operator.property('t1Half')), + t2halfs = Array.map(sideData, Operator.property('t2Half')), + } end --- Calculate Score and Winner of the map --- Use the half information if available ---@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - - map = mapFunctions._getHalfScores(map) - - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score - if Table.includes(ALLOWED_STATUSES, map['score' .. scoreIndex]) then - score = map['score' .. scoreIndex] - elseif Logic.isNotEmpty(map.extradata['t' .. scoreIndex .. 'halfs']) then - score = MathUtil.sum(map.extradata['t' .. scoreIndex .. 'halfs']) - else - score = tonumber(map['score' .. scoreIndex]) +---@return fun(opponentIndex: integer): integer? +function MapFunctions.calculateMapScore(map) + local sideData = MapFunctions.getSideData(map) + return function(opponentIndex) + local partialScores = sideData['t' .. opponentIndex .. 'halfs'] + if Logic.isEmpty(partialScores) then + return end - local obj = {} - if not Logic.isEmpty(score) then - if TypeUtil.isNumeric(score) then - obj.status = STATUS_SCORE - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = NOT_PLAYED_SCORE - end - map.scores[scoreIndex] = score - indexedScores[scoreIndex] = obj - end - end - if Table.includes(NOT_PLAYED_MATCH_STATUSES, map.finished) then - map.resulttype = NOT_PLAYED_RESULT_TYPE - else - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) + return MathUtil.sum(partialScores) end - - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/criticalops/match_summary.lua b/components/match2/wikis/criticalops/match_summary.lua index 3ce30aed072..9c4a29b8bde 100644 --- a/components/match2/wikis/criticalops/match_summary.lua +++ b/components/match2/wikis/criticalops/match_summary.lua @@ -18,11 +18,6 @@ local MatchSummary = Lua.import('Module:MatchSummary/Base') local GREEN_CHECK = Icon.makeIcon{iconName = 'winner', color = 'forest-green-text', size = '110%'} local NO_CHECK = '[[File:NoCheck.png|link=]]' -local ARROW_LEFT = '[[File:Arrow sans left.svg|15x15px|link=|Left team starts]]' -local ARROW_RIGHT = '[[File:Arrow sans right.svg|15x15px|link=|Right team starts]]' - -local TBD = 'TBD' - -- Score Class ---@class CriticalopsScore ---@operator call(string|number|nil): CriticalopsScore @@ -103,123 +98,6 @@ function Score:create() return self.root end --- Map Veto Class ----@class CriticalopsMapVeto: MatchSummaryRowInterface ----@operator call: CriticalopsMapVeto ----@field root Html ----@field table Html -local MapVeto = Class.new( - function(self) - self.root = mw.html.create('div'):addClass('brkts-popup-mapveto') - self.table = self.root:tag('table') - :addClass('wikitable-striped'):addClass('collapsible'):addClass('collapsed') - self:createHeader() - end -) - ----@return CriticalopsMapVeto -function MapVeto:createHeader() - self.table:tag('tr') - :tag('th'):css('width','33%'):done() - :tag('th'):css('width','34%'):wikitext('Map Veto'):done() - :tag('th'):css('width','33%'):done() - return self -end - ----@param firstVeto number? ----@return CriticalopsMapVeto -function MapVeto:vetoStart(firstVeto) - local textLeft - local textCenter - local textRight - if firstVeto == 1 then - textLeft = 'Start Map Veto' - textCenter = ARROW_LEFT - elseif firstVeto == 2 then - textCenter = ARROW_RIGHT - textRight = 'Start Map Veto' - else return self end - self.table:tag('tr'):addClass('brkts-popup-mapveto-vetostart') - :tag('th'):wikitext(textLeft or ''):done() - :tag('th'):wikitext(textCenter):done() - :tag('th'):wikitext(textRight or ''):done() - return self -end - ----@param map string? ----@return CriticalopsMapVeto -function MapVeto:addDecider(map) - map = Logic.emptyOr(map, TBD) - - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - self:addColumnVetoMap(row, map) - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - - self.table:node(row) - return self -end - ----@param vetotype string? ----@param map1 string? ----@param map2 string? ----@return CriticalopsMapVeto -function MapVeto:addRound(vetotype, map1, map2) - map1 = Logic.emptyOr(map1, TBD) - map2 = Logic.emptyOr(map2, TBD) - - local class - local vetoText - if vetotype == 'ban' then - vetoText = 'BAN' - class = 'brkts-popup-mapveto-ban' - elseif vetotype == 'pick' then - vetoText = 'PICK' - class = 'brkts-popup-mapveto-pick' - elseif vetotype == 'defaultban' then - vetoText = 'DEFAULT BAN' - class = 'brkts-popup-mapveto-defaultban' - else - return self - end - - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoMap(row, map1) - self:addColumnVetoType(row, class, vetoText) - self:addColumnVetoMap(row, map2) - - self.table:node(row) - return self -end - ----@param row Html ----@param styleClass string ----@param vetoText string ----@return CriticalopsMapVeto -function MapVeto:addColumnVetoType(row, styleClass, vetoText) - row:tag('td') - :tag('span') - :addClass(styleClass) - :addClass('brkts-popup-mapveto-vetotype') - :wikitext(vetoText) - return self -end - ----@param row Html ----@param map string? ----@return CriticalopsMapVeto -function MapVeto:addColumnVetoMap(row, map) - row:tag('td'):wikitext(map):done() - return self -end - ----@return Html -function MapVeto:create() - return self.root -end - ---@class CriticalopsMatchStatus: MatchSummaryRowInterface ---@operator call: CriticalopsMatchStatus ---@field root Html @@ -277,25 +155,7 @@ function CustomMatchSummary.createBody(match) end -- Add the Map Vetoes - if match.extradata.mapveto then - local vetoData = match.extradata.mapveto - if vetoData then - local mapVeto = MapVeto() - - for _,vetoRound in ipairs(vetoData) do - if vetoRound.vetostart then - mapVeto:vetoStart(tonumber(vetoRound.vetostart)) - end - if vetoRound.type == 'decider' then - mapVeto:addDecider(vetoRound.decider) - else - mapVeto:addRound(vetoRound.type, vetoRound.team1, vetoRound.team2) - end - end - - body:addRow(mapVeto) - end - end + body:addRow(MatchSummary.defaultMapVetoDisplay(match)) -- Match Status (postponed/ cancel(l)ed) if match.extradata.status then @@ -318,8 +178,8 @@ function CustomMatchSummary._createMap(game) local team2Score = Score('rtl'):setRight() -- Teams map score - team1Score:setMapScore(game.scores[1]) - team2Score:setMapScore(game.scores[2]) + team1Score:setMapScore(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) + team2Score:setMapScore(DisplayHelper.MapScore(game.scores[2], 2, game.resultType, game.walkover, game.winner)) local t1sides = extradata['t1sides'] or {} local t2sides = extradata['t2sides'] or {} diff --git a/components/match2/wikis/crossfire/match_group_input_custom.lua b/components/match2/wikis/crossfire/match_group_input_custom.lua index 969b59e804a..bb92e3ced09 100644 --- a/components/match2/wikis/crossfire/match_group_input_custom.lua +++ b/components/match2/wikis/crossfire/match_group_input_custom.lua @@ -9,179 +9,78 @@ local Array = require('Module:Array') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local Opponent = require('Module:Opponent') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') local Table = require('Module:Table') -local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') -local DateExt = require('Module:Date/Ext') -local Streams = require('Module:Links/Stream') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local NP_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local ALLOWED_STATUSES = { 'W', 'FF', 'DQ', 'L', 'D' } -local MAX_NUM_OPPONENTS = 8 -local MAX_NUM_MAPS = 9 local DEFAULT_BESTOF = 3 - -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_MODE = 'team' +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, + maxNumPlayers = 5 +} -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} -- called from Module:MatchGroup ---@param match table +---@param options table? ---@return table -function CustomMatchGroupInput.processMatch(match) - -- Count number of maps, check for empty maps to remove, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) - - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) - - return match -end - --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) - - return map -end - ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() - - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) +function CustomMatchGroupInput.processMatch(match, options) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) + + local games = MatchFunctions.extractMaps(match, #opponents) + + match.bestof = MatchFunctions.getBestOf(match.bestof) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if - Table.includes(NP_STATUSES, data.finished) or - Table.includes(NP_STATUSES, data.winner) - then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = 'default' - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'default') - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end - end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) - --set it as finished if we have a winner - if not Logic.isEmpty(data.winner) then - data.finished = true - end + match.games = games + match.opponents = opponents - return data, indexedScores -end - ----@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ----@return table[] ----@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == 'default' then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local lastScore = -99 - local lastPlacement = -99 - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and Logic.isEmpty(winner) then - if finished then - winner = scoreIndex - end - end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or counter - lastPlacement = counter - lastScore = score or -99 - end - end - end + match.extradata = MatchFunctions.getExtraData(match) - return opponents, winner -end - ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score) or -99 - local value2 = tonumber(tbl[key2].score) or -99 - return value1 > value2 + return match end -- @@ -189,182 +88,89 @@ end -- ---@param match table ----@return table -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match -end - --- Calculate the match scores based on the map results (counting map wins) --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local opponentNumber = 0 - for index = 1, MAX_NUM_OPPONENTS do - if String.isEmpty(match['opponent' .. index]) then - break - end - opponentNumber = index - end - local newScores = {} - local foundScores = false - - for i = 1, MAX_NUM_MAPS do - if match['map'..i] then - local winner = tonumber(match['map'..i].winner) - foundScores = true - if winner and winner > 0 and winner <= opponentNumber then - newScores[winner] = (newScores[winner] or 0) + 1 - end - else - break +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - for index = 1, opponentNumber do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end + table.insert(maps, map) + match[key] = nil end - return match + return maps end ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) - match = MatchGroupInput.getCommonTournamentVars(match) + if bestof then + Variables.varDefine('bestof', bestof) + return bestof + end - return match + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF end ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - - return match +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + end end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mvp = MatchGroupInput.readMvp(match), - casters = MatchGroupInput.readCasters(match, {noSort = true}) - } - return match +function MatchFunctions.getLinks(match) + return {} end ---@param match table ---@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if TypeUtil.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == Opponent.team and not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name, { - maxNumPlayers = 5, resolveRedirect = true, applyUnderScores = true - }) - end - end - end - - -- see if match should actually be finished if bestof limit was reached - match.finished = Logic.readBool(match.finished) - or isScoreSet and ( - Array.any(opponents, function(opponent) return (tonumber(opponent.score) or 0) > match.bestof/2 end) - or Array.all(opponents, function(opponent) return (tonumber(opponent.score) or 0) == match.bestof/2 end) - ) - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if not String.isEmpty(match.winner) or Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), + casters = MatchGroupInputUtil.readCasters(match, {noSort = true}), + } end -- -- map related functions -- --- Parse extradata information ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { +function MapFunctions.getExtraData(map, opponentCount) + return { comment = map.comment, header = map.header, } - return map -end - --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - if TypeUtil.isNumeric(score) then - obj.status = 'S' - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/crossfire/match_summary.lua b/components/match2/wikis/crossfire/match_summary.lua index 116d75de43d..15839bd1ce0 100644 --- a/components/match2/wikis/crossfire/match_summary.lua +++ b/components/match2/wikis/crossfire/match_summary.lua @@ -85,14 +85,6 @@ function CustomMatchSummary.createBody(match) return body end ----@param game MatchGroupUtilGame ----@param opponentIndex integer ----@return Html -function CustomMatchSummary._gameScore(game, opponentIndex) - local score = game.scores[opponentIndex] - return mw.html.create('div'):wikitext(score) -end - ---@param game MatchGroupUtilGame ---@return MatchSummaryRow function CustomMatchSummary._createMapRow(game) @@ -121,11 +113,11 @@ function CustomMatchSummary._createMapRow(game) local leftNode = mw.html.create('div') :addClass('brkts-popup-spaced') :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 1, Icons.CHECK)) - :node(CustomMatchSummary._gameScore(game, 1)) + :node(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) local rightNode = mw.html.create('div') :addClass('brkts-popup-spaced') - :node(CustomMatchSummary._gameScore(game, 2)) + :node(DisplayHelper.MapScore(game.scores[2], 2, game.resultType, game.walkover, game.winner)) :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 2, Icons.CHECK)) row:addElement(leftNode) diff --git a/components/match2/wikis/deadlock/match_group_input_custom.lua b/components/match2/wikis/deadlock/match_group_input_custom.lua index 7e4b88decde..17a6fe2abcf 100644 --- a/components/match2/wikis/deadlock/match_group_input_custom.lua +++ b/components/match2/wikis/deadlock/match_group_input_custom.lua @@ -19,7 +19,6 @@ local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') local OPPONENT_CONFIG = { resolveRedirect = true, pagifyTeamNames = true, - pagifyPlayerNames = true, maxNumPlayers = 10, } @@ -64,7 +63,7 @@ function CustomMatchGroupInput.processMatch(match, options) MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - MatchGroupInputUtil.getCommonTournamentVars(match) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) match.stream = Streams.processStreams(match) @@ -114,8 +113,6 @@ function MatchFunctions.extractMaps(match, opponents) return maps end -CustomMatchGroupInput.processMap = FnUtil.identity - ---@param maps table[] ---@return fun(opponentIndex: integer): integer function MatchFunctions.calculateMatchScore(maps) diff --git a/components/match2/wikis/dota2/match_group_input_custom.lua b/components/match2/wikis/dota2/match_group_input_custom.lua index daf1e1e9ac9..64269a3a4dc 100644 --- a/components/match2/wikis/dota2/match_group_input_custom.lua +++ b/components/match2/wikis/dota2/match_group_input_custom.lua @@ -23,7 +23,6 @@ local MatchGroupUtil = Lua.import('Module:MatchGroup/Util') local OPPONENT_CONFIG = { resolveRedirect = true, pagifyTeamNames = false, - pagifyPlayerNames = true, maxNumPlayers = 15, } local DEFAULT_MODE = 'team' @@ -80,7 +79,7 @@ function CustomMatchGroupInput.processMatchWithoutStandalone(MatchParser, match) local opponents = Array.mapIndexes(function(opponentIndex) return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) end) - local games = MatchFunctions.extractMaps(MatchParser, match, #opponents) + local games = MatchFunctions.extractMaps(MatchParser, match, opponents) match.bestof = MatchGroupInputUtil.getBestOf(match.bestof, games) local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) @@ -105,7 +104,8 @@ function CustomMatchGroupInput.processMatchWithoutStandalone(MatchParser, match) MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) match.stream = Streams.processStreams(match) match.links = MatchFunctions.getLinks(match, games) @@ -120,9 +120,9 @@ end ---@param MatchParser Dota2MatchParserInterface ---@param match table ----@param opponentCount integer +---@param opponents table[] ---@return table[] -function MatchFunctions.extractMaps(MatchParser, match, opponentCount) +function MatchFunctions.extractMaps(MatchParser, match, opponents) local maps = {} for key, mapInput, mapIndex in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do local map = MatchParser.getMap(mapInput) @@ -136,11 +136,11 @@ function MatchFunctions.extractMaps(MatchParser, match, opponentCount) map.length = MatchParser.getLength(map) map.vod = map.vod or String.nilIfEmpty(match['vodgame' .. mapIndex]) map.publisherid = map.matchid or String.nilIfEmpty(match['matchid' .. mapIndex]) - map.participants = MapFunctions.getParticipants(MatchParser, map, opponentCount) - map.extradata = MapFunctions.getExtraData(MatchParser, map, opponentCount) + map.participants = MapFunctions.getParticipants(MatchParser, map, opponents) + map.extradata = MapFunctions.getExtraData(MatchParser, map, #opponents) map.finished = MatchGroupInputUtil.mapIsFinished(map) - local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local opponentInfo = Array.map(opponents, function(_, opponentIndex) local score, status = MatchGroupInputUtil.computeOpponentScore({ walkover = map.walkover, winner = map.winner, @@ -164,8 +164,6 @@ function MatchFunctions.extractMaps(MatchParser, match, opponentCount) return maps end -CustomMatchGroupInput.processMap = FnUtil.identity - ---@param maps table[] ---@return fun(opponentIndex: integer): integer function MatchFunctions.calculateMatchScore(maps) @@ -174,14 +172,6 @@ function MatchFunctions.calculateMatchScore(maps) end end ----@param match table ----@return table -function MatchFunctions.getTournamentVars(match) - match.headtohead = Logic.emptyOr(match.headtohead, Variables.varDefault('headtohead')) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) - return MatchGroupInputUtil.getCommonTournamentVars(match) -end - ---@param match table ---@param games table[] ---@return table @@ -211,7 +201,7 @@ end function MatchFunctions.getExtraData(match) return { mvp = MatchGroupInputUtil.readMvp(match), - headtohead = match.headtohead, + headtohead = Logic.emptyOr(match.headtohead, Variables.varDefault('headtohead')), casters = MatchGroupInputUtil.readCasters(match, {noSort = true}), } end @@ -261,20 +251,34 @@ end -- Parse participant information ---@param MatchParser Dota2MatchParserInterface ---@param map table ----@param opponentCount integer +---@param opponents table[] ---@return table -function MapFunctions.getParticipants(MatchParser, map, opponentCount) - local participants = {} +function MapFunctions.getParticipants(MatchParser, map, opponents) + local allParticipants = {} local getCharacterName = FnUtil.curry(MatchGroupInputUtil.getCharacterName, HeroNames) - for opponentIndex = 1, opponentCount do - for playerIndex, participant in ipairs(MatchParser.getParticipants(map, opponentIndex) or {}) do - participant.character = getCharacterName(participant.character) - participants[opponentIndex .. '_' .. playerIndex] = participant - end - end + Array.forEach(opponents, function(opponent, opponentIndex) + local participantList = MatchParser.getParticipants(map, opponentIndex) or {} + local participants, unattachedParticipants = MatchGroupInputUtil.parseParticipants( + opponent.match2players, + participantList, + function (playerIndex) + local participant = participantList[playerIndex] + return participant and {name = participant.player} or nil + end, + function(playerIndex) + local participant = participantList[playerIndex] + participant.character = getCharacterName(participant.character) + return participant + end + ) + Array.forEach(unattachedParticipants, function(participant) + table.insert(participants, participant) + end) + Table.mergeInto(allParticipants, Table.map(participants, MatchGroupInputUtil.prefixPartcipants(opponentIndex))) + end) - return participants + return allParticipants end ---@param winnerInput string|integer|nil diff --git a/components/match2/wikis/dota2/match_group_input_custom_match_page.lua b/components/match2/wikis/dota2/match_group_input_custom_match_page.lua index 4f41b413418..99f20c8452e 100644 --- a/components/match2/wikis/dota2/match_group_input_custom_match_page.lua +++ b/components/match2/wikis/dota2/match_group_input_custom_match_page.lua @@ -110,9 +110,10 @@ end ---@param opponentIndex integer ---@return string[]? function CustomMatchGroupInputMatchPage.getHeroPicks(map, opponentIndex) - local team = map['team' .. opponentIndex] ---@type dota2MatchTeam? - if not team then return end - return Array.map(team.players or {}, Operator.property('heroName')) + if not map.heroVeto then return end + local teamVeto = map.heroVeto['team' .. opponentIndex] ---@type dota2TeamVeto? + if not teamVeto then return end + return Array.map(teamVeto.picks or {}, Operator.property('hero')) end ---@param map dota2MatchDataExtended diff --git a/components/match2/wikis/easportsfc/match_group_input_custom.lua b/components/match2/wikis/easportsfc/match_group_input_custom.lua index c5a88620ce9..244f1b15bab 100644 --- a/components/match2/wikis/easportsfc/match_group_input_custom.lua +++ b/components/match2/wikis/easportsfc/match_group_input_custom.lua @@ -7,386 +7,185 @@ -- local Array = require('Module:Array') -local DateExt = require('Module:Date/Ext') -local Flags = require('Module:Flags') -local Json = require('Module:Json') local Logic = require('Module:Logic') local Lua = require('Module:Lua') local Ordinal = require('Module:Ordinal') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') local Streams = Lua.import('Module:Links/Stream') - -local OpponentLibrary = require('Module:OpponentLibraries') -local Opponent = OpponentLibrary.Opponent - -local UNKNOWN_REASON_LOSS_STATUS = 'L' -local DEFAULT_WIN_STATUS = 'W' -local DEFAULT_WIN_RESULTTYPE = 'default' -local NO_SCORE = -1 -local SCORE_STATUS = 'S' -local ALLOWED_STATUSES = {DEFAULT_WIN_STATUS, 'FF', 'DQ', UNKNOWN_REASON_LOSS_STATUS} -local MAX_NUM_OPPONENTS = 2 -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) -local TBD = 'tbd' -local BYE = 'BYE' +local OpponentLibraries = Lua.import('Module:OpponentLibraries') +local Opponent = OpponentLibraries.Opponent local CustomMatchGroupInput = {} -CustomMatchGroupInput.walkoverProcessing = {} -local walkoverProcessing = CustomMatchGroupInput.walkoverProcessing - -- called from Module:MatchGroup ---@param match table +---@param options table? ---@return table -function CustomMatchGroupInput.processMatch(match) - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) +function CustomMatchGroupInput.processMatch(match, options) + local winnerInput = match.winner --[[@as string?]] + local finishedInput = match.finished --[[@as string?]] - CustomMatchGroupInput._getExtraData(match) - CustomMatchGroupInput._getTournamentVars(match) - CustomMatchGroupInput._adjustData(match) - CustomMatchGroupInput._getVodStuff(match) + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) - return match -end - ----@param match table -function CustomMatchGroupInput._getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'solo')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - - MatchGroupInput.getCommonTournamentVars(match) -end - ----@param match table -function CustomMatchGroupInput._getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod) -end - ----@param match table -function CustomMatchGroupInput._getExtraData(match) - match.extradata = { - casters = MatchGroupInput.readCasters(match, {noSort = true}), - hassubmatches = tostring(Logic.readBool(match.hasSubmatches)), - } -end - ----@param match table -function CustomMatchGroupInput._adjustData(match) - CustomMatchGroupInput._opponentInput(match) - - for _, _, mapIndex in Table.iter.pairsByPrefix(match, 'map') do - CustomMatchGroupInput._mapInput(match, mapIndex) - end - - local scores = Array.map(Array.range(1, MAX_NUM_OPPONENTS), function(opponentIndex) - local score = CustomMatchGroupInput._computeOpponentMatchScore(match, opponentIndex) - match['opponent' .. opponentIndex].score = score - if Logic.isNumeric(score) then - match['opponent' .. opponentIndex].status = SCORE_STATUS - end - return score + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) end) - CustomMatchGroupInput._winnerProcessing(match, scores, true) + local games = CustomMatchGroupInput.extractMaps(match, opponents) - CustomMatchGroupInput._setPlacements(match) -end - ----@param obj table ----@param scores integer[] ----@param isMatch boolean? -function CustomMatchGroupInput._winnerProcessing(obj, scores, isMatch) - walkoverProcessing.walkover(obj, scores) - - if isMatch and obj.resulttype == DEFAULT_WIN_RESULTTYPE then - walkoverProcessing.applyMatchWalkoverToOpponents(obj) - return - end - - if obj.winner == 'draw' then - obj.winner = 0 - obj.resulttype = 'draw' - obj.finished = true - end - - obj.finished = CustomMatchGroupInput._isFinished(obj) - - obj.winner = tonumber(obj.winner) - - if obj.finished and not obj.winner then - obj.winner = CustomMatchGroupInput._determineWinnerIfMissing(scores) - end -end - ----@param match table ----@param opponentIndex integer ----@return number|string -function CustomMatchGroupInput._computeOpponentMatchScore(match, opponentIndex) - if not match['opponent' .. opponentIndex] then - return NO_SCORE - end - - -- if valid manual input return that - local score = match['opponent' .. opponentIndex].score - if Logic.isNumeric(score) then - return tonumber(score) --[[@as number]] - elseif Table.includes(ALLOWED_STATUSES, (score or ''):upper()) then - return score:upper() - end - - if not match.map1 or not match.map1.winner then - return NO_SCORE - end - - local sumScore = 0 - - -- if submatches count submatch/map wins + local scoreType = 'mapScores' if Logic.readBool(match.hasSubmatches) then - for _, map in Table.iter.pairsByPrefix(match, 'map') do - if map.winner == opponentIndex then - sumScore = sumScore + 1 - end - end - - return sumScore - end - - -- if won in penalty shoot out return score of that shoot out - for _, map in Table.iter.pairsByPrefix(match, 'map') do - if Logic.readBool(map.penalty) then - return map.scores[opponentIndex] - end - end - - -- sum up scores - for _, map in Table.iter.pairsByPrefix(match, 'map') do - local mapScore = map.scores[opponentIndex] - if mapScore ~= NO_SCORE then - sumScore = sumScore + mapScore - end - end - - return sumScore -end + scoreType = 'mapWins' + elseif Array.any(Array.map(games, Operator.property('penalty')), Logic.readBool) then + scoreType = 'penalties' + end + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and CustomMatchGroupInput.calculateMatchScore(games, scoreType) + or nil + + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = winnerInput, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) ----@param obj table ----@return boolean? -function CustomMatchGroupInput._isFinished(obj) - if Logic.readBoolOrNil(obj.finished) == false then - return false - elseif Logic.readBool(obj.finished) or obj.winner then - return true - end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) - -- Match is automatically marked finished upon page edit after a - -- certain amount of time (depending on whether the date is exact) - if obj.timestamp and obj.timestamp > DateExt.defaultTimestamp then - local threshold = obj.dateexact and 30800 or 86400 - if obj.timestamp + threshold < NOW then - return true - end + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end -end ----@param scores integer[] ----@return integer? -function CustomMatchGroupInput._determineWinnerIfMissing(scores) - local maxScore = math.max(unpack(scores or {0})) - -- if we have a positive score and the match is finished we also have a winner - if maxScore > 0 then - if Array.all(scores, function(score) return score == maxScore end) then - return 0 - end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'solo')) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) - for opponentIndex, score in pairs(scores) do - if score == maxScore then - return opponentIndex - end - end - end -end + match.stream = Streams.processStreams(match) + match.extradata = CustomMatchGroupInput.getExtraData(match, scoreType == 'mapWins') ----@param match any -function CustomMatchGroupInput._setPlacements(match) - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local opponent = match['opponent' .. opponentIndex] + match.games = games + match.opponents = opponents - if match.winner == opponentIndex or match.winner == 0 then - opponent.placement = 1 - elseif match.winner then - opponent.placement = 2 - end - end + return match end ---@param match table -function CustomMatchGroupInput._opponentInput(match) - local opponentIndex = 1 - local opponent = match['opponent' .. opponentIndex] - - while opponentIndex <= MAX_NUM_OPPONENTS and Logic.isNotEmpty(opponent) do - opponent = Json.parseIfString(opponent) or Opponent.blank() - - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = BYE} +---@param opponents table[] +---@return table[] +function CustomMatchGroupInput.extractMaps(match, opponents) + local maps = {} + for mapKey, map, mapIndex in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + if Table.isEmpty(map) then + break end + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] - --process input - if opponent.type == Opponent.team or opponent.type == Opponent.solo or - opponent.type == Opponent.literal then - - opponent = CustomMatchGroupInput.processOpponent(opponent, match.timestamp) + if Logic.readBool(match.hasSubmatches) then + -- generic map name (not displayed) + map.map = 'Game ' .. mapIndex + elseif Logic.readBool(map.penalty) then + map.map = 'Penalties' + else + map.map = mapIndex .. Ordinal.suffix(mapIndex) .. ' Leg' + end + + map.mode = Opponent.toMode(opponents[1].type, opponents[2].type) + map.extradata = CustomMatchGroupInput.getMapExtraData(map, opponents, Logic.readBool(match.hasSubmatches)) + + map.opponents = CustomMatchGroupInput.getParticipants(map, opponents) + -- Match/Subobjects:luaGetMap sets a empty table as default value for participants. + -- Once subobjects have been refactored away this can be removed. + map.participants = nil + + map.finished = MatchGroupInputUtil.mapIsFinished(map) + local opponentInfo = Array.map(opponents, function(_, opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) + end + + table.insert(maps, map) + match[mapKey] = nil + end + + return maps +end + +--- TODO: Investigate if some parts of this should be a display rather than storage. +--- If penalties is supplied, than one map MUST have the penalty flag set to true. +---@param maps table[] +---@param calculateBy 'mapWins'|'mapScores'|'penalties' +---@return fun(opponentIndex: integer): integer +function CustomMatchGroupInput.calculateMatchScore(maps, calculateBy) + return function(opponentIndex) + if calculateBy == 'mapWins' then + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + elseif calculateBy == 'mapScores' then + return Array.reduce(Array.map(maps, function(map) + return map.scores[opponentIndex] or 0 + end), Operator.add, 0) + elseif calculateBy == 'penalties' then + return Array.filter(maps, function(map) + return Logic.readBool(map.penalty) + end)[1].scores[opponentIndex] else - error('Unsupported Opponent Type: ' .. (opponent.type or '')) + error('Unknown calculateBy: ' .. tostring(calculateBy)) end - - match['opponent' .. opponentIndex] = opponent - - opponentIndex = opponentIndex + 1 - opponent = match['opponent' .. opponentIndex] - end -end - ----@param record table ----@param timestamp integer ----@return table -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() - - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end - - Opponent.resolve(opponent, teamTemplateDate, {syncPlayer=true}) - - MatchGroupInput.mergeRecordWithOpponent(record, opponent) - - if record.type == Opponent.team then - record.match2players = CustomMatchGroupInput._readTeamPlayers(record, record.players) end - - return record end ----@param opponent table ----@param playerData string +---@param match table +---@param hasSubmatches boolean ---@return table -function CustomMatchGroupInput._readTeamPlayers(opponent, playerData) - local players = CustomMatchGroupInput._getManuallyEnteredPlayers(playerData) - - if Table.isEmpty(players) then - players = CustomMatchGroupInput._getPlayersFromVariables(opponent.name) - end - - return players +function CustomMatchGroupInput.getExtraData(match, hasSubmatches) + return { + casters = MatchGroupInputUtil.readCasters(match, {noSort = true}), + hassubmatches = tostring(hasSubmatches), + } end ----@param playerData string +---@param map table +---@param opponents table[] +---@param hasSubmatches boolean ---@return table -function CustomMatchGroupInput._getManuallyEnteredPlayers(playerData) - local players = {} - playerData = Json.parseIfString(playerData) or {} - - for prefix, displayName in Table.iter.pairsByPrefix(playerData, 'p') do - local name = mw.ext.TeamLiquidIntegration.resolve_redirect(Logic.emptyOr( - playerData[prefix .. 'link'] - ) or displayName):gsub(' ', '_') - - table.insert(players, { - name = name, - displayname = displayName, - flag = Flags.CountryName(playerData[prefix .. 'flag']), - }) - end - - return players -end - ----@param teamName string ----@return table[] -function CustomMatchGroupInput._getPlayersFromVariables(teamName) - local teamNameWithSpaces = teamName:gsub('_', ' ') - local players = {} - - local playerIndex = 1 - while true do - local prefix = teamName .. '_p' .. playerIndex - local prefixWithSpaces = teamNameWithSpaces .. '_p' .. playerIndex - local playerName = Variables.varDefault(prefix, Variables.varDefault(prefixWithSpaces)) - if String.isEmpty(playerName) then - break - end - ---@cast playerName -nil - table.insert(players, { - name = playerName:gsub(' ', '_'), - displayname = Variables.varDefault( - prefix .. 'dn', - Variables.varDefault(prefixWithSpaces .. 'dn', playerName:gsub('_', ' ')) - ), - flag = Flags.CountryName(Variables.varDefault(prefix .. 'flag', Variables.varDefault(prefixWithSpaces .. 'flag'))), - }) - playerIndex = playerIndex + 1 - end - - return players -end - ----@param match table ----@param mapIndex integer -function CustomMatchGroupInput._mapInput(match, mapIndex) - local map = Json.parseIfString(match['map' .. mapIndex]) - - if Table.isEmpty(map) then - match['map' .. mapIndex] = nil - return - end - - if Logic.readBool(match.hasSubmatches) then - -- generic map name (not displayed) - map.map = 'Game ' .. mapIndex - elseif Logic.readBool(map.penalty) then - map.map = 'Penalties' - else - map.map = mapIndex .. Ordinal.suffix(mapIndex) .. ' Leg' - end - - -- set initial extradata for maps - map.extradata = { +function CustomMatchGroupInput.getMapExtraData(map, opponents, hasSubmatches) + return { comment = map.comment, - penaltyscores = CustomMatchGroupInput._submatchPenaltyScores(match, map), + penaltyscores = CustomMatchGroupInput._submatchPenaltyScores(map, opponents, hasSubmatches), } - - -- determine score, resulttype, walkover and winner - CustomMatchGroupInput._mapWinnerProcessing(map) - - -- get participants data for the map + get map mode - CustomMatchGroupInput._processPlayerMapData(map, match) - - match['map' .. mapIndex] = map end ----@param match table ---@param map table +---@param opponents table[] +---@param hasSubmatches boolean ---@return integer[]? -function CustomMatchGroupInput._submatchPenaltyScores(match, map) - if not Logic.readBool(match.hasSubmatches) then +function CustomMatchGroupInput._submatchPenaltyScores(map, opponents, hasSubmatches) + if not hasSubmatches then return end local hasPenalties = false - local scores = Array.map(Array.range(1, MAX_NUM_OPPONENTS), function(opponentIndex) + local scores = Array.map(opponents, function(_, opponentIndex) local score = tonumber(map['penaltyScore' .. opponentIndex]) hasPenalties = hasPenalties or (score ~= nil) return score or 0 @@ -396,231 +195,28 @@ function CustomMatchGroupInput._submatchPenaltyScores(match, map) end ---@param map table -function CustomMatchGroupInput._mapWinnerProcessing(map) - if map.winner == 'skip' then - map.scores = {NO_SCORE, NO_SCORE} - map.resulttype = 'np' - - return - end - - map.scores = {} - - local scores = Array.map(Array.range(1, MAX_NUM_OPPONENTS), function(opponentIndex) - local score = map['score' .. opponentIndex] - map.scores[opponentIndex] = tonumber(score) or NO_SCORE - - return Table.includes(ALLOWED_STATUSES, string.upper(score or '')) - and score:upper() - or map.scores[opponentIndex] +---@param opponents MGIParsedOpponent[] +---@return {players: table[]}[] +function CustomMatchGroupInput.getParticipants(map, opponents) + return Array.map(opponents, function(opponent, opponentIndex) + local players = Array.mapIndexes(function(playerIndex) + return map['t' .. opponentIndex .. 'p' .. playerIndex] + end) + local participants, _ = MatchGroupInputUtil.parseParticipants( + opponent.match2players, + players, + function(playerIndex) + local data = map['t' .. opponentIndex .. 'p' .. playerIndex] + return data and {name = data} or nil + end, + function(playerIndex, playerIdData, playerInputData) + return { + played = true + } + end + ) + return {players = participants} end) - - CustomMatchGroupInput._winnerProcessing(map, scores) -end - ----@param map table ----@param match table -function CustomMatchGroupInput._processPlayerMapData(map, match) - local participants = {} - - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local opponent = match['opponent' .. opponentIndex] - if opponent.type == Opponent.team and Logic.readBool(match.hasSubmatches) then - CustomMatchGroupInput._processTeamPlayerMapData( - opponent.match2players or {}, - opponentIndex, - map, - participants - ) - else - CustomMatchGroupInput._processDefaultPlayerMapData( - opponent.match2players or {}, - opponentIndex, - map, - participants - ) - end - end - - map.mode = Opponent.toMode(match.opponent1.type, match.opponent2.type) - - map.participants = participants -end - ----@param players table[] ----@param opponentIndex integer ----@param map table ----@param participants table -function CustomMatchGroupInput._processDefaultPlayerMapData(players, opponentIndex, map, participants) - for playerIndex = 1, #players do - participants[opponentIndex .. '_' .. playerIndex] = { - played = true, - } - end -end - ----@param players table[] ----@param opponentIndex integer ----@param map table ----@param participants table -function CustomMatchGroupInput._processTeamPlayerMapData(players, opponentIndex, map, participants) - local prefix = 't' .. opponentIndex .. 'p' - - -- we need at least 1 player so if none ist set use TBD - if String.isEmpty(map[prefix .. 1]) then - map[prefix .. 1] = TBD - end - - for playerKey, player in Table.iter.pairsByPrefix(map, prefix) do - if player:lower() ~= TBD then - -- allows fetching the link of the player from preset wiki vars - player = mw.ext.TeamLiquidIntegration.resolve_redirect( - map[playerKey .. 'link'] or Variables.varDefault(player .. '_page') or player - ) - end - - local playerData = { - played = true, - } - - local match2playerIndex = CustomMatchGroupInput._fetchMatch2PlayerIndexOfPlayer(players, player) - - -- if we have the player not present in match2player add basic data here - if not match2playerIndex then - match2playerIndex = #players + 1 - playerData = Table.merge(playerData, {name = player:gsub(' ', '_'), displayname = map[playerKey] or player}) - end - - participants[opponentIndex .. '_' .. match2playerIndex] = playerData - end -end - ----@param players table[] ----@param player string ----@return integer? -function CustomMatchGroupInput._fetchMatch2PlayerIndexOfPlayer(players, player) - local displayNameIndex - local displayNameFoundTwice = false - player = mw.ext.TeamLiquidIntegration.resolve_redirect(player) - local playerWithUnderscores = player:gsub(' ', '_') - - for match2playerIndex, match2player in pairs(players) do - if match2player and match2player.name == playerWithUnderscores then - return match2playerIndex - elseif not displayNameIndex and match2player and match2player.displayname == player then - displayNameIndex = match2playerIndex - elseif match2player and match2player.displayname == player then - displayNameFoundTwice = true - end - end - - if not displayNameFoundTwice then - return displayNameIndex - end -end - ----@param obj table ----@param scores string[] -function walkoverProcessing.walkover(obj, scores) - local walkover = obj.walkover - - if Logic.isNumeric(walkover) then - walkoverProcessing.numericWalkover(obj, walkover) - elseif walkover then - walkoverProcessing.nonNumericWalkover(obj, walkover) - elseif #scores ~=2 then -- since we always have 2 opponents outside of ffa - error('Unexpected number of opponents when calculating winner') - elseif Array.all(scores, function(score) - return Table.includes(ALLOWED_STATUSES, score) and score ~= DEFAULT_WIN_STATUS - end) then - - walkoverProcessing.scoreDoubleWalkover(obj, scores) - elseif Array.any(scores, function(score) return Table.includes(ALLOWED_STATUSES, score) end) then - walkoverProcessing.scoreWalkover(obj, scores) - end -end - ----@param obj table ----@param walkover string|integer -function walkoverProcessing.numericWalkover(obj, walkover) - local winner = tonumber(walkover) - - obj.winner = winner - obj.finished = true - obj.walkover = UNKNOWN_REASON_LOSS_STATUS - obj.resulttype = DEFAULT_WIN_RESULTTYPE -end - ----@param obj table ----@param walkover string -function walkoverProcessing.nonNumericWalkover(obj, walkover) - if not Table.includes(ALLOWED_STATUSES, string.upper(walkover)) then - error('Invalid walkover "' .. walkover .. '"') - elseif not Logic.isNumeric(obj.winner) then - error('Walkover without winner specified') - end - - obj.winner = tonumber(obj.winner) - obj.finished = true - obj.walkover = walkover:upper() - obj.resulttype = DEFAULT_WIN_RESULTTYPE -end - ----@param obj table ----@param scores (integer|string)[] -function walkoverProcessing.scoreDoubleWalkover(obj, scores) - obj.winner = -1 - obj.finished = true - obj.walkover = scores[1] - obj.resulttype = DEFAULT_WIN_RESULTTYPE -end - ----@param obj table ----@param scores (integer|string)[] -function walkoverProcessing.scoreWalkover(obj, scores) - local winner, status - - for scoreIndex, score in pairs(scores) do - score = string.upper(score) - if score == DEFAULT_WIN_STATUS then - winner = scoreIndex - elseif Table.includes(ALLOWED_STATUSES, score) then - status = score - else - status = UNKNOWN_REASON_LOSS_STATUS - end - end - - if not winner then - error('Invalid score combination "{' .. scores[1] .. ', ' .. scores[2] .. '}"') - end - - obj.winner = winner - obj.finished = true - obj.walkover = status - obj.resulttype = DEFAULT_WIN_RESULTTYPE -end - ----@param match table -function walkoverProcessing.applyMatchWalkoverToOpponents(match) - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local score = match['opponent' .. opponentIndex].score - - if Logic.isNumeric(score) or String.isEmpty(score) then - match['opponent' .. opponentIndex].score = tonumber(score) or NO_SCORE - match['opponent' .. opponentIndex].status = match.walkover - elseif score and Table.includes(ALLOWED_STATUSES, score:upper()) then - match['opponent' .. opponentIndex].score = NO_SCORE - match['opponent' .. opponentIndex].status = score - else - error('Invalid score "' .. score .. '"') - end - end - - -- neither match.opponent0 nor match['opponent-1'] does exist hence the if - if match['opponent' .. match.winner] then - match['opponent' .. match.winner].status = DEFAULT_WIN_STATUS - end end return CustomMatchGroupInput diff --git a/components/match2/wikis/fighters/match_group_input_custom.lua b/components/match2/wikis/fighters/match_group_input_custom.lua index 6f17fb8388c..17e41b8592d 100644 --- a/components/match2/wikis/fighters/match_group_input_custom.lua +++ b/components/match2/wikis/fighters/match_group_input_custom.lua @@ -7,446 +7,193 @@ -- local Array = require('Module:Array') -local FnUtil = require('Module:FnUtil') local Game = require('Module:Game') local Json = require('Module:Json') -local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') local Opponent = Lua.import('Module:Opponent') local Streams = Lua.import('Module:Links/Stream') -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L'} -local CONVERT_STATUS_INPUT = {W = 'W', FF = 'FF', L = 'L', DQ = 'DQ', ['-'] = 'L'} -local DEFAULT_LOSS_STATUSES = {'FF', 'L', 'DQ'} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_PLAYERS = 10 -local DEFAULT_BESTOF = 99 - local DUMMY_MAP_NAME = 'Null' -- Is set in Template:Map when |map= is empty. +local OPPONENT_CONFIG = { + resolveRedirect = true, + applyUnderScores = true, + maxNumPlayers = 10, +} local CustomMatchGroupInput = {} ---- called from Module:MatchGroup +-- called from Module:MatchGroup ---@param match table ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - Table.mergeInto(match, MatchGroupInput.readDate(match.date, { - 'match_date', + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + + Table.mergeInto(match, Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date, { 'tournament_enddate', 'tournament_startdate', - })) - CustomMatchGroupInput._getOpponents(match) - CustomMatchGroupInput._getTournamentVars(match) - CustomMatchGroupInput._verifyMaps(match) - CustomMatchGroupInput._processMaps(match) - CustomMatchGroupInput._calculateWinner(match) - CustomMatchGroupInput._updateFinished(match) - match.stream = Streams.processStreams(match) - CustomMatchGroupInput._getVod(match) - return match -end - -CustomMatchGroupInput.processMap = FnUtil.identity + }))) ----@param match table -function CustomMatchGroupInput._getTournamentVars(match) - match = MatchGroupInput.getCommonTournamentVars(match) + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) + local games = CustomMatchGroupInput.extractMaps(match, opponents) + match.bestof = tonumber(match.bestof) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and CustomMatchGroupInput.calculateMatchScore(games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) - match.mode = Variables.varDefault('tournament_mode', 'singles') -end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) ----@param match table -function CustomMatchGroupInput._updateFinished(match) - match.finished = Logic.nilOr(Logic.readBoolOrNil(match.finished), Logic.isNotEmpty(match.winner)) if match.finished then - return + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - -- Match is automatically marked finished upon page edit after a - -- certain amount of time (depending on whether the date is exact) - local currentUnixTime = os.time(os.date('!*t') --[[@as osdateparam]]) - local threshold = match.dateexact and 30800 or 86400 - match.finished = match.timestamp + threshold < currentUnixTime -end + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) + match.mode = Variables.varDefault('tournament_mode', 'singles') ----@param match table -function CustomMatchGroupInput._getVod(match) match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod) -end - --- This function is used to discard maps that are none-relevant ones --- Template:Map sets a default map name because sometimes --- we have results without knowledge of which map it was planned on. ----@param match table -function CustomMatchGroupInput._verifyMaps(match) - for key, map in Table.iter.pairsByPrefix(match, 'map') do - if CustomMatchGroupInput._discardMap(map) then - match[key] = nil - elseif map.map == DUMMY_MAP_NAME then - match[key].map = '' - end - end -end --- Check if a map should be discarded due to not containing anything relevant --- DUMMY_MAP_NAME variable must the match the default value in Template:Map ----@param map table ----@return boolean -function CustomMatchGroupInput._discardMap(map) - return map.map == DUMMY_MAP_NAME - and not map.score1 and not map.score2 and not map.winner and not map.o1p1 and not map.o2p1 -end + match.games = games + match.opponents = opponents ----@param match table -function CustomMatchGroupInput._processMaps(match) - for _, _, mapIndex in Table.iter.pairsByPrefix(match, 'map') do - CustomMatchGroupInput._mapInput(match, mapIndex) - end + return match end ---@param match table -function CustomMatchGroupInput._calculateWinner(match) - local bestof = match.bestof or DEFAULT_BESTOF - local numberOfOpponents = 0 - - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local opponent = match['opponent' .. opponentIndex] - if Logic.isEmpty(opponent) then - break - end - - numberOfOpponents = numberOfOpponents + 1 +---@param matchOpponents table[] +---@return table[] +function CustomMatchGroupInput.extractMaps(match, matchOpponents) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] - if Logic.isNotEmpty(match.walkover) then - if Logic.isNumeric(match.walkover) then - local walkover = tonumber(match.walkover) - if walkover == opponentIndex then - match.winner = opponentIndex - match.walkover = 'FF' - opponent.status = 'W' - elseif walkover == 0 then - match.winner = 0 - match.walkover = 'FF' - opponent.status = 'FF' - else - local score = string.upper(opponent.score or '') - opponent.status = CONVERT_STATUS_INPUT[score] or 'FF' - end - elseif Table.includes(ALLOWED_STATUSES, string.upper(match.walkover)) then - if tonumber(match.winner or 0) == opponentIndex then - opponent.status = 'W' - else - opponent.status = CONVERT_STATUS_INPUT[string.upper(match.walkover)] or 'L' - end - else - local score = string.upper(opponent.score or '') - opponent.status = CONVERT_STATUS_INPUT[score] or 'L' - match.walkover = 'L' - end - opponent.score = -1 - match.finished = true - match.resulttype = 'default' - elseif CONVERT_STATUS_INPUT[string.upper(opponent.score or '')] then - if string.upper(opponent.score) == 'W' then - match.winner = opponentIndex - match.finished = true - opponent.score = -1 - opponent.status = 'W' - else - local score = string.upper(opponent.score) - match.finished = true - match.walkover = CONVERT_STATUS_INPUT[score] - opponent.status = CONVERT_STATUS_INPUT[score] - opponent.score = -1 - end - match.resulttype = 'default' - else - opponent.status = 'S' - opponent.score = tonumber(opponent.score) or tonumber(opponent.autoscore) or -1 - if opponent.score > bestof / 2 then - match.finished = Logic.emptyOr(match.finished, true) - match.winner = tonumber(match.winner) or opponentIndex - end + if map.map == DUMMY_MAP_NAME then + map.map = '' end - end + map.extradata = { + comment = map.comment, + } + map.finished = MatchGroupInputUtil.mapIsFinished(map) + map.opponents = Array.map(matchOpponents, function(opponent, opponentIndex) + return CustomMatchGroupInput.getParticipantsOfOpponent(map, opponent, opponentIndex) + end) - CustomMatchGroupInput._determineWinnerIfMissing(match) + local opponentInfo = Array.map(matchOpponents, function(_, opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }, CustomMatchGroupInput.calculateMapScore(map.winner, map.finished)) + return {score = score, status = status} + end) - for opponentIndex = 1, numberOfOpponents do - local opponent = match['opponent' .. opponentIndex] - if match.winner == 'draw' or tonumber(match.winner) == 0 or - (match.opponent1.score == bestof / 2 and match.opponent1.score == match.opponent2.score) then - match.finished = true - match.winner = 0 - match.resulttype = 'draw' + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - if tonumber(match.winner) == opponentIndex or - match.resulttype == 'draw' then - opponent.placement = 1 - elseif Logic.isNumeric(match.winner) then - opponent.placement = 2 - end + table.insert(maps, map) + match[key] = nil end -end ----@param match table -function CustomMatchGroupInput._determineWinnerIfMissing(match) - if not Logic.readBool(match.finished) or Logic.isNotEmpty(match.winner) then - return - end - - local scores = Array.mapIndexes(function(opponentIndex) - local opponent = match['opponent' .. opponentIndex] - if not opponent then - return nil - end - return match['opponent' .. opponentIndex].score or -1 - end - ) - local maxScore = math.max(unpack(scores) or 0) - -- if we have a positive score and the match is finished we also have a winner - if maxScore > 0 then - local maxIndexFound = false - for opponentIndex, score in pairs(scores) do - if maxIndexFound and score == maxScore then - match.winner = 0 - break - elseif score == maxScore then - maxIndexFound = true - match.winner = opponentIndex - end - end - end + return maps end ----@param match table ----@return table -function CustomMatchGroupInput._getOpponents(match) - -- read opponents and ignore empty ones - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if Logic.isNotEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - end - match['opponent' .. opponentIndex] = opponent - - if opponent.type == Opponent.team and Logic.isNotEmpty(opponent.name) then - MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name, { - resolveRedirect = true, - applyUnderScores = true, - maxNumPlayers = MAX_NUM_PLAYERS, - }) - end +---@param maps table[] +---@return fun(opponentIndex: integer): integer +function CustomMatchGroupInput.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - - return match -end - ----@param record table ----@param timestamp number -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) or Opponent.blank() - - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - Opponent.resolve(opponent, timestamp, {syncPlayer = true}) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end - ----@param match table ----@param mapIndex integer -function CustomMatchGroupInput._mapInput(match, mapIndex) - local map = Json.parseIfString(match['map' .. mapIndex]) - if String.isNotEmpty(map.map) and map.map ~= 'TBD' then - map.map = mw.ext.TeamLiquidIntegration.resolve_redirect(map.map) - end - - -- set initial extradata for maps - map.extradata = { - comment = map.comment, - header = map.header, - } - map.game = match.game - map.mode = match.mode - - -- determine score, resulttype, walkover and winner - map = CustomMatchGroupInput._mapWinnerProcessing(map) - - -- Init score if match started and map info is present - if not match.opponent1.autoscore and not match.opponent2.autoscore - and map.map and map.map ~= 'TBD' - and match.timestamp < os.time(os.date('!*t') --[[@as osdateparam]]) then - match.opponent1.autoscore = 0 - match.opponent2.autoscore = 0 - end - - if Logic.isEmpty(map.resulttype) and map.scores[1] and map.scores[2] then - match.opponent1.autoscore = (match.opponent1.autoscore or 0) + (map.winner == 1 and 1 or 0) - match.opponent2.autoscore = (match.opponent2.autoscore or 0) + (map.winner == 2 and 1 or 0) - end - - CustomMatchGroupInput.processPlayersMapData(map, match, 2) - - match['map' .. mapIndex] = map -end - ----@param map table ----@return table -function CustomMatchGroupInput._mapWinnerProcessing(map) - map.scores = {} - local hasManualScores = false - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] - local obj = {} - if Logic.isNotEmpty(score) then - hasManualScores = true - score = CONVERT_STATUS_INPUT[score] or score - if Logic.isNumeric(score) then - obj.status = 'S' - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - - local winnerInput = tonumber(map.winner) - if Logic.isNotEmpty(map.walkover) then - local walkoverInput = tonumber(map.walkover) - if walkoverInput == 1 or walkoverInput == 2 or walkoverInput == 0 then - map.winner = walkoverInput - end - map.walkover = Table.includes(ALLOWED_STATUSES, map.walkover) and map.walkover or 'L' - map.scores = {-1, -1} - map.resulttype = 'default' - - return map - end - - if hasManualScores then - for scoreIndex, _ in Table.iter.spairs(indexedScores, CustomMatchGroupInput._placementSortFunction) do - if not tonumber(map.winner) then - map.winner = scoreIndex - else - break - end - end - - return map - end - - if map.winner == 'skip' then - map.scores = {-1, -1} - map.resulttype = 'np' - elseif winnerInput == 1 then - map.scores = {1, 0} - elseif winnerInput == 2 then - map.scores = {0, 1} - elseif winnerInput == 0 or map.winner == 'draw' then - map.scores = {0, 0} - map.resulttype = 'draw' - end - - return map end ---@param map table ----@param match table ----@param numberOfOpponents integer -function CustomMatchGroupInput.processPlayersMapData(map, match, numberOfOpponents) - local participants = {} - for opponentIndex = 1, numberOfOpponents do - local opponent = match['opponent' .. opponentIndex] - if opponent.type ~= Opponent.literal then - local playerCount = Opponent.partySize(opponent.type) or MAX_NUM_PLAYERS - for playerIndex = 1, playerCount do - local player = opponent.match2players[playerIndex] - if player then - local playerData = CustomMatchGroupInput._processPlayerMapData(player, map, opponentIndex, playerIndex) - participants[opponentIndex .. '_' .. playerIndex] = playerData - end - end - end +---@param opponent table +---@param opponentIndex integer +---@return table? +function CustomMatchGroupInput.getParticipantsOfOpponent(map, opponent, opponentIndex) + if opponent.type == Opponent.literal then + return {} end - map.participants = participants + return CustomMatchGroupInput._processPlayerMapData(map, opponent, opponentIndex) end ----@param player table ---@param map table +---@param opponent table ---@param opponentIndex integer ----@param playerIndex integer ----@return {characters: {name: string, active: boolean}[], player: string} -function CustomMatchGroupInput._processPlayerMapData(player, map, opponentIndex, playerIndex) +---@return {players: table[]} +function CustomMatchGroupInput._processPlayerMapData(map, opponent, opponentIndex) local game = Game.toIdentifier{game = Variables.varDefault('tournament_game')} local CharacterStandardizationData = mw.loadData('Module:CharacterStandardization/' .. game) - local charInputs = Json.parseIfTable(map['o' .. opponentIndex .. 'p' .. playerIndex]) or {} ---@type string[] + local players = Array.mapIndexes(function(playerIndex) + return opponent.match2players[playerIndex] or + (map['t' .. opponentIndex .. 'p' .. playerIndex] and {}) or + nil + end) + local participants, unattachedParticipants = MatchGroupInputUtil.parseParticipants( + opponent.match2players, + players, + function(playerIndex) + return (opponent.match2players[playerIndex] or {}).name + end, + function(playerIndex, playerIdData, playerInputData) + local charInputs = Json.parseIfTable(map['o' .. opponentIndex .. 'p' .. playerIndex]) or {} ---@type string[] + local characters = Array.map(charInputs, function(characterInput) + local character = MatchGroupInputUtil.getCharacterName(CharacterStandardizationData, characterInput) + if not character then + return nil + end - local characters = Array.map(charInputs, function(characterInput) - local character = MatchGroupInput.getCharacterName(CharacterStandardizationData, characterInput) - if not character then - return nil + return {name = character} + end) + return { + characters = characters, + player = playerIdData.name, + } end - - return {name = character, active = true} + ) + Array.forEach(unattachedParticipants, function(participant) + table.insert(participants, participant) end) - - return { - characters = characters, - player = player.name, - } + return {players = participants} end --- function to sort out winner/placements ----@param tbl table ----@param key1 string ----@param key2 string ----@return boolean -function CustomMatchGroupInput._placementSortFunction(tbl, key1, key2) - local opponent1 = tbl[key1] - local opponent2 = tbl[key2] - local opponent1Norm = opponent1.status == 'S' - local opponent2Norm = opponent2.status == 'S' - if opponent1Norm then - if opponent2Norm then - return tonumber(opponent1.score) > tonumber(opponent2.score) - else - return true - end - else - if opponent2Norm then - return false - elseif opponent1.status == 'W' then - return true - elseif Table.includes(DEFAULT_LOSS_STATUSES, opponent1.status) then - return false - elseif opponent2.status == 'W' then - return false - elseif Table.includes(DEFAULT_LOSS_STATUSES, opponent2.status) then - return true - else - return true +---@param winnerInput string|integer|nil +---@param finished boolean +---@return fun(opponentIndex: integer): integer? +function CustomMatchGroupInput.calculateMapScore(winnerInput, finished) + local winner = tonumber(winnerInput) + return function(opponentIndex) + -- TODO Better to check if map has started, rather than finished, for a more correct handling + if not winner and not finished then + return end + return winner == opponentIndex and 1 or 0 end end diff --git a/components/match2/wikis/fighters/match_summary.lua b/components/match2/wikis/fighters/match_summary.lua index 9cc9584c4e6..9b331d1eae0 100644 --- a/components/match2/wikis/fighters/match_summary.lua +++ b/components/match2/wikis/fighters/match_summary.lua @@ -89,7 +89,7 @@ end ---@param game MatchGroupUtilGame ---@param teamIdx integer ----@return {name: string, active: boolean}[] +---@return {name: string}[] function CustomMatchSummary.fetchCharacters(game, teamIdx) local characters = {} for _, playerCharacters in Table.iter.pairsByPrefix(game.participants, teamIdx .. '_', {requireIndex = true}) do @@ -135,7 +135,7 @@ function CustomMatchSummary._createStandardGame(game, props) return elements end ----@param characters {name: string, active: boolean}[]? +---@param characters {name: string}[]? ---@param game string? ---@param reverse boolean? ---@return Html @@ -151,22 +151,17 @@ function CustomMatchSummary._createCharacterDisplay(characters, game, reverse) if #characters == 1 then local characterDisplay = mw.html.create('span'):addClass('draft faction') local character = characters[1] - if not character.active then - characterDisplay:css('opacity', '0.3') - end if reverse then characterDisplay:wikitext(character.name):wikitext(' '):wikitext(CharacterIcons[character.name]) else characterDisplay:wikitext(CharacterIcons[character.name]):wikitext(' '):wikitext(character.name) end + return characterDisplay end local characterDisplays = Array.map(characters, function (character, index) local characterDisplay = mw.html.create('span'):addClass('draft faction') - if not character.active then - characterDisplay:css('opacity', '0.3') - end characterDisplay:wikitext(CharacterIcons[character.name]) return characterDisplay end) diff --git a/components/match2/wikis/geoguessr/get_match_group_copy_paste_wiki.lua b/components/match2/wikis/geoguessr/get_match_group_copy_paste_wiki.lua new file mode 100644 index 00000000000..84c3ee85655 --- /dev/null +++ b/components/match2/wikis/geoguessr/get_match_group_copy_paste_wiki.lua @@ -0,0 +1,48 @@ +--- +-- @Liquipedia +-- wiki=geoguessr +-- page=Module:GetMatchGroupCopyPaste/wiki +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local Class = require('Module:Class') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') + +local BaseCopyPaste = Lua.import('Module:GetMatchGroupCopyPaste/wiki/Base') + +---@class GeoGuessrMatch2CopyPaste: Match2CopyPasteBase +local WikiCopyPaste = Class.new(BaseCopyPaste) + +local INDENT = WikiCopyPaste.Indent + +--returns the Code for a Match, depending on the input +---@param bestof integer +---@param mode string +---@param index integer +---@param opponents integer +---@param args table +---@return string +function WikiCopyPaste.getMatchCode(bestof, mode, index, opponents, args) + local showScore = Logic.readBool(args.score) + local streams = Logic.readBool(args.streams) + + local lines = Array.extend({}, + '{{Match', + Array.map(Array.range(1, opponents), function(opponentIndex) + return INDENT .. '|opponent' .. opponentIndex .. '=' .. WikiCopyPaste.getOpponent(mode, showScore) + end), + INDENT .. '|date= |finished=', + streams and (INDENT .. '|twitch=|vod=') or nil, + Array.map(Array.range(1, bestof), function(mapIndex) + return INDENT .. '|map' .. mapIndex .. '={{Map|map=|score1=|score2=|finished=}}' + end), + '}}' + ) + + return table.concat(lines, '\n') +end + +return WikiCopyPaste diff --git a/components/match2/wikis/geoguessr/match_group_input_custom.lua b/components/match2/wikis/geoguessr/match_group_input_custom.lua new file mode 100644 index 00000000000..d1b7d25090b --- /dev/null +++ b/components/match2/wikis/geoguessr/match_group_input_custom.lua @@ -0,0 +1,135 @@ +--- +-- @Liquipedia +-- wiki=geoguessr +-- page=Module:MatchGroup/Input/Custom +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') +local Table = require('Module:Table') +local Variables = require('Module:Variables') + +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') + +local DEFAULT_MODE = '1v1' + +-- containers for process helper functions +local MatchFunctions = {} +local MapFunctions = {} + +local CustomMatchGroupInput = {} + +---@param match table +---@param options table? +---@return table +function CustomMatchGroupInput.processMatch(match, options) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) + local games = CustomMatchGroupInput.extractMaps(match, #opponents) + match.bestof = MatchGroupInputUtil.getBestOf(match.bestof, games) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) + end + + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) + + match.stream = Streams.processStreams(match) + match.games = games + match.opponents = opponents + + return match +end + +---@param match table +---@param opponentCount integer +---@return table[] +function CustomMatchGroupInput.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) + end + + table.insert(maps, map) + match[key] = nil + end + + return maps +end + +-- +-- match related functions +-- + +---@param maps table[] +---@return fun(opponentIndex: integer): integer +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + end +end + +-- +-- map related functions +-- + +-- Parse extradata information +---@param map table +---@return table +function MapFunctions.getExtraData(map) + return { + comment = map.comment, + } +end + +return CustomMatchGroupInput diff --git a/components/match2/wikis/geoguessr/match_summary.lua b/components/match2/wikis/geoguessr/match_summary.lua new file mode 100644 index 00000000000..1e68334032e --- /dev/null +++ b/components/match2/wikis/geoguessr/match_summary.lua @@ -0,0 +1,124 @@ +--- +-- @Liquipedia +-- wiki=geoguessr +-- page=Module:MatchSummary +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local DateExt = require('Module:Date/Ext') +local Icon = require('Module:Icon') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') +local Page = require('Module:Page') + +local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper') +local MatchSummary = Lua.import('Module:MatchSummary/Base') + +local Icons = { + CHECK = Icon.makeIcon{iconName = 'winner', color = 'forest-green-text', size = 'initial'}, + EMPTY = '[[File:NoCheck.png|link=]]', +} + +local CustomMatchSummary = {} + +---@param args table +---@return Html +function CustomMatchSummary.getByMatchId(args) + return MatchSummary.defaultGetByMatchId(CustomMatchSummary, args) +end + +---@param match MatchGroupUtilMatch +---@param footer MatchSummaryFooter +---@return MatchSummaryFooter +function CustomMatchSummary.addToFooter(match, footer) + return MatchSummary.addVodsToFooter(match, footer) +end + +---@param match MatchGroupUtilMatch +---@return MatchSummaryBody +function CustomMatchSummary.createBody(match) + local body = MatchSummary.Body() + + if not DateExt.isDefaultTimestamp(match.timestamp) then + -- dateIsExact means we have both date and time. Show countdown + -- if match is not default timestamp, we have a date, so display the date + body:addRow(MatchSummary.Row():addElement( + DisplayHelper.MatchCountdownBlock(match) + )) + end + + -- Iterate each map + Array.forEach(match.games, function(game) + body:addRow(CustomMatchSummary._createMapRow(game)) + end) + + return body +end + +---@param game MatchGroupUtilGame +---@param opponentIndex integer +---@return Html +function CustomMatchSummary._gameScore(game, opponentIndex) + local score = game.scores[opponentIndex] + local scoreDisplay = DisplayHelper.MapScore(score, opponentIndex, game.resultType, game.walkover, game.winner) + return mw.html.create('div'):wikitext(scoreDisplay) +end + +---@param game MatchGroupUtilGame +---@return MatchSummaryRow +function CustomMatchSummary._createMapRow(game) + local row = MatchSummary.Row() + + local centerNode = mw.html.create('div') + :addClass('brkts-popup-spaced') + :wikitext(Page.makeInternalLink(game.map)) + :css('text-align', 'center') + + if game.resultType == 'np' then + centerNode:addClass('brkts-popup-spaced-map-skip') + end + + local leftNode = mw.html.create('div') + :addClass('brkts-popup-spaced') + :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 1, Icons.CHECK)) + :node(CustomMatchSummary._gameScore(game, 1)) + :css('width', '20%') + + local rightNode = mw.html.create('div') + :addClass('brkts-popup-spaced') + :node(CustomMatchSummary._gameScore(game, 2)) + :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 2, Icons.CHECK)) + :css('width', '20%') + + row:addElement(leftNode) + :addElement(centerNode) + :addElement(rightNode) + + row:addClass('brkts-popup-body-game') + :css('overflow', 'hidden') + + -- Add Comment + if Logic.isNotEmpty(game.comment) then + row:addElement(MatchSummary.Break():create()) + local comment = mw.html.create('div') + :wikitext(game.comment) + :css('margin', 'auto') + row:addElement(comment) + end + + return row +end + +---@param showIcon boolean? +---@param icon string? +---@return Html +function CustomMatchSummary._createCheckMarkOrCross(showIcon, icon) + return mw.html.create('div') + :addClass('brkts-popup-spaced') + :css('line-height', '27px') + :node(showIcon and icon or Icons.EMPTY) +end + +return CustomMatchSummary diff --git a/components/match2/wikis/halo/match_group_input_custom.lua b/components/match2/wikis/halo/match_group_input_custom.lua index 9b842b0b729..3108d9133d2 100644 --- a/components/match2/wikis/halo/match_group_input_custom.lua +++ b/components/match2/wikis/halo/match_group_input_custom.lua @@ -6,30 +6,22 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- -local DateExt = require('Module:Date/Ext') -local Json = require('Module:Json') +local Array = require('Module:Array') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Operator = require('Module:Operator') local Streams = require('Module:Links/Stream') -local String = require('Module:StringUtils') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L', 'D'} -local FINISHED_INDICATORS = {'skip', 'np', 'cancelled', 'canceled'} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_MAPS = 9 local DEFAULT_BESTOF = 3 -local NO_SCORE = -99 -local MATCH_BYE = {'bye', 'BYE'} -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_MODE = 'team' -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} @@ -38,148 +30,52 @@ local CustomMatchGroupInput = {} ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - -- Count number of maps, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) - return match -end + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) + local games = MatchFunctions.extractMaps(match, #opponents) - return map -end + match.bestof = MatchFunctions.getBestOf(match.bestof) ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if Table.includes(FINISHED_INDICATORS, data.finished) or Table.includes(FINISHED_INDICATORS, data.winner) then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = 'default' - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'default') - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end - end - - --set it as finished if we have a winner - if not Logic.isEmpty(data.winner) then - data.finished = true - end - - return data, indexedScores -end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) ----@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ----@return table[] ----@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == 'default' then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local lastScore = NO_SCORE - local lastPlacement = NO_SCORE - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and (winner or '') == '' then - if finished then - winner = scoreIndex - end - end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or counter - lastPlacement = counter - lastScore = score or NO_SCORE - end - end - end + match.games = games + match.opponents = opponents - return opponents, winner -end + match.extradata = MatchFunctions.getExtraData(match) ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score or NO_SCORE) or NO_SCORE - local value2 = tonumber(tbl[key2].score or NO_SCORE) or NO_SCORE - return value1 > value2 + return match end -- @@ -187,224 +83,92 @@ end -- ---@param match table ----@return table -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match -end - --- Calculate the match scores based on the map results (counting map wins) --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local opponentNumber = 0 - for index = 1, MAX_NUM_OPPONENTS do - if String.isEmpty(match['opponent' .. index]) then - break - end - opponentNumber = index - end - local newScores = {} - local foundScores = false - - for i = 1, MAX_NUM_MAPS do - if match['map'..i] then - local winner = tonumber(match['map'..i].winner) - foundScores = true - if winner and winner > 0 and winner <= opponentNumber then - newScores[winner] = (newScores[winner] or 0) + 1 - end - else - break +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - for index = 1, opponentNumber do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end + table.insert(maps, map) + match[key] = nil end - return match -end - ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - return MatchGroupInput.getCommonTournamentVars(match) + return maps end ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) + if bestof then + Variables.varDefine('bestof', bestof) + return bestof + end - match.links = {} - local links = match.links - if match.faceit then links.faceit = 'https://www.faceit.com/en/halo_infinite/room/' .. match.faceit end - if match.halodatahive then links.halodatahive = 'https://halodatahive.com/Series/Summary/' .. match.halodatahive end - if match.stats then links.stats = match.stats end + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF +end - return match +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + end end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mvp = MatchGroupInput.readMvp(match), - casters = MatchGroupInput.readCasters(match), +function MatchFunctions.getLinks(match) + return { + faceit = match.faceit and ('https://www.faceit.com/en/halo_infinite/room/' .. match.faceit) or nil, + halodatahive = match.halodatahive and ('https://halodatahive.com/Series/Summary/' .. match.halodatahive) or nil, + stats = match.stats, } - return match end ---@param match table ---@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if Logic.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == Opponent.team and not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name) - end - end - end - - -- see if match should actually be finished if bestof limit was reached - if isScoreSet and not Logic.readBool(match.finished) then - local firstTo = math.ceil(match.bestof/2) - for _, item in pairs(opponents) do - if (tonumber(item.score or 0) or 0) >= firstTo then - match.finished = true - break - end - end - end - - -- check if match should actually be finished due to a non score status - if not Logic.readBool(match.finished) then - for _, opponent in pairs(opponents) do - if String.isNotEmpty(opponent.status) and opponent.status ~= 'S' then - match.finished = true - break - end - end - end - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if not Logic.isEmpty(match.winner) or Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match -end - --- Get Playerdata for non-team opponents ----@param match table ----@param opponentType OpponentType ----@param opponentIndex integer ----@return table[] -function matchFunctions.getPlayers(match, opponentType, opponentIndex) - local players = {} - for playerIndex = 1, Opponent.partySize(opponentType) do - -- parse player - local player = Json.parseIfString(match['opponent' .. opponentIndex .. '_p' .. playerIndex]) or {} - player.name = player.name or 'TBD' - player.flag = player.flag - player.displayname = player.displayname or player.name - if Table.isNotEmpty(player) then - table.insert(players, player) - end - end - - return players -end - ----@param player table ----@return boolean -function CustomMatchGroupInput._playerIsBye(player) - return (player.name or ''):lower() == MATCH_BYE or (player.displayname or ''):lower() == MATCH_BYE +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), + casters = MatchGroupInputUtil.readCasters(match), + } end -- -- map related functions -- --- Parse extradata information ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { +function MapFunctions.getExtraData(map, opponentCount) + return { comment = map.comment, } - return map -end - --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - if Logic.isNumeric(score) then - obj.status = 'S' - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/halo/match_summary.lua b/components/match2/wikis/halo/match_summary.lua index 2b587156380..d1521916fd3 100644 --- a/components/match2/wikis/halo/match_summary.lua +++ b/components/match2/wikis/halo/match_summary.lua @@ -119,14 +119,6 @@ function CustomMatchSummary.createBody(match) return body end ----@param game MatchGroupUtilGame ----@param opponentIndex integer ----@return Html -function CustomMatchSummary._gameScore(game, opponentIndex) - local score = game.scores[opponentIndex] or '' - return mw.html.create('div'):wikitext(score) -end - ---@param game MatchGroupUtilGame ---@return MatchSummaryRow function CustomMatchSummary._createMapRow(game) @@ -155,11 +147,11 @@ function CustomMatchSummary._createMapRow(game) local leftNode = mw.html.create('div') :addClass('brkts-popup-spaced') :node(CustomMatchSummary._addCheckmark(game.winner == 1)) - :node(CustomMatchSummary._gameScore(game, 1)) + :node(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) local rightNode = mw.html.create('div') :addClass('brkts-popup-spaced') - :node(CustomMatchSummary._gameScore(game, 2)) + :node(DisplayHelper.MapScore(game.scores[2], 2, game.resultType, game.walkover, game.winner)) :node(CustomMatchSummary._addCheckmark(game.winner == 2)) row:addElement(leftNode) diff --git a/components/match2/wikis/heroes/match_group_input_custom.lua b/components/match2/wikis/heroes/match_group_input_custom.lua index 250c8dfcd0f..b1b7e0e2989 100644 --- a/components/match2/wikis/heroes/match_group_input_custom.lua +++ b/components/match2/wikis/heroes/match_group_input_custom.lua @@ -69,7 +69,8 @@ function CustomMatchGroupInput.processMatch(match, options) MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) match.stream = Streams.processStreams(match) match.links = MatchFunctions.getLinks(match) @@ -122,8 +123,6 @@ function CustomMatchGroupInput.extractMaps(match, opponentCount) return maps end -CustomMatchGroupInput.processMap = FnUtil.identity - -- -- match related functions @@ -145,13 +144,6 @@ function MatchFunctions.getBestOf(match) return bestOf or DEFAULT_BESTOF end ----@param match table ----@return table -function MatchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) - return MatchGroupInputUtil.getCommonTournamentVars(match) -end - ---@param match table ---@return table function MatchFunctions.getLinks(match) diff --git a/components/match2/wikis/heroes/match_summary.lua b/components/match2/wikis/heroes/match_summary.lua index acf16869d61..7c7571e0609 100644 --- a/components/match2/wikis/heroes/match_summary.lua +++ b/components/match2/wikis/heroes/match_summary.lua @@ -18,6 +18,7 @@ local ExternalLinks = require('Module:ExternalLinks') local Icon = require('Module:Icon') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Page = require('Module:Page') local String = require('Module:StringUtils') local Table = require('Module:Table') @@ -29,19 +30,9 @@ local NUM_CHAMPIONS_PICK = 5 local GREEN_CHECK = Icon.makeIcon{iconName = 'winner', color = 'forest-green-text', size = '110%'} local NO_CHECK = '[[File:NoCheck.png|link=]]' local NO_CHARACTER = 'default' -local MAP_VETO_START = 'Start Map Veto' -local ARROW_LEFT = '[[File:Arrow sans left.svg|15x15px|link=|Left team starts]]' -local ARROW_RIGHT = '[[File:Arrow sans right.svg|15x15px|link=|Right team starts]]' local FP = Abbreviation.make('First Pick', 'First Pick for Heroes on this map') local TBD = Abbreviation.make('TBD', 'To Be Determined') -local VETO_TYPE_TO_TEXT = { - ban = 'BAN', - pick = 'PICK', - decider = 'DECIDER', - defaultban = 'DEFAULT BAN', -} - -- Champion Ban Class ---@class HeroesOfTheStormHeroBan: MatchSummaryRowInterface ---@operator call: HeroesOfTheStormHeroBan @@ -94,114 +85,25 @@ function ChampionBan:create() return self.root end --- Map Veto Class ----@class HeroesOfTheStormMapVeto: MatchSummaryRowInterface ----@operator call: HeroesOfTheStormMapVeto ----@field root Html ----@field table Html -local MapVeto = Class.new( - function(self) - self.root = mw.html.create('div'):addClass('brkts-popup-mapveto') - self.table = self.root:tag('table') - :addClass('wikitable-striped'):addClass('collapsible'):addClass('collapsed') - self:createHeader() - end -) - ----@return HeroesOfTheStormMapVeto -function MapVeto:createHeader() - self.table:tag('tr') - :tag('th'):css('width','33%'):done() - :tag('th'):css('width','34%'):wikitext('Map Veto'):done() - :tag('th'):css('width','33%'):done() - return self -end - ----@param firstVeto number? ----@param format string? ----@return HeroesOfTheStormMapVeto -function MapVeto:vetoStart(firstVeto, format) - format = format and ('Veto format: ' .. format) or nil - local textLeft - local textCenter - local textRight - if firstVeto == 1 then - textLeft = MAP_VETO_START - textCenter = ARROW_LEFT - textRight = format - elseif firstVeto == 2 then - textLeft = format - textCenter = ARROW_RIGHT - textRight = MAP_VETO_START - else return self end - - self.table:tag('tr'):addClass('brkts-popup-mapveto-vetostart') - :tag('th'):wikitext(textLeft or ''):done() - :tag('th'):wikitext(textCenter):done() - :tag('th'):wikitext(textRight or ''):done() - - return self -end +---@class HeroesOfTheStormMapVeto: VetoDisplay +local MapVeto = Class.new(MatchSummary.MapVeto) ---@param map1 string? ---@param map2 string? ----@return string, string -function MapVeto._displayMaps(map1, map2) +---@return string +---@return string +function MapVeto:displayMaps(map1, map2) if Logic.isEmpty(map1) and Logic.isEmpty(map2) then return TBD, TBD end - return Logic.isEmpty(map1) and FP or ('[[' .. map1 .. ']]'), - Logic.isEmpty(map2) and FP or ('[[' .. map2 .. ']]') + return self:displayMap(map1), self:displayMap(map2) end ----@param vetoType string? ----@param map1 string? ----@param map2 string? ----@return HeroesOfTheStormMapVeto -function MapVeto:addRound(vetoType, map1, map2) - map1, map2 = MapVeto._displayMaps(map1, map2) - - local vetoText = VETO_TYPE_TO_TEXT[vetoType] - - if not vetoText then return self end - - local class = 'brkts-popup-mapveto-' .. vetoType - - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoMap(row, map1) - self:addColumnVetoType(row, class, vetoText) - self:addColumnVetoMap(row, map2) - - self.table:node(row) - return self -end - ----@param row Html ----@param styleClass string ----@param vetoText string ----@return HeroesOfTheStormMapVeto -function MapVeto:addColumnVetoType(row, styleClass, vetoText) - row:tag('td') - :tag('span') - :addClass(styleClass) - :addClass('brkts-popup-mapveto-vetotype') - :wikitext(vetoText) - return self -end - ----@param row Html ----@param map string ----@return HeroesOfTheStormMapVeto -function MapVeto:addColumnVetoMap(row, map) - row:tag('td'):wikitext(map):done() - return self -end - ----@return Html -function MapVeto:create() - return self.root +---@param map string? +---@return string +function MapVeto:displayMap(map) + return Logic.isEmpty(map) and FP or Page.makeInternalLink(map) --[[@as string]] end ---@param args table @@ -299,21 +201,7 @@ function CustomMatchSummary.createBody(match) end -- Add the Map Vetoes - if match.extradata.mapveto then - local vetoData = match.extradata.mapveto - if vetoData then - local mapVeto = MapVeto() - if vetoData[1] and vetoData[1].vetostart then - mapVeto:vetoStart(tonumber(vetoData[1].vetostart), vetoData[1].format) - end - - for _,vetoRound in ipairs(vetoData) do - mapVeto:addRound(vetoRound.type, vetoRound.team1, vetoRound.team2) - end - - body:addRow(mapVeto) - end - end + body:addRow(MatchSummary.defaultMapVetoDisplay(match, MapVeto())) return body end diff --git a/components/match2/wikis/arenaofvalor/legacy/match_group_legacy_default.lua b/components/match2/wikis/honorofkings/legacy/match_group_legacy_default.lua similarity index 93% rename from components/match2/wikis/arenaofvalor/legacy/match_group_legacy_default.lua rename to components/match2/wikis/honorofkings/legacy/match_group_legacy_default.lua index 2698595d6c1..a0a6aa2cfe8 100644 --- a/components/match2/wikis/arenaofvalor/legacy/match_group_legacy_default.lua +++ b/components/match2/wikis/honorofkings/legacy/match_group_legacy_default.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- wiki=arenaofvalor +-- wiki=honorofkings -- page=Module:MatchGroup/Legacy/Default -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute @@ -15,7 +15,7 @@ local MatchGroupLegacy = Lua.import('Module:MatchGroup/Legacy') local MAX_NUMBER_OF_OPPONENTS = 2 local MAX_NUMBER_OF_PLAYERS = 5 ----@class ArenaofvalorMatchGroupLegacyDefault: MatchGroupLegacy +---@class HonorofkingsMatchGroupLegacyDefault: MatchGroupLegacy local MatchGroupLegacyDefault = Class.new(MatchGroupLegacy) ---@return table diff --git a/components/match2/wikis/arenaofvalor/legacy/match_maps_legacy.lua b/components/match2/wikis/honorofkings/legacy/match_maps_legacy.lua similarity index 99% rename from components/match2/wikis/arenaofvalor/legacy/match_maps_legacy.lua rename to components/match2/wikis/honorofkings/legacy/match_maps_legacy.lua index 96cea226a49..8448a907476 100644 --- a/components/match2/wikis/arenaofvalor/legacy/match_maps_legacy.lua +++ b/components/match2/wikis/honorofkings/legacy/match_maps_legacy.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- wiki=arenaofvalor +-- wiki=honorofkings -- page=Module:MatchMaps/Legacy -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute diff --git a/components/match2/wikis/arenaofvalor/match_group_input_custom.lua b/components/match2/wikis/honorofkings/match_group_input_custom.lua similarity index 95% rename from components/match2/wikis/arenaofvalor/match_group_input_custom.lua rename to components/match2/wikis/honorofkings/match_group_input_custom.lua index 49983db46f0..83245c8c56b 100644 --- a/components/match2/wikis/arenaofvalor/match_group_input_custom.lua +++ b/components/match2/wikis/honorofkings/match_group_input_custom.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- wiki=arenaofvalor +-- wiki=honorofkings -- page=Module:MatchGroup/Input/Custom -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute @@ -26,7 +26,6 @@ local DUMMY_MAP = 'default' local OPPONENT_CONFIG = { resolveRedirect = true, pagifyTeamNames = false, - pagifyPlayerNames = true, maxNumPlayers = 5, } @@ -76,7 +75,8 @@ function CustomMatchGroupInput.processMatch(match, options) MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) match.stream = Streams.processStreams(match) match.links = MatchFunctions.getLinks(match) @@ -129,8 +129,6 @@ function CustomMatchGroupInput.extractMaps(match, opponentCount) return maps end -CustomMatchGroupInput.processMap = FnUtil.identity - -- -- match related functions -- @@ -151,13 +149,6 @@ function MatchFunctions.getBestOf(match) return bestOf or DEFAULT_BESTOF end ----@param match table ----@return table -function MatchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) - return MatchGroupInputUtil.getCommonTournamentVars(match) -end - ---@param match table ---@return table function MatchFunctions.getLinks(match) diff --git a/components/match2/wikis/arenaofvalor/match_legacy.lua b/components/match2/wikis/honorofkings/match_legacy.lua similarity index 99% rename from components/match2/wikis/arenaofvalor/match_legacy.lua rename to components/match2/wikis/honorofkings/match_legacy.lua index 3232022aa29..eb56df53f3d 100644 --- a/components/match2/wikis/arenaofvalor/match_legacy.lua +++ b/components/match2/wikis/honorofkings/match_legacy.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- wiki=arenaofvalor +-- wiki=honorofkings -- page=Module:Match/Legacy -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute diff --git a/components/match2/wikis/arenaofvalor/match_summary.lua b/components/match2/wikis/honorofkings/match_summary.lua similarity index 99% rename from components/match2/wikis/arenaofvalor/match_summary.lua rename to components/match2/wikis/honorofkings/match_summary.lua index 6167aaf0197..97226f0e2e9 100644 --- a/components/match2/wikis/arenaofvalor/match_summary.lua +++ b/components/match2/wikis/honorofkings/match_summary.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- wiki=arenaofvalor +-- wiki=honorofkings -- page=Module:MatchSummary -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute diff --git a/components/match2/wikis/leagueoflegends/match_group_input_custom.lua b/components/match2/wikis/leagueoflegends/match_group_input_custom.lua index f1f69ba15cf..63d58b3b71b 100644 --- a/components/match2/wikis/leagueoflegends/match_group_input_custom.lua +++ b/components/match2/wikis/leagueoflegends/match_group_input_custom.lua @@ -23,7 +23,6 @@ local MatchGroupUtil = Lua.import('Module:MatchGroup/Util') local OPPONENT_CONFIG = { resolveRedirect = true, pagifyTeamNames = false, - pagifyPlayerNames = true, maxNumPlayers = 15, } local DEFAULT_MODE = 'team' @@ -80,7 +79,7 @@ function CustomMatchGroupInput.processMatchWithoutStandalone(MatchParser, match) local opponents = Array.mapIndexes(function(opponentIndex) return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) end) - local games = MatchFunctions.extractMaps(MatchParser, match, #opponents) + local games = MatchFunctions.extractMaps(MatchParser, match, opponents) match.bestof = MatchGroupInputUtil.getBestOf(match.bestof, games) local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) @@ -105,7 +104,8 @@ function CustomMatchGroupInput.processMatchWithoutStandalone(MatchParser, match) MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) match.stream = Streams.processStreams(match) match.links = MatchFunctions.getLinks(match) @@ -120,9 +120,9 @@ end ---@param MatchParser LeagueOfLegendsMatchParserInterface ---@param match table ----@param opponentCount integer +---@param opponents table[] ---@return table[] -function MatchFunctions.extractMaps(MatchParser, match, opponentCount) +function MatchFunctions.extractMaps(MatchParser, match, opponents) local maps = {} for key, mapInput, mapIndex in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do local map = MatchParser.getMap(mapInput) @@ -135,11 +135,11 @@ function MatchFunctions.extractMaps(MatchParser, match, opponentCount) map.length = MatchParser.getLength(map) map.vod = map.vod or String.nilIfEmpty(match['vodgame' .. mapIndex]) - map.participants = MapFunctions.getParticipants(MatchParser, map, opponentCount) - map.extradata = MapFunctions.getExtraData(MatchParser, map, opponentCount) + map.participants = MapFunctions.getParticipants(MatchParser, map, opponents) + map.extradata = MapFunctions.getExtraData(MatchParser, map, #opponents) map.finished = MatchGroupInputUtil.mapIsFinished(map) - local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local opponentInfo = Array.map(opponents, function(_, opponentIndex) local score, status = MatchGroupInputUtil.computeOpponentScore({ walkover = map.walkover, winner = map.winner, @@ -163,8 +163,6 @@ function MatchFunctions.extractMaps(MatchParser, match, opponentCount) return maps end -CustomMatchGroupInput.processMap = FnUtil.identity - ---@param maps table[] ---@return fun(opponentIndex: integer): integer function MatchFunctions.calculateMatchScore(maps) @@ -173,13 +171,6 @@ function MatchFunctions.calculateMatchScore(maps) end end ----@param match table ----@return table -function MatchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) - return MatchGroupInputUtil.getCommonTournamentVars(match) -end - ---@param match table ---@return table function MatchFunctions.getLinks(match) @@ -242,20 +233,34 @@ end -- Parse participant information ---@param MatchParser LeagueOfLegendsMatchParserInterface ---@param map table ----@param opponentCount integer +---@param opponents table[] ---@return table -function MapFunctions.getParticipants(MatchParser, map, opponentCount) - local participants = {} +function MapFunctions.getParticipants(MatchParser, map, opponents) + local allParticipants = {} local getCharacterName = FnUtil.curry(MatchGroupInputUtil.getCharacterName, HeroNames) - for opponentIndex = 1, opponentCount do - for playerIndex, participant in ipairs(MatchParser.getParticipants(map, opponentIndex) or {}) do - participant.character = getCharacterName(participant.character) - participants[opponentIndex .. '_' .. playerIndex] = participant - end - end + Array.forEach(opponents, function(opponent, opponentIndex) + local participantList = MatchParser.getParticipants(map, opponentIndex) or {} + local participants, unattachedParticipants = MatchGroupInputUtil.parseParticipants( + opponent.match2players, + participantList, + function (playerIndex) + local participant = participantList[playerIndex] + return participant and {name = participant.player} or nil + end, + function(playerIndex) + local participant = participantList[playerIndex] + participant.character = getCharacterName(participant.character) + return participant + end + ) + Array.forEach(unattachedParticipants, function(participant) + table.insert(participants, participant) + end) + Table.mergeInto(allParticipants, Table.map(participants, MatchGroupInputUtil.prefixPartcipants(opponentIndex))) + end) - return participants + return allParticipants end ---@param winnerInput string|integer|nil diff --git a/components/match2/wikis/leagueoflegends/match_page.lua b/components/match2/wikis/leagueoflegends/match_page.lua index 8eec4276862..634732256f8 100644 --- a/components/match2/wikis/leagueoflegends/match_page.lua +++ b/components/match2/wikis/leagueoflegends/match_page.lua @@ -57,7 +57,6 @@ end) local NO_CHARACTER = 'default' local NOT_PLAYED = 'np' local DEFAULT_ITEM = 'EmptyIcon' -local TEAMS = Array.range(1, 2) local AVAILABLE_FOR_TIERS = {1, 2, 3} local ITEMS_TO_SHOW = 6 @@ -87,13 +86,13 @@ function MatchPage.getByMatchId(props) -- Update the view model with game and team data Array.forEach(viewModel.games, function(game) game.finished = game.winner ~= nil and game.winner ~= -1 - game.teams = Array.map(TEAMS, function(teamIdx) + game.teams = Array.map(game.opponents, function(opponent, teamIdx) local team = {players = {}} team.scoreDisplay = game.winner == teamIdx and 'W' or game.finished and 'L' or '-' team.side = String.nilIfEmpty(game.extradata['team' .. teamIdx ..'side']) - for _, player in Table.iter.pairsByPrefix(game.participants, teamIdx .. '_') do + for _, player in ipairs(opponent.players) do table.insert(team.players, Table.mergeInto(player, { roleIcon = player.role .. ' ' .. team.side, items = Array.map(Array.range(1, ITEMS_TO_SHOW), function(idx) diff --git a/components/match2/wikis/magic/match_group_input_custom.lua b/components/match2/wikis/magic/match_group_input_custom.lua index 3911cb8d8b4..3842b61bf0d 100644 --- a/components/match2/wikis/magic/match_group_input_custom.lua +++ b/components/match2/wikis/magic/match_group_input_custom.lua @@ -7,36 +7,26 @@ -- local Array = require('Module:Array') -local DateExt = require('Module:Date/Ext') -local Json = require('Module:Json') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') local Table = require('Module:Table') local Variables = require('Module:Variables') +local Streams = require('Module:Links/Stream') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Streams = Lua.import('Module:Links/Stream') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local OpponentLibraries = require('Module:OpponentLibraries') -local Opponent = OpponentLibraries.Opponent +local OpponentLibrary = require('Module:OpponentLibraries') +local Opponent = OpponentLibrary.Opponent -local UNKNOWN_REASON_LOSS_STATUS = 'L' -local DEFAULT_WIN_STATUS = 'W' -local DEFAULT_WIN_RESULTTYPE = 'default' -local NO_SCORE = -1 -local SCORE_STATUS = 'S' -local ALLOWED_STATUSES = {DEFAULT_WIN_STATUS, 'FF', 'DQ', UNKNOWN_REASON_LOSS_STATUS} -local MAX_NUM_OPPONENTS = 2 -local DEFAULT_BEST_OF = 99 -local NOW = os.time(os.date('!*t')--[[@as osdateparam]]) -local BYE = 'BYE' -local MAX_NUM_MAPS = 30 +local DEFAULT_BESTOF = 99 -local CustomMatchGroupInput = {} +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, +} -CustomMatchGroupInput.walkoverProcessing = {} -local walkoverProcessing = CustomMatchGroupInput.walkoverProcessing +local CustomMatchGroupInput = {} -- called from Module:MatchGroup ---@param match table @@ -44,399 +34,108 @@ local walkoverProcessing = CustomMatchGroupInput.walkoverProcessing function CustomMatchGroupInput.processMatch(match) if Logic.readBool(match.ffa) then error('FFA matches are not yet supported') - -- later call ffa processing from here - elseif match['opponent' .. (MAX_NUM_OPPONENTS + 1)] then - error('Unexpected number of opponents in a non-FFA match') - end - - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = CustomMatchGroupInput._getExtraData(match) - match = CustomMatchGroupInput._getTournamentVars(match) - match = CustomMatchGroupInput._adjustData(match) - match = CustomMatchGroupInput._getVodStuff(match) - - return match -end - ----@param match any ----@return table -function CustomMatchGroupInput._getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'solo')) - return MatchGroupInput.getCommonTournamentVars(match) -end - ----@param match any ----@return table -function CustomMatchGroupInput._getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod) - - return match -end - ----@param match any ----@return table -function CustomMatchGroupInput._getExtraData(match) - match.extradata = {} - - for subGroupIndex = 1, MAX_NUM_MAPS do - local prefix = 'subgroup' .. subGroupIndex - - match.extradata[prefix .. 'header'] = CustomMatchGroupInput._getSubGroupHeader(subGroupIndex, match) - end - - return match -end - ----@param subGroupIndex integer ----@param match table ----@return string? -function CustomMatchGroupInput._getSubGroupHeader(subGroupIndex, match) - local header = match['set' .. subGroupIndex .. 'header'] - - return String.isNotEmpty(header) and header or nil -end - ----@param match table ----@return table -function CustomMatchGroupInput._adjustData(match) - --parse opponents + set base sumscores - match = CustomMatchGroupInput._opponentInput(match) - - --main processing done here - local subGroupIndex = 0 - for _, _, mapIndex in Table.iter.pairsByPrefix(match, 'map') do - match, subGroupIndex = CustomMatchGroupInput._mapInput(match, mapIndex, subGroupIndex) end - - match = CustomMatchGroupInput._matchWinnerProcessing(match) - - CustomMatchGroupInput._setPlacements(match) - if CustomMatchGroupInput._hasTeamOpponent(match) then - error('Team opponents are currently not yet supported on Magic wiki') + error('Team opponents are currently not yet supported on magic wiki') end - if Logic.isNumeric(match.winner) then - match.finished = true - end + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) - return match -end - ----@param match table ----@return table -function CustomMatchGroupInput._matchWinnerProcessing(match) - local bestof = tonumber(match.bestof) or Variables.varDefault('bestof', DEFAULT_BEST_OF) - match.bestof = bestof - Variables.varDefine('bestof', bestof) - - local scores = Array.map(Array.range(1, MAX_NUM_OPPONENTS), function(opponentIndex) - local opponent = match['opponent' .. opponentIndex] - if not opponent then - return NO_SCORE - end - - -- set the score either from manual input or sumscore - opponent.score = Table.includes(ALLOWED_STATUSES, string.upper(opponent.score or '')) - and string.upper(opponent.score) - or tonumber(opponent.score) or tonumber(opponent.sumscore) or NO_SCORE - - return opponent.score + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) + local games = CustomMatchGroupInput.extractMaps(match, opponents) + match.bestof = CustomMatchGroupInput.getBestOf(match) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and CustomMatchGroupInput.calculateMatchScore(games) + or nil + + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) end) - walkoverProcessing.walkover(match, scores) - - if match.resulttype == DEFAULT_WIN_RESULTTYPE then - walkoverProcessing.applyMatchWalkoverToOpponents(match) - return match - end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) - if match.winner == 'draw' then - match.winner = 0 + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local opponent = match['opponent' .. opponentIndex] - if Logic.isNotEmpty(opponent) then - opponent.status = SCORE_STATUS - if opponent.score > bestof / 2 then - match.finished = Logic.emptyOr(match.finished, true) - match.winner = tonumber(match.winner) or opponentIndex - elseif match.winner == 0 or (opponent.score == bestof / 2 and match.opponent1.score == match.opponent2.score) then - match.finished = Logic.emptyOr(match.finished, true) - match.winner = 0 - match.resulttype = 'draw' - end - end - end - - match.winner = tonumber(match.winner) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'solo')) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) - CustomMatchGroupInput._checkFinished(match) + match.stream = Streams.processStreams(match) - if match.finished and not match.winner then - CustomMatchGroupInput._determineWinnerIfMissing(match, scores) - end + match.games = games + match.opponents = opponents return match end ---@param match table -function CustomMatchGroupInput._checkFinished(match) - if Logic.readBoolOrNil(match.finished) == false then - match.finished = false - elseif Logic.readBool(match.finished) or match.winner then - match.finished = true - end - - -- Match is automatically marked finished upon page edit after a - -- certain amount of time (depending on whether the date is exact) - if not match.finished and match.timestamp > DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end -end - ----@param match table ----@param scores number[] -function CustomMatchGroupInput._determineWinnerIfMissing(match, scores) - local maxScore = math.max(unpack(scores) or 0) - -- if we have a positive score and the match is finished we also have a winner - if maxScore > 0 then - if Array.all(scores, function(score) return score == maxScore end) then - match.winner = 0 - return +---@param opponents table[] +---@return table[] +function CustomMatchGroupInput.extractMaps(match, opponents) + local maps = {} + for key, map, mapIndex in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = { + comment = map.comment, + } + map.map = 'Game ' .. mapIndex + map.mode = Opponent.toMode(opponents[1].type, opponents[2].type) + + map.finished = MatchGroupInputUtil.mapIsFinished(map) + local opponentInfo = Array.map(opponents, function(_, opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }, CustomMatchGroupInput.calculateMapScore(map.winner, map.finished)) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - for opponentIndex, score in pairs(scores) do - if score == maxScore then - match.winner = opponentIndex - return - end - end + table.insert(maps, map) + match[key] = nil end -end ----@param match table -function CustomMatchGroupInput._setPlacements(match) - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local opponent = match['opponent' .. opponentIndex] - - if match.winner == opponentIndex or match.winner == 0 then - opponent.placement = 1 - elseif match.winner then - opponent.placement = 2 - end - end + return maps end ---[[ - -OpponentInput functions - -]]-- - ----@param match table ----@return table -function CustomMatchGroupInput._opponentInput(match) - local opponentIndex = 1 - local opponent = match['opponent' .. opponentIndex] - - while opponentIndex <= MAX_NUM_OPPONENTS and Logic.isNotEmpty(opponent) do - opponent = Json.parseIfString(opponent) or Opponent.blank() - - -- Convert byes to literals - if Opponent.isBye(opponent) - then - opponent = {type = Opponent.literal, name = BYE} - end - - --process input - if opponent.type == Opponent.team or opponent.type == Opponent.solo or - opponent.type == Opponent.literal then - - opponent = CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - else - error('Unsupported Opponent Type') - end - - --set initial opponent sumscore - opponent.sumscore = 0 - - match['opponent' .. opponentIndex] = opponent - - opponentIndex = opponentIndex + 1 - opponent = match['opponent' .. opponentIndex] +---@param maps table[] +---@return fun(opponentIndex: integer): integer +function CustomMatchGroupInput.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - - return match end ----@param record table ----@param timestamp integer ----@return table -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() - - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end - - Opponent.resolve(opponent, teamTemplateDate, {syncPlayer=true}) - - MatchGroupInput.mergeRecordWithOpponent(record, opponent) - - for _, player in pairs(record.players or {}) do - player.name = player.name:gsub(' ', '_') - end - - if record.name then - record.name = record.name:gsub(' ', '_') - end - - return record -end - ---[[ - -MapInput functions - -]]-- - ---@param match table ----@param mapIndex integer ----@param subGroupIndex integer ----@return table ---@return integer -function CustomMatchGroupInput._mapInput(match, mapIndex, subGroupIndex) - local map = Json.parseIfString(match['map' .. mapIndex]) - - if Table.isEmpty(map) then - match['map' .. mapIndex] = nil - return match, subGroupIndex - end - - -- Magic has no map names, use generic one instead - map.map = 'Game ' .. mapIndex - - -- set initial extradata for maps - map.extradata = { - comment = map.comment, - } - - -- determine score, resulttype, walkover and winner - map = CustomMatchGroupInput._mapWinnerProcessing(map) - - -- get participants data for the map + get map mode - map = CustomMatchGroupInput._processPlayerMapData(map, match) - - -- set sumscore to 0 if it isn't a number - if Logic.isEmpty(match.opponent1.sumscore) then - match.opponent1.sumscore = 0 - end - if Logic.isEmpty(match.opponent2.sumscore) then - match.opponent2.sumscore = 0 - end - - --adjust sumscore for winner opponent - if (tonumber(map.winner) or 0) > 0 then - match['opponent' .. map.winner].sumscore = - match['opponent' .. map.winner].sumscore + 1 - end - - match['map' .. mapIndex] = map - - return match, subGroupIndex -end - ----@param map table ----@return table -function CustomMatchGroupInput._mapWinnerProcessing(map) - if map.winner == 'skip' then - map.scores = {NO_SCORE, NO_SCORE} - map.resulttype = 'np' - - return map - end - - map.scores = {} - local hasManualScores = false - - local scores = Array.map(Array.range(1, MAX_NUM_OPPONENTS), function(opponentIndex) - local score = map['score' .. opponentIndex] - map.scores[opponentIndex] = tonumber(score) or NO_SCORE - - if String.isNotEmpty(score) then - hasManualScores = true - end - - return Table.includes(ALLOWED_STATUSES, string.upper(score or '')) - and score:upper() - or map.scores[opponentIndex] - end) - - if not hasManualScores then - local winnerInput = tonumber(map.winner) - if winnerInput == 1 then - map.scores = {1, 0} - elseif winnerInput == 2 then - map.scores = {0, 1} - end - - return map - end - - walkoverProcessing.walkover(map, scores) - - return map -end - ----@param map table ----@param match table ----@return table -function CustomMatchGroupInput._processPlayerMapData(map, match) - local participants = {} - - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local opponent = match['opponent' .. opponentIndex] - if Opponent.typeIsParty(opponent.type) then - CustomMatchGroupInput._processDefaultPlayerMapData( - opponent.match2players or {}, - opponentIndex, - map, - participants - ) - elseif opponent.type == Opponent.team then - error('Team opponents are currently not yet supported on magic wiki') - end - end - - map.mode = Opponent.toMode(match.opponent1.type, match.opponent2.type) - - map.participants = participants - - return map -end - ----@param players table ----@param opponentIndex integer ----@param map table ----@param participants table -function CustomMatchGroupInput._processDefaultPlayerMapData(players, opponentIndex, map, participants) - for playerIndex = 1, #players do - participants[opponentIndex .. '_' .. playerIndex] = { - played = true, - } - end +function CustomMatchGroupInput.getBestOf(match) + local bestOf = tonumber(Logic.emptyOr(match.bestof, Variables.varDefault('match_bestof'))) + Variables.varDefine('match_bestof', bestOf) + return bestOf or DEFAULT_BESTOF end ---@param match table @@ -445,107 +144,17 @@ function CustomMatchGroupInput._hasTeamOpponent(match) return match.opponent1.type == Opponent.team or match.opponent2.type == Opponent.team end ----@param obj table ----@param scores (integer|string)[] -function walkoverProcessing.walkover(obj, scores) - local walkover = obj.walkover - - if Logic.isNumeric(walkover) then - walkoverProcessing.numericWalkover(obj, walkover) - elseif walkover then - walkoverProcessing.nonNumericWalkover(obj, walkover) - elseif #scores ~=2 then -- since we always have 2 opponents outside of ffa - error('Unexpected number of opponents when calculating winner') - elseif Array.all(scores, function(score) - return Table.includes(ALLOWED_STATUSES, score) and score ~= DEFAULT_WIN_STATUS - end) then - - walkoverProcessing.scoreDoubleWalkover(obj, scores) - elseif Array.any(scores, function(score) return Table.includes(ALLOWED_STATUSES, score) end) then - walkoverProcessing.scoreWalkover(obj, scores) - end -end - ----@param obj table ----@param walkover integer|string -function walkoverProcessing.numericWalkover(obj, walkover) - local winner = tonumber(walkover) - - obj.winner = winner - obj.finished = true - obj.walkover = UNKNOWN_REASON_LOSS_STATUS - obj.resulttype = DEFAULT_WIN_RESULTTYPE -end - ----@param obj table ----@param walkover integer|string -function walkoverProcessing.nonNumericWalkover(obj, walkover) - if not Table.includes(ALLOWED_STATUSES, string.upper(walkover)) then - error('Invalid walkover "' .. walkover .. '"') - elseif not Logic.isNumeric(obj.winner) then - error('Walkover without winner specified') - end - - obj.winner = tonumber(obj.winner) - obj.finished = true - obj.walkover = walkover:upper() - obj.resulttype = DEFAULT_WIN_RESULTTYPE -end - ----@param obj table ----@param scores string[] -function walkoverProcessing.scoreDoubleWalkover(obj, scores) - obj.winner = -1 - obj.finished = true - obj.walkover = scores[1] - obj.resulttype = DEFAULT_WIN_RESULTTYPE -end - ----@param obj table ----@param scores (string|number)[] -function walkoverProcessing.scoreWalkover(obj, scores) - local winner, status - - for scoreIndex, score in pairs(scores) do - score = string.upper(score) - if score == DEFAULT_WIN_STATUS then - winner = scoreIndex - elseif Table.includes(ALLOWED_STATUSES, score) then - status = score - else - status = UNKNOWN_REASON_LOSS_STATUS - end - end - - if not winner then - error('Invalid score combination "{' .. scores[1] .. ', ' .. scores[2] .. '}"') - end - - obj.winner = winner - obj.finished = true - obj.walkover = status - obj.resulttype = DEFAULT_WIN_RESULTTYPE -end - ----@param match table -function walkoverProcessing.applyMatchWalkoverToOpponents(match) - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local score = match['opponent' .. opponentIndex].score - - if Logic.isNumeric(score) or String.isEmpty(score) then - match['opponent' .. opponentIndex].score = String.isNotEmpty(score) and score or NO_SCORE - match['opponent' .. opponentIndex].status = match.walkover - elseif score and Table.includes(ALLOWED_STATUSES, score:upper()) then - match['opponent' .. opponentIndex].score = NO_SCORE - match['opponent' .. opponentIndex].status = score - else - error('Invalid score "' .. score .. '"') +---@param winnerInput string|integer|nil +---@param finished boolean +---@return fun(opponentIndex: integer): integer? +function CustomMatchGroupInput.calculateMapScore(winnerInput, finished) + local winner = tonumber(winnerInput) + return function(opponentIndex) + -- TODO Better to check if map has started, rather than finished, for a more correct handling + if not winner and not finished then + return end - end - - -- neither match.opponent0 nor match['opponent-1'] does exist hence the if - if match['opponent' .. match.winner] then - match['opponent' .. match.winner].status = DEFAULT_WIN_STATUS + return winner == opponentIndex and 1 or 0 end end diff --git a/components/match2/wikis/mobilelegends/match_group_input_custom.lua b/components/match2/wikis/mobilelegends/match_group_input_custom.lua index e33b189a729..a7f85bf950e 100644 --- a/components/match2/wikis/mobilelegends/match_group_input_custom.lua +++ b/components/match2/wikis/mobilelegends/match_group_input_custom.lua @@ -7,523 +7,202 @@ -- local Array = require('Module:Array') -local DateExt = require('Module:Date/Ext') local FnUtil = require('Module:FnUtil') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Operator = require('Module:Operator') local String = require('Module:StringUtils') local Table = require('Module:Table') local Variables = require('Module:Variables') -local ChampionNames = mw.loadData('Module:HeroNames') local Streams = require('Module:Links/Stream') +local ChampionNames = mw.loadData('Module:HeroNames') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') - -local STATUS_SCORE = 'S' -local STATUS_DRAW = 'D' -local STATUS_DEFAULT_WIN = 'W' -local STATUS_FORFEIT = 'FF' -local STATUS_DISQUALIFIED = 'DQ' -local STATUS_DEFAULT_LOSS = 'L' -local ALLOWED_STATUSES = { - STATUS_DRAW, - STATUS_DEFAULT_WIN, - STATUS_FORFEIT, - STATUS_DISQUALIFIED, - STATUS_DEFAULT_LOSS, -} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_PLAYERS = 5 -local DEFAULT_BESTOF = 3 -local DEFAULT_MODE = 'team' -local NO_SCORE = -99 -local DUMMY_MAP = 'default' -local NP_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local DEFAULT_RESULT_TYPE = 'default' -local NOT_PLAYED_SCORE = -1 -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') + +local DEFAULT_BESTOF_MATCH = 3 -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} --- called from Module:MatchGroup+ ---@param match table ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - -- Count number of maps, check for empty maps to remove, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) - - -- process match - Table.mergeInto( - match, - matchFunctions.readDate(match) - ) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) - - -- Adjust map data, especially set participants data - match = matchFunctions.adjustMapData(match) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - return match -end + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date, {'tournament_enddate'})) ----@param match table ----@return table -function matchFunctions.adjustMapData(match) - local opponents = {} - for opponentIndex = 1, MAX_NUM_OPPONENTS do - opponents[opponentIndex] = match['opponent' .. opponentIndex] - end - local mapIndex = 1 - while match['map'..mapIndex] do - match['map'..mapIndex] = mapFunctions.getParticipants(match['map'..mapIndex], opponents) - mapIndex = mapIndex + 1 - end + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) + local games = CustomMatchGroupInput.extractMaps(match, opponents) + match.bestof = MatchFunctions.getBestOf(match) - return match -end + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games, match.bestof) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - if map.map == DUMMY_MAP then - map.map = nil - end - map = mapFunctions.getScoresAndWinner(map) + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) - return map -end - ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() - - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) - Opponent.resolve(opponent, teamTemplateDate, {syncPlayer=true}) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + match.stream = Streams.processStreams(match) ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if - Table.includes(NP_STATUSES, data.finished) or - Table.includes(NP_STATUSES, data.winner) - then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif CustomMatchGroupInput.placementCheckSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = DEFAULT_RESULT_TYPE - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, DEFAULT_RESULT_TYPE) - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end - end + match.games = games + match.opponents = opponents - --set it as finished if we have a winner - if not Logic.isEmpty(data.winner) then - data.finished = true - end + match.extradata = MatchFunctions.getExtraData(match) - return data, indexedScores + return match end +---@param match table ---@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ---@return table[] ----@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == DEFAULT_RESULT_TYPE then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end +function CustomMatchGroupInput.extractMaps(match, opponents) + local maps = {} + for key, map, mapIndex in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.vod = map.vod or String.nilIfEmpty(match['vodgame' .. mapIndex]) + map.participants = MapFunctions.getParticipants(map, opponents) + map.extradata = MapFunctions.getExtraData(map, #opponents) + + local opponentInfo = Array.map(opponents, function(_, opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.finished = MatchGroupInputUtil.mapIsFinished(map, opponentInfo) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - else - local lastScore = NO_SCORE - local lastPlacement = NO_SCORE - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and (winner or '') == '' then - if finished then - winner = scoreIndex - end - end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or counter - lastPlacement = counter - lastScore = score or NO_SCORE - end - end - end - return opponents, winner -end + table.insert(maps, map) + match[key] = nil + end ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score or NO_SCORE) or NO_SCORE - local value2 = tonumber(tbl[key2].score or NO_SCORE) or NO_SCORE - return value1 > value2 -end - --- Check if any opponent has a none-standard status ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckSpecialStatus(tbl) - return Table.any(tbl, - function (_, scoreinfo) - return scoreinfo.status ~= STATUS_SCORE and String.isNotEmpty(scoreinfo.status) - end - ) + return maps end -- -- match related functions -- ----@param match table ----@return table -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match -end - --- Calculate the match scores based on the map results (counting map wins) --- Only update an opponents result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local newScores = {} - local foundScores = false - - local mapIndex = 1 - while match['map'..mapIndex] do - local winner = tonumber(match['map'..mapIndex].winner) - foundScores = true - if winner and winner > 0 and winner <= MAX_NUM_OPPONENTS then - newScores[winner] = (newScores[winner] or 0) + 1 - end - mapIndex = mapIndex + 1 +---@param maps table[] +---@param bestOf integer +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps, bestOf) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - - for index = 1, MAX_NUM_OPPONENTS do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end - end - - return match -end - ----@param matchArgs table ----@return {date: string, dateexact: boolean, timestamp: integer, timezoneId: string?, timezoneOffset: string?} -function matchFunctions.readDate(matchArgs) - return MatchGroupInput.readDate(matchArgs.date, { - 'tournament_enddate', - 'tournament_startdate', - }) end ---@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - return MatchGroupInput.getCommonTournamentVars(match) -end - ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - - match.links = {} - local links = match.links - if match.reddit then links.reddit = match.reddit end - - return match +---@return integer +function MatchFunctions.getBestOf(match) + local bestof = tonumber(Logic.emptyOr(match.bestof, Variables.varDefault('bestof'))) + Variables.varDefine('bestof', bestof) + return bestof or DEFAULT_BESTOF_MATCH end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mvp = MatchGroupInput.readMvp(match), - mvpteam = match.mvpteam or match.winner, - casters = MatchGroupInput.readCasters(match, {noSort = true}), +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), + casters = MatchGroupInputUtil.readCasters(match, {noSort = true}), } - return match -end - ----@param match table ----@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - opponent.score = string.upper(opponent.score or '') - if Logic.isNumeric(opponent.score) then - opponent.score = tonumber(opponent.score) - opponent.status = STATUS_SCORE - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = NOT_PLAYED_SCORE - end - - -- get players from vars for teams - assert(Opponent.isType(opponent.type), 'Unsupported Opponent Type "' .. (opponent.type or '') .. '"') - if opponent.type == Opponent.team then - if not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name, { - resolveRedirect = true, - applyUnderScores = true, - maxNumPlayers = MAX_NUM_PLAYERS, - }) - end - end - - opponents[opponentIndex] = opponent - end - end - - --apply walkover input - match.walkover = string.upper(match.walkover or '') - if Logic.isNumeric(match.walkover) then - local winnerIndex = tonumber(match.walkover) - opponents = matchFunctions._makeAllOpponentsLoseByWalkover(opponents, STATUS_DEFAULT_LOSS) - opponents[winnerIndex].status = STATUS_DEFAULT_WIN - match.finished = true - elseif Logic.isNumeric(match.winner) and Table.includes(ALLOWED_STATUSES, match.walkover) then - local winnerIndex = tonumber(match.winner) - opponents = matchFunctions._makeAllOpponentsLoseByWalkover(opponents, match.walkover) - opponents[winnerIndex].status = STATUS_DEFAULT_WIN - match.finished = true - end - - -- see if match should actually be finished if bestof limit was reached - match.finished = Logic.readBool(match.finished) - or isScoreSet and ( - Array.any(opponents, function(opponent) return tonumber(opponent.score or 0) > match.bestof/2 end) - or Array.all(opponents, function(opponent) return tonumber(opponent.score or 0) == match.bestof/2 end) - ) - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if - not Logic.isEmpty(match.winner) or - Logic.readBool(match.finished) or - CustomMatchGroupInput.placementCheckSpecialStatus(opponents) - then - match.finished = true - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match -end - ----@param opponents table[] ----@param walkoverType string? ----@return any -function matchFunctions._makeAllOpponentsLoseByWalkover(opponents, walkoverType) - for index, _ in pairs(opponents) do - opponents[index].score = NOT_PLAYED_SCORE - opponents[index].status = walkoverType - end - return opponents end -- -- map related functions -- --- Parse extradata information ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getAdditionalExtraData(map) - map.extradata.comment = map.comment - map.extradata.team1side = string.lower(map.team1side or '') - map.extradata.team2side = string.lower(map.team2side or '') - - return map -end +function MapFunctions.getExtraData(map, opponentCount) + local extradata = { + comment = map.comment, + team1side = string.lower(map.team1side or ''), + team2side = string.lower(map.team2side or ''), + } --- Parse participant information ----@param map table ----@param opponents table[] ----@return table -function mapFunctions.getParticipants(map, opponents) - local participants = {} - local championData = {} - local getCharacterName = FnUtil.curry(MatchGroupInput.getCharacterName, ChampionNames) - for opponentIndex = 1, MAX_NUM_OPPONENTS do - for playerIndex = 1, MAX_NUM_PLAYERS do - local champ = map['t' .. opponentIndex .. 'h' .. playerIndex] - championData['team' .. opponentIndex .. 'champion' .. playerIndex] = getCharacterName(champ) - - championData['t' .. opponentIndex .. 'kda' .. playerIndex] = - map['t' .. opponentIndex .. 'kda' .. playerIndex] - - local player = map['t' .. opponentIndex .. 'p' .. playerIndex] - if String.isNotEmpty(player) then - participants = mapFunctions.attachToParticipant( - player, - opponentIndex, - opponents[opponentIndex].match2players, - participants, - championData['team' .. opponentIndex .. 'champion' .. playerIndex], - championData['team' .. opponentIndex .. 'kda' .. playerIndex] - ) - end + local getCharacterName = FnUtil.curry(MatchGroupInputUtil.getCharacterName, ChampionNames) + for opponentIndex = 1, opponentCount do + for _, ban, idx in Table.iter.pairsByPrefix(map, 't' .. opponentIndex .. 'b') do + extradata['team' .. opponentIndex .. 'ban' .. idx] = getCharacterName(ban) end - local banIndex = 1 - local currentBan = map['t' .. opponentIndex .. 'b' .. banIndex] - while currentBan do - championData['team' .. opponentIndex .. 'ban' .. banIndex] = getCharacterName(currentBan) - banIndex = banIndex + 1 - currentBan = map['t' .. opponentIndex .. 'b' .. banIndex] + for _, pick, idx in Table.iter.pairsByPrefix(map, 't' .. opponentIndex .. 'h') do + extradata['team' .. opponentIndex .. 'champion' .. idx] = getCharacterName(pick) end end - map.extradata = championData - map.participants = participants - return mapFunctions.getAdditionalExtraData(map) + return extradata end ----@param player string ----@param opponentIndex integer ----@param players table[] ----@param participants table ----@param champion string? ----@param kda string? ----@return table -function mapFunctions.attachToParticipant(player, opponentIndex, players, participants, champion, kda) - player = mw.ext.TeamLiquidIntegration.resolve_redirect(player):gsub(' ', '_') - for playerIndex, item in pairs(players or {}) do - if player == item.name then - participants[opponentIndex .. '_' .. playerIndex] = { - champion = champion, - kda = kda - } - break - end - end - - return participants -end - --- Calculate Score and Winner of the map ---@param map table +---@param opponents table[] ---@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - if Logic.isNumeric(score) then - obj.status = STATUS_SCORE - score = tonumber(score) - map['score' .. scoreIndex] = score - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = NOT_PLAYED_SCORE +function MapFunctions.getParticipants(map, opponents) + local allParticipants = {} + local getCharacterName = FnUtil.curry(MatchGroupInputUtil.getCharacterName, ChampionNames) + Array.forEach(opponents, function(opponent, opponentIndex) + local players = Array.mapIndexes(function(playerIndex) + return opponent.match2players[playerIndex] or Logic.nilIfEmpty(map['t' .. opponentIndex .. 'h' .. playerIndex]) + end) + local participants, unattachedParticipants = MatchGroupInputUtil.parseParticipants( + opponent.match2players, + players, + function(playerIndex) + local player = map['t' .. opponentIndex .. 'p' .. playerIndex] + return player and {name = player} or nil + end, + function(playerIndex, playerIdData) + local character = map['t' .. opponentIndex .. 'h' .. playerIndex] + return { + champion = getCharacterName(character), + } end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) + ) + Array.forEach(unattachedParticipants, function(participant) + table.insert(participants, participant) + end) + Table.mergeInto(allParticipants, Table.map(participants, MatchGroupInputUtil.prefixPartcipants(opponentIndex))) + end) - return map + return allParticipants end return CustomMatchGroupInput diff --git a/components/match2/wikis/omegastrikers/match_group_input_custom.lua b/components/match2/wikis/omegastrikers/match_group_input_custom.lua index ff41a786689..97d589b99d4 100644 --- a/components/match2/wikis/omegastrikers/match_group_input_custom.lua +++ b/components/match2/wikis/omegastrikers/match_group_input_custom.lua @@ -7,289 +7,131 @@ -- local Array = require('Module:Array') -local DateExt = require('Module:Date/Ext') local FnUtil = require('Module:FnUtil') -local Json = require('Module:Json') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') local Table = require('Module:Table') -local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') local Streams = require('Module:Links/Stream') local StrikerNames = mw.loadData('Module:StrikerNames') -local Opponent = Lua.import('Module:Opponent') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L', 'D'} -local STATUS_TO_WALKOVER = {FF = 'ff', DQ = 'dq', L = 'l'} -local NOT_PLAYED = {'skip', 'np'} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_PLAYERS = 10 -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_BESTOF_MATCH = 5 +local DEFAULT_BESTOF_MAP = 3 -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} --- called from Module:MatchGroup ---@param match table ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - Table.mergeInto( - match, - matchFunctions.readDate(match) - ) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - CustomMatchGroupInput._underScoreAdjusts(match) + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) - return match -end - ----@param match table -function CustomMatchGroupInput._underScoreAdjusts(match) - local fixUnderscore = function(page) - return page and page:gsub(' ', '_') or page - end - - for opponentKey, opponent in Table.iter.pairsByPrefix(match, 'opponent') do - opponent.name = fixUnderscore(opponent.name) - - for _, player in Table.iter.pairsByPrefix(match, opponentKey .. '_p') do - player.name = fixUnderscore(player.name) - end - end -end - --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - local bestof = tonumber(Logic.emptyOr(map.bestof, Variables.varDefault('map_bestof'))) or 3 - Variables.varDefine('map_bestof', bestof) - map.bestof = bestof - - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) - map = mapFunctions.getParticipantsData(map) - - return map -end - ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() - - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) + local games = CustomMatchGroupInput.extractMaps(match, opponents) + match.bestof = MatchFunctions.getBestOf(match) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games, match.bestof) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) --- function to sort out winner/placements ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput._placementSortFunction(tbl, key1, key2) - local op1 = tbl[key1] - local op2 = tbl[key2] - local op1norm = op1.status == 'S' - local op2norm = op2.status == 'S' - if op1norm and op2norm then return tonumber(op1.score) > tonumber(op2.score) - elseif not op2norm then return true - elseif not op1norm then return false - elseif op1.status == 'W' then return true - elseif op1.status == 'DQ' then return false - elseif op2.status == 'W' then return false - elseif op2.status == 'DQ' then return true - else return true + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end -end - --- --- match related functions --- ----@param matchArgs table ----@return {date: string, dateexact: boolean, timestamp: integer, timezoneId: string?, timezoneOffset: string?} -function matchFunctions.readDate(matchArgs) - return MatchGroupInput.readDate(matchArgs.date, {'tournament_enddate'}) -end ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - return MatchGroupInput.getCommonTournamentVars(match) -end + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) ----@param match table ----@return table -function matchFunctions.getVodStuff(match) match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) + + match.games = games + match.opponents = opponents return match end ----@param args table ----@return table -function matchFunctions.getOpponents(args) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - - local sumscores = {} - for _, map in Table.iter.pairsByPrefix(args, 'map') do - if map.winner then - sumscores[map.winner] = (sumscores[map.winner] or 0) + 1 - end - end - - local bestof = Logic.emptyOr(args.bestof, Variables.varDefault('bestof', 5)) - bestof = tonumber(bestof) or 5 - Variables.varDefine('bestof', bestof) - local firstTo = math.ceil(bestof / 2) - - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = args['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, args.timestamp) - - opponent.score = opponent.score or sumscores[opponentIndex] - - -- apply status - if TypeUtil.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - if firstTo <= tonumber(opponent.score) then - args.finished = true - end - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - args.finished = true - end - - --set Walkover from Opponent status - args.walkover = args.walkover or STATUS_TO_WALKOVER[opponent.status] - - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == 'team' and not Logic.isEmpty(opponent.name) then - args = matchFunctions.getPlayers(args, opponentIndex, opponent.name) - end - end - end - - opponents = Array.map(opponents, function(opponent) - opponent.score = tonumber(opponent.score) - return opponent - end) - - --set resulttype to 'default' if walkover is set - if args.walkover then - args.resulttype = 'default' - elseif isScoreSet then - -- if isScoreSet is true we have scores from at least one opponent - -- in case the other opponent(s) have no score set manually and - -- no sumscore set we have to set them to 0 now so they are - -- not displayed as blank - for _, opponent in pairs(opponents) do - if - String.isEmpty(opponent.status) - and Logic.isEmpty(opponent.score) - then - opponent.score = 0 - opponent.status = 'S' - end +---@param match table +---@param opponents table[] +---@return table[] +function CustomMatchGroupInput.extractMaps(match, opponents) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.bestof = MapFunctions.getBestOf(map) + map.participants = MapFunctions.getParticipants(map, opponents) + map.extradata = MapFunctions.getExtraData(map, #opponents) + + local opponentInfo = Array.map(opponents, function(_, opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.finished = MatchGroupInputUtil.mapIsFinished(map, opponentInfo) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(args.finished) then - local threshold = args.dateexact and 30800 or 86400 - if args.timestamp + threshold < NOW then - args.finished = true - end + table.insert(maps, map) + match[key] = nil end - -- apply placements and winner if finshed - if Logic.readBool(args.finished) then - if #opponents == 2 and opponents[1].score == opponents[2].score and opponents[1].score == firstTo then - args.winner = tonumber(args.winner) or 0 - end - - local currentPlacement = 1 - local placement = 1 - local lastOpponentScore, lastOpponentStatus - for opponentIndex, opponent in Table.iter.spairs(opponents, CustomMatchGroupInput._placementSortFunction) do - if lastOpponentStatus ~= opponent.status or lastOpponentScore ~= opponent.score then - lastOpponentStatus = opponent.status - lastOpponentScore = opponent.score - currentPlacement = placement - end - + return maps +end - if currentPlacement == 1 then - args.winner = tonumber(args.winner) or opponentIndex - end - opponent.placement = currentPlacement - args['opponent' .. opponentIndex] = opponent - placement = placement + 1 - end - -- only apply arg changes otherwise - else - for opponentIndex, opponent in pairs(opponents) do - args['opponent' .. opponentIndex] = opponent - end - end +-- +-- match related functions +-- - if args.winner == 0 then - args.resulttype = args.resulttype or 'draw' +---@param maps table[] +---@param bestOf integer +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps, bestOf) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - - return args end ---@param match table ----@param opponentIndex integer ----@param teamName string ----@return table -function matchFunctions.getPlayers(match, opponentIndex, teamName) - for playerIndex = 1, MAX_NUM_PLAYERS do - -- parse player - local player = Json.parseIfString(match['opponent' .. opponentIndex .. '_p' .. playerIndex]) or {} - player.name = player.name or Variables.varDefault(teamName .. '_p' .. playerIndex) - player.flag = player.flag or Variables.varDefault(teamName .. '_p' .. playerIndex .. 'flag') - if not Table.isEmpty(player) then - match['opponent' .. opponentIndex .. '_p' .. playerIndex] = player - end - end - return match +---@return integer +function MatchFunctions.getBestOf(match) + local bestof = tonumber(Logic.emptyOr(match.bestof, Variables.varDefault('bestof'))) + Variables.varDefine('bestof', bestof) + return bestof or DEFAULT_BESTOF_MATCH end -- @@ -297,72 +139,69 @@ end -- ---@param map table +---@return integer +function MapFunctions.getBestOf(map) + local bestof = tonumber(Logic.emptyOr(map.bestof, Variables.varDefault('map_bestof'))) + Variables.varDefine('map_bestof', bestof) + return bestof or DEFAULT_BESTOF_MAP +end + +---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getExtraData(map) +function MapFunctions.getExtraData(map, opponentCount) + local extradata = { + bestof = map.bestof, + comment = map.comment, + } + local bans = {} - local getCharacterName = FnUtil.curry(MatchGroupInput.getCharacterName, StrikerNames) - for opponentIndex = 1, MAX_NUM_OPPONENTS do + local getCharacterName = FnUtil.curry(MatchGroupInputUtil.getCharacterName, StrikerNames) + for opponentIndex = 1, opponentCount do bans['team' .. opponentIndex] = {} for _, ban in Table.iter.pairsByPrefix(map, 't' .. opponentIndex .. 'b') do - table.insert(bans['team' .. opponentIndex], getCharacterName(ban)) + ban = getCharacterName(ban) + table.insert(bans['team' .. opponentIndex], ban) end end - map.extradata = { - comment = map.comment, - bans = Json.stringify(bans) - } + extradata.bans = bans - return map + return extradata end ---@param map table +---@param opponents table[] ---@return table -function mapFunctions.getScoresAndWinner(map) - map.score1 = tonumber(map.score1) - map.score2 = tonumber(map.score2) - map.scores = { map.score1, map.score2 } - if Table.includes(NOT_PLAYED, string.lower(map.winner or '')) then - map.winner = 0 - map.resulttype = 'np' - elseif Logic.isNumeric(map.winner) then - map.winner = tonumber(map.winner) - end - local firstTo = math.ceil(map.bestof / 2) - if (map.score1 or 0) >= firstTo then - map.winner = 1 - map.finished = true - elseif (map.score2 or 0) >= firstTo then - map.winner = 2 - map.finished = true - end - - return map -end - ----@param map table ----@return table -function mapFunctions.getParticipantsData(map) - local participants = {} - local getCharacterName = FnUtil.curry(MatchGroupInput.getCharacterName, StrikerNames) - local maximumPickIndex = 0 - for opponentIndex = 1, MAX_NUM_OPPONENTS do - for _, player, playerIndex in Table.iter.pairsByPrefix(map, 't' .. opponentIndex .. 'p') do - participants[opponentIndex .. '_' .. playerIndex] = {player = player} - end - - for _, striker, pickIndex in Table.iter.pairsByPrefix(map, 't' .. opponentIndex .. 'c') do - participants[opponentIndex .. '_' .. pickIndex] = participants[opponentIndex .. '_' .. pickIndex] or {} - participants[opponentIndex .. '_' .. pickIndex].striker = getCharacterName(striker) - if maximumPickIndex < pickIndex then - maximumPickIndex = pickIndex +function MapFunctions.getParticipants(map, opponents) + local allParticipants = {} + local getCharacterName = FnUtil.curry(MatchGroupInputUtil.getCharacterName, StrikerNames) + Array.forEach(opponents, function(opponent, opponentIndex) + local players = Array.mapIndexes(function(playerIndex) + return opponent.match2players[playerIndex] or Logic.nilIfEmpty(map['t' .. opponentIndex .. 'c' .. playerIndex]) + end) + local participants, unattachedParticipants = MatchGroupInputUtil.parseParticipants( + opponent.match2players, + players, + function(playerIndex) + local player = map['t' .. opponentIndex .. 'p' .. playerIndex] + return player and {name = player} or nil + end, + function(playerIndex, playerIdData) + local striker = map['t' .. opponentIndex .. 'c' .. playerIndex] + return { + player = playerIdData.name, + striker = getCharacterName(striker), + } end - end - end + ) + Array.forEach(unattachedParticipants, function(participant) + table.insert(participants, participant) + end) + Table.mergeInto(allParticipants, Table.map(participants, MatchGroupInputUtil.prefixPartcipants(opponentIndex))) + end) - map.extradata.maximumpickindex = maximumPickIndex - map.participants = participants - return map + return allParticipants end return CustomMatchGroupInput diff --git a/components/match2/wikis/omegastrikers/match_summary.lua b/components/match2/wikis/omegastrikers/match_summary.lua index c76a386cd27..74e8acf3c3b 100644 --- a/components/match2/wikis/omegastrikers/match_summary.lua +++ b/components/match2/wikis/omegastrikers/match_summary.lua @@ -145,19 +145,23 @@ function CustomMatchSummary.createBody(match) local showGamePicks = {} for gameIndex, game in ipairs(match.games) do local pickData = {{}, {}} - local numberOfPicks = game.extradata.maximumpickindex or 0 local participants = game.participants - for index = 1, numberOfPicks do - if not Table.isEmpty(participants['1_' .. index]) then + local index = 1 + while true do + if Table.isEmpty(participants['1_' .. index]) and Table.isEmpty(participants['2_' .. index]) then + break + end + if Table.isNotEmpty(participants['1_' .. index]) then pickData[1][index] = participants['1_' .. index].striker end - if not Table.isEmpty(participants['2_' .. index]) then + if Table.isNotEmpty(participants['2_' .. index]) then pickData[2][index] = participants['2_' .. index].striker end + index = index + 1 end - if numberOfPicks > 0 then - pickData.numberOfPicks = numberOfPicks + if index > 1 then + pickData.numberOfPicks = index - 1 showGamePicks[gameIndex] = pickData end end diff --git a/components/match2/wikis/osu/match_group_input_custom.lua b/components/match2/wikis/osu/match_group_input_custom.lua index a253ead4ce5..8b00e5cba5e 100644 --- a/components/match2/wikis/osu/match_group_input_custom.lua +++ b/components/match2/wikis/osu/match_group_input_custom.lua @@ -6,182 +6,77 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- +local Array = require('Module:Array') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local Opponent = require('Module:Opponent') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') local Table = require('Module:Table') -local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') -local DateExt = require('Module:Date/Ext') -local Streams = require('Module:Links/Stream') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local NP_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local ALLOWED_VETOES = {'decider', 'pick', 'ban', 'defaultban', 'protect'} -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L', 'D'} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_MAPS = 20 +local ALLOWED_VETOES = Array.append(MatchGroupInputUtil.DEFAULT_ALLOWED_VETOES, 'protect') local DEFAULT_BESTOF = 3 - -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_MODE = 'team' -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} -- called from Module:MatchGroup ---@param match table +---@param options table? ---@return table -function CustomMatchGroupInput.processMatch(match) - -- Count number of maps, check for empty maps to remove, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) - - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) - - return match -end - --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) - - return map -end - ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() - - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end - - Opponent.resolve(opponent, teamTemplateDate, {syncPlayer=true}) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end - ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if - Table.includes(NP_STATUSES, data.finished) or - Table.includes(NP_STATUSES, data.winner) - then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = 'default' - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'default') - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end +function CustomMatchGroupInput.processMatch(match, options) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) + + local games = MatchFunctions.extractMaps(match, #opponents) + + match.bestof = MatchFunctions.getBestOf(match.bestof) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - --set it as finished if we have a winner - if Logic.isNotEmpty(data.winner) then - data.finished = true - end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) - return data, indexedScores -end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) ----@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ----@return table[] ----@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == 'default' then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local temporaryScore - local temporaryPlace = -99 - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) or '' - counter = counter + 1 - if counter == 1 and Logic.isEmpty(winner) then - if finished then - winner = scoreIndex - end - end - if temporaryScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or temporaryPlace - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or counter - temporaryPlace = counter - temporaryScore = score - end - end - end + match.games = games + match.opponents = opponents - return opponents, winner -end + match.extradata = MatchFunctions.getExtraData(match) ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score) or -99 - local value2 = tonumber(tbl[key2].score) or -99 - return value1 > value2 + return match end -- @@ -189,184 +84,100 @@ end -- ---@param match table ----@return table -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match -end - --- Calculate the match scores based on the map results (counting map wins) --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local opponentNumber = 0 - for index = 1, MAX_NUM_OPPONENTS do - if String.isEmpty(match['opponent' .. index]) then - break - end - opponentNumber = index - end - local newScores = {} - local foundScores = false - - for i = 1, MAX_NUM_MAPS do - if match['map'..i] then - local winner = tonumber(match['map'..i].winner) - foundScores = true - if winner and winner > 0 and winner <= opponentNumber then - newScores[winner] = (newScores[winner] or 0) + 1 +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local percentageScore = (map['score' .. opponentIndex] or ''):match('(%d+)%%') + if percentageScore then + return {score = map['score' .. opponentIndex], status = MatchGroupInputUtil.STATUS.SCORE} end - else - break + local scoreInput = string.gsub(map['score' .. opponentIndex] or '', ',', '') + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = scoreInput, + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end + + table.insert(maps, map) + match[key] = nil end - for index = 1, opponentNumber do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end + return maps +end + +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) + + if bestof then + Variables.varDefine('bestof', bestof) + return bestof end - return match + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF end ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - return MatchGroupInput.getCommonTournamentVars(match) +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + end end ---@param match table ---@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) +function MatchFunctions.getLinks(match) + local links = {preview = match.preview} - match.links = {} - local links = match.links - if match.preview then links.preview = match.preview end for key, linkPart in Table.iter.pairsByPrefix(match, 'mplink', {requireIndex = false}) do links[key] = 'https://osu.ppy.sh/community/matches/' .. linkPart end - return match + return links end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mvp = MatchGroupInput.readMvp(match), - mapveto = MatchGroupInput.getMapVeto(match, ALLOWED_VETOES), - casters = MatchGroupInput.readCasters(match), +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), + mapveto = MatchGroupInputUtil.getMapVeto(match, ALLOWED_VETOES), + casters = MatchGroupInputUtil.readCasters(match), } - return match -end - ----@param match table ----@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if TypeUtil.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - opponents[opponentIndex] = opponent - end - end - - -- see if match should actually be finished if bestof limit was reached - if isScoreSet and not Logic.readBool(match.finished) then - local firstTo = math.ceil(match.bestof/2) - for _, item in pairs(opponents) do - if (tonumber(item.score) or 0) >= firstTo then - match.finished = true - break - end - end - end - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if not String.isEmpty(match.winner) or Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match end -- -- map related functions -- --- Parse extradata information ---@param map table ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { +function MapFunctions.getExtraData(map) + return { comment = map.comment, header = map.header, } - return map -end - --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - if TypeUtil.isNumeric(score) then - obj.status = 'S' - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/osu/match_summary.lua b/components/match2/wikis/osu/match_summary.lua index b32825c3e17..0009c1f10f7 100644 --- a/components/match2/wikis/osu/match_summary.lua +++ b/components/match2/wikis/osu/match_summary.lua @@ -18,9 +18,6 @@ local Table = require('Module:Table') local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper') local MatchSummary = Lua.import('Module:MatchSummary/Base') -local MAP_VETO_START = 'Start Map Veto' -local ARROW_LEFT = '[[File:Arrow sans left.svg|15x15px|link=|Left team starts]]' -local ARROW_RIGHT = '[[File:Arrow sans right.svg|15x15px|link=|Right team starts]]' local NONE = '-' local TBD = Abbreviation.make('TBD', 'To Be Determined') @@ -37,69 +34,19 @@ local LINK_DATA = { LINK_DATA.mplink2 = LINK_DATA.mplink LINK_DATA.mplink3 = LINK_DATA.mplink -local VETO_TYPE_TO_TEXT = { - ban = 'BAN', - pick = 'PICK', - decider = 'DECIDER', - defaultban = 'DEFAULT BAN', - protect = 'PROTECT', +local VETO_TYPE_TO_TEXT = Table.copy(MatchSummary.DEFAULT_VETO_TYPE_TO_TEXT) +VETO_TYPE_TO_TEXT.protect = 'PROTECT' -} local CustomMatchSummary = {} --- Map Veto Class ----@class OsuMapVeto: MatchSummaryRowInterface ----@operator call: self ----@field root Html ----@field table Html -local MapVeto = Class.new( - function(self) - self.root = mw.html.create('div'):addClass('brkts-popup-mapveto') - self.table = self.root:tag('table') - :addClass('wikitable-striped'):addClass('collapsible'):addClass('collapsed') - self:createHeader() - end -) - ----@return self -function MapVeto:createHeader() - self.table:tag('tr') - :tag('th'):css('width','33%'):done() - :tag('th'):css('width','34%'):wikitext('Map Veto'):done() - :tag('th'):css('width','33%'):done() - return self -end - ----@param firstVeto number? ----@param format string? ----@return self -function MapVeto:vetoStart(firstVeto, format) - format = format and ('Veto format: ' .. format) or nil - local textLeft - local textCenter - local textRight - if firstVeto == 1 then - textLeft = MAP_VETO_START - textCenter = ARROW_LEFT - textRight = format - elseif firstVeto == 2 then - textLeft = format - textCenter = ARROW_RIGHT - textRight = MAP_VETO_START - else return self end - - self.table:tag('tr'):addClass('brkts-popup-mapveto-vetostart') - :tag('th'):wikitext(textLeft):done() - :tag('th'):wikitext(textCenter):done() - :tag('th'):wikitext(textRight):done() - - return self -end +---@class OsuMapVeto: VetoDisplay +local MapVeto = Class.new(MatchSummary.MapVeto) ---@param map1 string? ---@param map2 string? ----@return string, string -function MapVeto._displayMaps(map1, map2) +---@return string +---@return string +function MapVeto:displayMaps(map1, map2) if Logic.isEmpty(map1) and Logic.isEmpty(map2) then return TBD, TBD end @@ -108,69 +55,6 @@ function MapVeto._displayMaps(map1, map2) Page.makeInternalLink(map2) or NONE end ----@param map string? ----@return self -function MapVeto:addDecider(map) - map = Page.makeInternalLink(map) or TBD - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - self:addColumnVetoMap(row, map) - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - - self.table:node(row) - return self -end - ----@param vetoType string? ----@param map1 string? ----@param map2 string? ----@return self -function MapVeto:addRound(vetoType, map1, map2) - map1, map2 = MapVeto._displayMaps(map1, map2) - - local vetoText = VETO_TYPE_TO_TEXT[vetoType] - - if not vetoText then return self end - - local class = 'brkts-popup-mapveto-' .. vetoType - - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoMap(row, map1) - self:addColumnVetoType(row, class, vetoText) - self:addColumnVetoMap(row, map2) - - self.table:node(row) - return self -end - ----@param row Html ----@param styleClass string ----@param vetoText string ----@return self -function MapVeto:addColumnVetoType(row, styleClass, vetoText) - row:tag('td') - :tag('span') - :addClass(styleClass) - :addClass('brkts-popup-mapveto-vetotype') - :wikitext(vetoText) - return self -end - ----@param row Html ----@param map string? ----@return self -function MapVeto:addColumnVetoMap(row, map) - row:tag('td'):wikitext(map):done() - return self -end - ----@return Html -function MapVeto:create() - return self.root -end - ---@param args table ---@return Html function CustomMatchSummary.getByMatchId(args) @@ -225,34 +109,10 @@ function CustomMatchSummary.createBody(match) body:addRow(MatchSummary.makeCastersRow(match.extradata.casters)) -- Add the Map Vetoes - if match.extradata.mapveto then - local vetoData = match.extradata.mapveto - if vetoData then - local mapVeto = MapVeto() - - for _,vetoRound in ipairs(vetoData) do - if vetoRound.vetostart then - mapVeto:vetoStart(tonumber(vetoRound.vetostart)) - end - if vetoRound.type == 'decider' then - mapVeto:addDecider(vetoRound.decider) - else - mapVeto:addRound(vetoRound.type, vetoRound.team1, vetoRound.team2) - end - end - - body:addRow(mapVeto) - end - end + body:addRow(MatchSummary.defaultMapVetoDisplay(match, MapVeto(VETO_TYPE_TO_TEXT))) return body end ----@param game MatchGroupUtilGame ----@param opponentIndex integer ----@return Html -function CustomMatchSummary._gameScore(game, opponentIndex) - return mw.html.create('div'):wikitext(game.scores[opponentIndex]) -end ---@param game MatchGroupUtilGame ---@return MatchSummaryRow @@ -279,15 +139,24 @@ function CustomMatchSummary._createMapRow(game) centerNode:addClass('brkts-popup-spaced-map-skip') end + ---@param score integer|string|nil + ---@return integer|string|nil + local displayNumericScore = function(score) + if not Logic.isNumeric(score) then + return score + end + return mw.getContentLanguage():formatNum(score --[[@as integer]]) + end + local leftNode = mw.html.create('div') :addClass('brkts-popup-spaced') :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 1, Icons.CHECK)) - :node(CustomMatchSummary._gameScore(game, 1)) + :node(DisplayHelper.MapScore(displayNumericScore(game.scores[1]), 1, game.resultType, game.walkover, game.winner)) :css('width', '20%') local rightNode = mw.html.create('div') :addClass('brkts-popup-spaced') - :node(CustomMatchSummary._gameScore(game, 2)) + :node(DisplayHelper.MapScore(displayNumericScore(game.scores[2]), 2, game.resultType, game.walkover, game.winner)) :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 2, Icons.CHECK)) :css('width', '20%') diff --git a/components/match2/wikis/overwatch/match_group_input_custom.lua b/components/match2/wikis/overwatch/match_group_input_custom.lua index ab8f6c21665..6666057c8e8 100644 --- a/components/match2/wikis/overwatch/match_group_input_custom.lua +++ b/components/match2/wikis/overwatch/match_group_input_custom.lua @@ -7,7 +7,6 @@ -- local Array = require('Module:Array') -local FnUtil = require('Module:FnUtil') local Logic = require('Module:Logic') local Lua = require('Module:Lua') local Operator = require('Module:Operator') @@ -41,7 +40,7 @@ function CustomMatchGroupInput.processMatch(match, options) match.bestof = MatchFunctions.getBestOf(match) local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) - and MatchFunctions.calculateMatchScore(games, match.bestof) + and MatchFunctions.calculateMatchScore(games) or nil Array.forEach(opponents, function(opponent, opponentIndex) opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ @@ -61,7 +60,8 @@ function CustomMatchGroupInput.processMatch(match, options) MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) match.stream = Streams.processStreams(match) match.links = MatchFunctions.getLinks(match) @@ -114,16 +114,13 @@ function CustomMatchGroupInput.extractMaps(match, opponentCount) return maps end -CustomMatchGroupInput.processMap = FnUtil.identity - -- -- match related functions -- ---@param maps table[] ----@param bestOf integer ---@return fun(opponentIndex: integer): integer? -function MatchFunctions.calculateMatchScore(maps, bestOf) +function MatchFunctions.calculateMatchScore(maps) return function(opponentIndex) return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end @@ -137,13 +134,6 @@ function MatchFunctions.getBestOf(match) return bestof or DEFAULT_BESTOF end ----@param match table ----@return table -function MatchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - return MatchGroupInputUtil.getCommonTournamentVars(match) -end - ---@param match table ---@return table function MatchFunctions.getLinks(match) diff --git a/components/match2/wikis/pokemon/match_group_input_custom.lua b/components/match2/wikis/pokemon/match_group_input_custom.lua index d230a4f653e..98e700a5036 100644 --- a/components/match2/wikis/pokemon/match_group_input_custom.lua +++ b/components/match2/wikis/pokemon/match_group_input_custom.lua @@ -6,47 +6,30 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- -local DateExt = require('Module:Date/Ext') +local Array = require('Module:Array') +local ChampionNames = mw.loadData('Module:HeroNames') local FnUtil = require('Module:FnUtil') -local Json = require('Module:Json') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') local Table = require('Module:Table') local Variables = require('Module:Variables') -local ChampionNames = mw.loadData('Module:HeroNames') local Streams = require('Module:Links/Stream') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') - -local STATUS_SCORE = 'S' -local STATUS_DRAW = 'D' -local STATUS_DEFAULT_WIN = 'W' -local STATUS_FORFEIT = 'FF' -local STATUS_DISQUALIFIED = 'DQ' -local STATUS_DEFAULT_LOSS = 'L' -local ALLOWED_STATUSES = { - STATUS_DRAW, - STATUS_DEFAULT_WIN, - STATUS_FORFEIT, - STATUS_DISQUALIFIED, - STATUS_DEFAULT_LOSS, -} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_PLAYERS = 5 +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') + local DEFAULT_BESTOF = 3 local DEFAULT_MODE = 'team' -local NO_SCORE = -99 local DUMMY_MAP = 'default' -local NP_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local DEFAULT_RESULT_TYPE = 'default' -local NOT_PLAYED_SCORE = -1 -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) --- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, + maxNumPlayers = 5, +} + +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} @@ -55,482 +38,161 @@ local CustomMatchGroupInput = {} ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - -- Count number of maps, check for empty maps to remove, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) - - -- process match - Table.mergeInto( - match, - matchFunctions.readDate(match) - ) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) - - -- Adjust map data, especially set participants data - match = matchFunctions.adjustMapData(match) - - return match -end - ----@param match table ----@return table -function matchFunctions.adjustMapData(match) - local opponents = {} - for opponentIndex = 1, MAX_NUM_OPPONENTS do - opponents[opponentIndex] = match['opponent' .. opponentIndex] - end - local mapIndex = 1 - while match['map'..mapIndex] do - match['map'..mapIndex] = mapFunctions.getParticipants(match['map'..mapIndex], opponents) - mapIndex = mapIndex + 1 - end + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date, { + 'tournament_enddate', + 'tournament_startdate', + })) - return match -end + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) + local games = CustomMatchGroupInput.extractMaps(match, #opponents) + match.bestof = MatchFunctions.getBestOf(match) --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - if map.map == DUMMY_MAP then - map.map = nil - end - map = mapFunctions.getScoresAndWinner(map) + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil - return map -end + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) - Opponent.resolve(opponent, teamTemplateDate, {syncPlayer=true}) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + match.stream = Streams.processStreams(match) ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if - Table.includes(NP_STATUSES, data.finished) or - Table.includes(NP_STATUSES, data.winner) - then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif CustomMatchGroupInput.placementCheckSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = DEFAULT_RESULT_TYPE - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, DEFAULT_RESULT_TYPE) - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end - end + match.games = games + match.opponents = opponents - --set it as finished if we have a winner - if not Logic.isEmpty(data.winner) then - data.finished = true - end + match.extradata = MatchFunctions.getExtraData(match) - return data, indexedScores + return match end ----@param opponents table ----@param winner integer? ----@param specialType string? ----@param finished boolean? +---@param match table +---@param opponentCount integer ---@return table[] ----@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == DEFAULT_RESULT_TYPE then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end +function CustomMatchGroupInput.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + if map.map == DUMMY_MAP then + map.map = nil end - else - local lastScore = NO_SCORE - local lastPlacement = NO_SCORE - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and (winner or '') == '' then - if finished then - winner = scoreIndex - end - end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or counter - lastPlacement = counter - lastScore = score or NO_SCORE - end - end - end - return opponents, winner -end + map.extradata = MapFunctions.getExtraData(map, opponentCount) + + map.finished = MatchGroupInputUtil.mapIsFinished(map) + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }, MapFunctions.calculateMapScore(map.winner, map.finished)) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) + end ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score) or NO_SCORE - local value2 = tonumber(tbl[key2].score) or NO_SCORE - return value1 > value2 -end + table.insert(maps, map) + match[key] = nil + end --- Check if any opponent has a none-standard status ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckSpecialStatus(tbl) - return Table.any(tbl, - function (_, scoreinfo) - return scoreinfo.status ~= STATUS_SCORE and String.isNotEmpty(scoreinfo.status) - end - ) + return maps end -- -- match related functions -- ----@param match table ----@return table -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match -end - --- Calculate the match scores based on the map results (counting map wins) --- Only update an opponents result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local newScores = {} - local foundScores = false - - local mapIndex = 1 - while match['map'..mapIndex] do - local winner = tonumber(match['map'..mapIndex].winner) - foundScores = true - if winner and winner > 0 and winner <= MAX_NUM_OPPONENTS then - newScores[winner] = (newScores[winner] or 0) + 1 - end - mapIndex = mapIndex + 1 - end - - for index = 1, MAX_NUM_OPPONENTS do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end +---@param maps table[] +---@return fun(opponentIndex: integer): integer +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - - return match -end - ----@param matchArgs table ----@return {date: string, dateexact: boolean, timestamp: integer, timezoneId: string?, timezoneOffset: string?} -function matchFunctions.readDate(matchArgs) - return MatchGroupInput.readDate(matchArgs.date, { - 'tournament_enddate', - 'tournament_startdate', - }) end ---@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - return MatchGroupInput.getCommonTournamentVars(match) +---@return integer +function MatchFunctions.getBestOf(match) + local bestOf = tonumber(Logic.emptyOr(match.bestof, Variables.varDefault('bestof'))) + Variables.varDefine('bestof', bestOf) + return bestOf or DEFAULT_BESTOF end ---@param match table ---@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - - match.links = {} - local links = match.links - if match.reddit then links.reddit = match.reddit end - - return match -end - ----@param match table ----@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mvp = MatchGroupInput.readMvp(match), - mvpteam = match.mvpteam or match.winner +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), } - return match -end - ----@param match table ----@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - opponent.score = string.upper(opponent.score or '') - if Logic.isNumeric(opponent.score) then - opponent.score = tonumber(opponent.score) - opponent.status = STATUS_SCORE - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = NOT_PLAYED_SCORE - end - - -- get players from vars for teams - if opponent.type == Opponent.team then - if not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name, { - resolveRedirect = true, - applyUnderScores = true, - maxNumPlayers = MAX_NUM_PLAYERS, - }) - end - elseif Opponent.typeIsParty(opponent.type) then - opponent.match2players = Json.parseIfString(opponent.match2players) or {{}} - opponent.match2players[1].name = opponent.match2players[1].name or opponent.name - elseif opponent.type ~= Opponent.literal then - error('Unsupported Opponent Type "' .. (opponent.type or '') .. '"') - end - - opponents[opponentIndex] = opponent - end - end - - --apply walkover input - match.walkover = string.upper(match.walkover or '') - if Logic.isNumeric(match.walkover) then - local winnerIndex = tonumber(match.walkover) - opponents = matchFunctions._makeAllOpponentsLoseByWalkover(opponents, STATUS_DEFAULT_LOSS) - opponents[winnerIndex].status = STATUS_DEFAULT_WIN - match.finished = true - elseif Logic.isNumeric(match.winner) and Table.includes(ALLOWED_STATUSES, match.walkover) then - local winnerIndex = tonumber(match.winner) - opponents = matchFunctions._makeAllOpponentsLoseByWalkover(opponents, match.walkover) - opponents[winnerIndex].status = STATUS_DEFAULT_WIN - match.finished = true - end - - -- see if match should actually be finished if bestof limit was reached - if isScoreSet and not Logic.readBool(match.finished) then - local firstTo = math.ceil(match.bestof/2) - for _, item in pairs(opponents) do - if tonumber(item.score or 0) >= firstTo then - match.finished = true - break - end - end - end - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if - not Logic.isEmpty(match.winner) or - Logic.readBool(match.finished) or - CustomMatchGroupInput.placementCheckSpecialStatus(opponents) - then - match.finished = true - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match -end - ----@param opponents table[] ----@param walkoverType string? ----@return table[] -function matchFunctions._makeAllOpponentsLoseByWalkover(opponents, walkoverType) - for index, _ in pairs(opponents) do - opponents[index].score = NOT_PLAYED_SCORE - opponents[index].status = walkoverType - end - return opponents end -- -- map related functions -- --- Parse extradata information ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getAdditionalExtraData(map) - map.extradata.comment = map.comment - map.extradata.team1side = string.lower(map.team1side or '') - map.extradata.team2side = string.lower(map.team2side or '') - - return map -end +function MapFunctions.getExtraData(map, opponentCount) + local extradata = { + comment = map.comment, + team1side = string.lower(map.team1side or ''), + team2side = string.lower(map.team2side or ''), + } --- Parse participant information ----@param map table ----@param opponents table ----@return table -function mapFunctions.getParticipants(map, opponents) - local participants = {} - local championData = {} - local getCharacterName = FnUtil.curry(MatchGroupInput.getCharacterName, ChampionNames) - for opponentIndex = 1, MAX_NUM_OPPONENTS do - for playerIndex = 1, MAX_NUM_PLAYERS do - local champ = map['t' .. opponentIndex .. 'h' .. playerIndex] - championData['team' .. opponentIndex .. 'champion' .. playerIndex] = getCharacterName(champ) - - championData['t' .. opponentIndex .. 'kda' .. playerIndex] = - map['t' .. opponentIndex .. 'kda' .. playerIndex] - - local player = map['t' .. opponentIndex .. 'p' .. playerIndex] - if String.isNotEmpty(player) then - participants = mapFunctions.attachToParticipant( - player, - opponentIndex, - opponents[opponentIndex].match2players, - participants, - championData['team' .. opponentIndex .. 'champion' .. playerIndex], - championData['team' .. opponentIndex .. 'kda' .. playerIndex] - ) - end + local getCharacterName = FnUtil.curry(MatchGroupInputUtil.getCharacterName, ChampionNames) + for opponentIndex = 1, opponentCount do + for _, ban, banIndex in Table.iter.pairsByPrefix(map, 'team' .. opponentIndex .. 'b') do + extradata['team' .. opponentIndex .. 'ban' .. banIndex] = getCharacterName(ban) end - local banIndex = 1 - local currentBan = map['t' .. opponentIndex .. 'b' .. banIndex] - while currentBan do - championData['team' .. opponentIndex .. 'ban' .. banIndex] = getCharacterName(currentBan) - banIndex = banIndex + 1 - currentBan = map['t' .. opponentIndex .. 'b' .. banIndex] + for _, pick, pickIndex in Table.iter.pairsByPrefix(map, 'team' .. opponentIndex .. 'h') do + extradata['team' .. opponentIndex .. 'champion' .. pickIndex] = getCharacterName(pick) end end - map.extradata = championData - map.participants = participants - return mapFunctions.getAdditionalExtraData(map) + return extradata end ----@param player string ----@param opponentIndex integer ----@param players table[] ----@param participants table ----@param champion string? ----@param kda string? ----@return table -function mapFunctions.attachToParticipant(player, opponentIndex, players, participants, champion, kda) - player = mw.ext.TeamLiquidIntegration.resolve_redirect(player):gsub(' ', '_') - for playerIndex, item in pairs(players or {}) do - if player == item.name then - participants[opponentIndex .. '_' .. playerIndex] = { - champion = champion, - kda = kda - } - break +---@param winnerInput string|integer|nil +---@param finished boolean +---@return fun(opponentIndex: integer): integer? +function MapFunctions.calculateMapScore(winnerInput, finished) + local winner = tonumber(winnerInput) + return function(opponentIndex) + -- TODO Better to check if map has started, rather than finished, for a more correct handling + if not winner and not finished then + return end + return winner == opponentIndex and 1 or 0 end - - return participants -end - --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - if Logic.isNumeric(score) then - obj.status = STATUS_SCORE - score = tonumber(score) - map['score' .. scoreIndex] = score - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = NOT_PLAYED_SCORE - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/rainbowsix/legacy/match_group_legacy_default.lua b/components/match2/wikis/rainbowsix/legacy/match_group_legacy_default.lua index c3e7aba97ad..30cb6263676 100644 --- a/components/match2/wikis/rainbowsix/legacy/match_group_legacy_default.lua +++ b/components/match2/wikis/rainbowsix/legacy/match_group_legacy_default.lua @@ -47,4 +47,24 @@ function MatchGroupLegacyDefault.run(frame) return MatchGroupLegacyDefault(frame):build() end -return MatchGroupLegacyDefault \ No newline at end of file +---@param isReset boolean +---@param match table +function MatchGroupLegacyDefault:handleOtherMatchParams(isReset, match) + local opp1score, opp2score = (match.opponent1 or {}).score, (match.opponent2 or {}).score + -- Legacy maps are Bo10 or Bo12, while >Bo5 in legacy matches are non existent + -- Let's assume that if the sum of the scores is less than 6, it's a match, otherwise it's a map + if (tonumber(opp1score) or 0) + (tonumber(opp2score) or 0) < 6 then + return + end + + (match.opponent1 or {}).score = nil + (match.opponent2 or {}).score = nil + match.map1 = match.map1 or { + map = 'Unknown', + finished = true, + score1 = opp1score, + score2 = opp2score, + } +end + +return MatchGroupLegacyDefault diff --git a/components/match2/wikis/rainbowsix/legacy/match_maps_legacy.lua b/components/match2/wikis/rainbowsix/legacy/match_maps_legacy.lua index d97f136f11d..ff3f14ecd58 100644 --- a/components/match2/wikis/rainbowsix/legacy/match_maps_legacy.lua +++ b/components/match2/wikis/rainbowsix/legacy/match_maps_legacy.lua @@ -16,8 +16,8 @@ local Arguments = require('Module:Arguments') local Json = require('Module:Json') local Logic = require('Module:Logic') local PageVariableNamespace = require('Module:PageVariableNamespace') +local Table = require('Module:Table') local Template = require('Module:Template') -local CustomInput = require('Module:MatchGroup/Input/Custom') local matchlistVars = PageVariableNamespace('LegacyMatchlist') @@ -26,11 +26,10 @@ local MAX_GAME_NUM = 9 local MatchMaps = {} function MatchMaps.main(frame) - local args = Arguments.getArgs(frame) - return MatchMaps._main(args, frame) + return MatchMaps._main(Arguments.getArgs(frame)) end -function MatchMaps._main(args, frame) +function MatchMaps._main(args) -- Data storage (LPDB) if Logic.readBool(matchlistVars:get('store')) then @@ -40,7 +39,7 @@ function MatchMaps._main(args, frame) local storage_args = {} local details = Json.parseIfString(args.details) or {} - storage_args['title'] = args.date + storage_args.title = args.date --opponents for i = 1, 2 do @@ -58,7 +57,7 @@ function MatchMaps._main(args, frame) storage_args['opponent' .. i] = { ['type'] = 'team', template = args['team' .. i], - score = args['games' .. i] or args['score' .. i], + score = args['games' .. i], } end end @@ -72,75 +71,64 @@ function MatchMaps._main(args, frame) for i = 1, MAX_GAME_NUM do local prefix = 'map' .. i if Logic.isNotEmpty(details[prefix]) or Logic.isNotEmpty(details[prefix ..'finished']) then - storage_args[prefix] = CustomInput.processMap{ - map = details[prefix] or 'Unknown', - finished = details[prefix..'finished'], - score1 = details[prefix..'score1'], - score2 = details[prefix..'score2'], - t1ban1 = details[prefix..'t1ban1'], - t1ban2 = details[prefix..'t1ban2'], - t2ban1 = details[prefix..'t2ban1'], - t2ban2 = details[prefix..'t2ban2'], - t1firstside = details[prefix..'t1firstside'], - t1firstsideot = details[prefix..'o1t1firstside'], - t1atk = details[prefix..'t1atk'], - t1def = details[prefix..'t1def'], - t2atk = details[prefix..'t2atk'], - t2def = details[prefix..'t2def'], - t1otatk = details[prefix..'o1t1atk'], - t1otdef = details[prefix..'o1t1def'], - t2otatk = details[prefix..'o1t2atk'], - t2otdef = details[prefix..'o1t2def'], - vod = details['vod'..i], - winner = details[prefix .. 'win'] + storage_args[prefix] = { + map = Table.extract(details, prefix) or 'Unknown', + finished = Table.extract(details, prefix..'finished'), + score1 = Table.extract(details, prefix..'score1'), + score2 = Table.extract(details, prefix..'score2'), + t1ban1 = Table.extract(details, prefix..'t1ban1'), + t1ban2 = Table.extract(details, prefix..'t1ban2'), + t2ban1 = Table.extract(details, prefix..'t2ban1'), + t2ban2 = Table.extract(details, prefix..'t2ban2'), + t1firstside = Table.extract(details, prefix..'t1firstside'), + t1firstsideot = Table.extract(details, prefix..'o1t1firstside'), + t1atk = Table.extract(details, prefix..'t1atk'), + t1def = Table.extract(details, prefix..'t1def'), + t2atk = Table.extract(details, prefix..'t2atk'), + t2def = Table.extract(details, prefix..'t2def'), + t1otatk = Table.extract(details, prefix..'o1t1atk'), + t1otdef = Table.extract(details, prefix..'o1t1def'), + t2otatk = Table.extract(details, prefix..'o1t2atk'), + t2otdef = Table.extract(details, prefix..'o1t2def'), + vod = Table.extract(details, 'vod'..i), + winner = Table.extract(details, prefix .. 'win'), } - details[prefix] = nil - details[prefix ..'win'] = nil - details[prefix ..'score'] = nil - details[prefix ..'t1ban1'] = nil - details[prefix ..'t1ban2'] = nil - details[prefix ..'t2ban1'] = nil - details[prefix ..'t2ban2'] = nil - details[prefix ..'t1firstside'] = nil - details[prefix ..'o1t1firstside'] = nil - details[prefix ..'t1atk'] = nil - details[prefix ..'t1def'] = nil - details[prefix ..'t2atk'] = nil - details[prefix ..'t2def'] = nil - details[prefix ..'o1t1atk'] = nil - details[prefix ..'o1t1def'] = nil - details[prefix ..'o1t2atk'] = nil - details[prefix ..'o1t2def'] = nil - details['vod'..i] = nil else break end end - storage_args['mapveto'] = Json.parseIfString(details.mapveto) - details.mapbans = nil + storage_args.mapveto = Json.parseIfString(Table.extract(details, 'mapveto')) - -- Add date - storage_args.date = details.date - -- If details is missing, let's assume it's finished - if #details == 0 then - storage_args.finished = true - else - storage_args.finished = details.finished - end - - details.date = nil + storage_args.date = Table.extract(details, 'date') + -- It's legacy, let's assume it's finished + storage_args.finished = true details.finished = nil for key, value in pairs(details) do storage_args[key] = value end - -- Store the processed args for later usage + local opp1score, opp2score = storage_args.opponent1.score, storage_args.opponent2.score + -- Legacy maps are Bo10 or Bo12, while >Bo5 in legacy matches are non existent + -- Let's assume that if the sum of the scores is less than 6, it's a match, otherwise it's a map + if (tonumber(opp1score) or 0) + (tonumber(opp2score) or 0) < 6 then + Template.stashReturnValue(storage_args, 'LegacyMatchlist') + return + end + + storage_args.opponent1.score = nil + storage_args.opponent2.score = nil + storage_args.map1 = storage_args.map1 or { + map = 'Unknown', + finished = true, + score1 = opp1score, + score2 = opp2score, + } + Template.stashReturnValue(storage_args, 'LegacyMatchlist') end end end - return MatchMaps diff --git a/components/match2/wikis/rainbowsix/match_group_input_custom.lua b/components/match2/wikis/rainbowsix/match_group_input_custom.lua index d5273919c20..8505d1cb8da 100644 --- a/components/match2/wikis/rainbowsix/match_group_input_custom.lua +++ b/components/match2/wikis/rainbowsix/match_group_input_custom.lua @@ -46,7 +46,7 @@ function CustomMatchGroupInput.processMatch(match, options) games = MatchFunctions.removeUnsetMaps(games) local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) - and MatchFunctions.calculateMatchScore(games, match.bestof) + and MatchFunctions.calculateMatchScore(games) or nil Array.forEach(opponents, function(opponent, opponentIndex) opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ @@ -66,7 +66,8 @@ function CustomMatchGroupInput.processMatch(match, options) MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) match.stream = Streams.processStreams(match) match.links = MatchFunctions.getLinks(match) @@ -79,8 +80,6 @@ function CustomMatchGroupInput.processMatch(match, options) return match end -CustomMatchGroupInput.processMap = FnUtil.identity - -- -- match related functions -- @@ -121,14 +120,6 @@ function MatchFunctions.extractMaps(match, opponentCount) return maps end ---- Fetch information about the tournament ----@param match table ----@return table -function MatchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) - return MatchGroupInputUtil.getCommonTournamentVars(match) -end - -- Template:Map sets a default map name so we can count the number of maps. -- These maps however shouldn't be stored -- The keepMap function will check if a map should be kept @@ -138,20 +129,10 @@ function MatchFunctions.removeUnsetMaps(games) return Array.filter(games, MapFunctions.keepMap) end --- Calculate the match scores based on the map results. --- If it's a Best of 1, we'll take the exact score of that map --- If it's not a Best of 1, we should count the map wins ---@param maps table[] ----@param bestOf integer ---@return fun(opponentIndex: integer): integer? -function MatchFunctions.calculateMatchScore(maps, bestOf) +function MatchFunctions.calculateMatchScore(maps) return function(opponentIndex) - if bestOf == 1 then - if not maps[1] or not maps[1].scores then - return - end - return maps[1].scores[opponentIndex] - end return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end end diff --git a/components/match2/wikis/rainbowsix/match_legacy.lua b/components/match2/wikis/rainbowsix/match_legacy.lua index 041ef175808..8a825fa3937 100644 --- a/components/match2/wikis/rainbowsix/match_legacy.lua +++ b/components/match2/wikis/rainbowsix/match_legacy.lua @@ -142,8 +142,14 @@ function MatchLegacy._convertParameters(match2) local opponentmatch2players = opponent.match2players or {} if opponent.type == 'team' then match[prefix] = mw.ext.TeamTemplate.teampage(opponent.template) - match[prefix..'score'] = (tonumber(opponent.score) or 0) > 0 and opponent.score or 0 - local opponentplayers = {} + if match2.bestof == 1 then + if ((match2.match2games or {})[1] or {}).scores then + match[prefix..'score'] = Json.parseIfString(match2.match2games[1].scores)[index] + end + end + if not match[prefix..'score'] then + match[prefix..'score'] = (tonumber(opponent.score) or 0) > 0 and opponent.score or 0 + end local opponentplayers = {} for i = 1,10 do local player = opponentmatch2players[i] or {} opponentplayers['p' .. i] = mw.ext.TeamLiquidIntegration.resolve_redirect(player.name or '') diff --git a/components/match2/wikis/rainbowsix/match_summary.lua b/components/match2/wikis/rainbowsix/match_summary.lua index 37c8594e6eb..bb8edbd094d 100644 --- a/components/match2/wikis/rainbowsix/match_summary.lua +++ b/components/match2/wikis/rainbowsix/match_summary.lua @@ -6,12 +6,14 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- +local Abbreviation = require('Module:Abbreviation') local CharacterIcon = require('Module:CharacterIcon') local Class = require('Module:Class') local DateExt = require('Module:Date/Ext') local Icon = require('Module:Icon') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Page = require('Module:Page') local Table = require('Module:Table') local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper') @@ -28,8 +30,6 @@ local ROUND_ICONS = { otatk = '[[File:R6S Para Bellum atk logo ot rounds.png|11px|link=]]', otdef = '[[File:R6S Para Bellum def logo ot rounds.png|11px|link=]]', } -local ARROW_LEFT = '[[File:Arrow sans left.svg|15x15px|link=|Left team starts]]' -local ARROW_RIGHT = '[[File:Arrow sans right.svg|15x15px|link=|Right team starts]]' local LINK_DATA = { siegegg = {icon = 'File:SiegeGG icon.png', text = 'SiegeGG Match Page'}, opl = {icon = 'File:OPL Icon 2023 allmode.png', text = 'OPL Match Page'}, @@ -49,6 +49,7 @@ local LINK_DATA = { stats = {icon = 'File:Match_Info_Stats.png', text = 'Match Statistics'}, ebattle = {icon = 'File:Ebattle Series allmode.png', text = 'Match page on ebattle'}, } +local TBD = Abbreviation.make('TBD', 'To Be Determined') -- Operator Bans Class ---@class R6OperatorBan @@ -253,131 +254,16 @@ function Score:create() return self.root end --- Map Veto Class ----@class R6MapVeto: MatchSummaryRowInterface ----@operator call: R6MapVeto ----@field root Html ----@field table Html -local MapVeto = Class.new( - function(self) - self.root = mw.html.create('div'):addClass('brkts-popup-mapveto') - self.table = self.root:tag('table') - :addClass('wikitable-striped'):addClass('collapsible'):addClass('collapsed') - self:createHeader() - end -) - ----@return self -function MapVeto:createHeader() - self.table:tag('tr') - :tag('th'):css('width','33%'):done() - :tag('th'):css('width','34%'):wikitext('Map Veto'):done() - :tag('th'):css('width','33%'):done() - return self -end - ----@param firstVeto number? ----@return self -function MapVeto:vetoStart(firstVeto) - local textLeft - local textCenter - local textRight - if firstVeto == 1 then - textLeft = 'Start Map Veto' - textCenter = ARROW_LEFT - elseif firstVeto == 2 then - textCenter = ARROW_RIGHT - textRight = 'Start Map Veto' - else return self end - self.table:tag('tr'):addClass('brkts-popup-mapveto-vetostart') - :tag('th'):wikitext(textLeft or ''):done() - :tag('th'):wikitext(textCenter):done() - :tag('th'):wikitext(textRight or ''):done() - return self -end +---@class R6MapVeto: VetoDisplay +---@field game string? +local MapVeto = Class.new(MatchSummary.MapVeto, function(self, game) + self.game = game +end) ---@param map string? ----@return self -function MapVeto:addDecider(map) - if Logic.isEmpty(map) then - map = 'TBD' - else - map = '[[' .. map .. '/siege|' .. map .. ']]' - end - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - self:addColumnVetoMap(row, map) - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - - self.table:node(row) - return self -end - ----@param vetotype string? ----@param map1 string? ----@param map2 string? ----@return self -function MapVeto:addRound(vetotype, map1, map2) - if Logic.isEmpty(map1) then - map1 = 'TBD' - else - map1 = '[[' .. map1 .. '/siege|' .. map1 .. ']]' - end - if Logic.isEmpty(map2) then - map2 = 'TBD' - else - map2 = '[[' .. map2 .. '/siege|' .. map2 .. ']]' - end - local class - local vetoText - if vetotype == 'ban' then - vetoText = 'BAN' - class = 'brkts-popup-mapveto-ban' - elseif vetotype == 'pick' then - vetoText = 'PICK' - class = 'brkts-popup-mapveto-pick' - elseif vetotype == 'defaultban' then - vetoText = 'DEFAULT BAN' - class = 'brkts-popup-mapveto-defaultban' - else - return self - end - - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoMap(row, map1) - self:addColumnVetoType(row, class, vetoText) - self:addColumnVetoMap(row, map2) - - self.table:node(row) - return self -end - ----@param row Html ----@param styleClass string ----@param vetoText string ----@return self -function MapVeto:addColumnVetoType(row, styleClass, vetoText) - row:tag('td') - :tag('span') - :addClass(styleClass) - :addClass('brkts-popup-mapveto-vetotype') - :wikitext(vetoText) - return self -end - ----@param row Html ----@param map string ----@return self -function MapVeto:addColumnVetoMap(row,map) - row:tag('td'):wikitext(map):done() - return self -end - ----@return Html -function MapVeto:create() - return self.root +---@return string +function MapVeto:displayMap(map) + return Page.makeInternalLink(map, map and (map .. '/siege') or nil) or TBD end local CustomMatchSummary = {} @@ -442,25 +328,7 @@ function CustomMatchSummary.createBody(match) body:addRow(MatchSummary.makeCastersRow(match.extradata.casters)) -- Add the Map Vetoes - if match.extradata.mapveto then - local vetoData = match.extradata.mapveto - if vetoData then - local mapVeto = MapVeto() - - for _,vetoRound in ipairs(vetoData) do - if vetoRound.vetostart then - mapVeto:vetoStart(tonumber(vetoRound.vetostart)) - end - if vetoRound.type == 'decider' then - mapVeto:addDecider(vetoRound.decider) - else - mapVeto:addRound(vetoRound.type, vetoRound.team1, vetoRound.team2) - end - end - - body:addRow(mapVeto) - end - end + body:addRow(MatchSummary.defaultMapVetoDisplay(match, MapVeto())) return body end diff --git a/components/match2/wikis/rocketleague/match_group_input_custom.lua b/components/match2/wikis/rocketleague/match_group_input_custom.lua index f10ff5eeb2b..0132d22f908 100644 --- a/components/match2/wikis/rocketleague/match_group_input_custom.lua +++ b/components/match2/wikis/rocketleague/match_group_input_custom.lua @@ -9,7 +9,6 @@ local CustomMatchGroupInput = {} local Array = require('Module:Array') -local FnUtil = require('Module:FnUtil') local Json = require('Module:Json') local Logic = require('Module:Logic') local Lua = require('Module:Lua') @@ -39,7 +38,7 @@ function CustomMatchGroupInput.processMatch(match, options) local finishedInput = match.finished --[[@as string?]] local winnerInput = match.winner --[[@as string?]] - Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date, {'tournament_enddate'})) local opponents = Array.mapIndexes(function(opponentIndex) return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) @@ -68,7 +67,8 @@ function CustomMatchGroupInput.processMatch(match, options) MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) match.stream = Streams.processStreams(match) match.links = MatchFunctions.getLinks(match) @@ -123,10 +123,8 @@ function CustomMatchGroupInput.extractMaps(match, opponentCount) return maps end -CustomMatchGroupInput.processMap = FnUtil.identity - ---@param opponent table ----@return table? +---@return table function CustomMatchGroupInput.getOpponentExtradata(opponent) if not Logic.isNumeric(opponent.score2) then return {} @@ -157,13 +155,6 @@ end -- match related functions -- ----@param match table ----@return table -function MatchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) - return MatchGroupInputUtil.getCommonTournamentVars(match) -end - ---@param match table ---@param opponents table[] ---@return table @@ -189,7 +180,10 @@ end ---@return boolean function MatchFunctions._checkForNonEmptyOpponent(opponent) if Opponent.typeIsParty(opponent.type) then - return not Array.all(opponent.match2players, Opponent.playerIsTbd) + local playerIsTbd = function (player) + return String.isEmpty(player.displayname) or player.displayname:upper() == 'TBD' + end + return not Array.all(opponent.match2players, playerIsTbd) end -- Literal and Teams can use the default function, player's can not because of match2player vs player list names return not Opponent.isTbd(opponent) diff --git a/components/match2/wikis/sideswipe/match_group_input_custom.lua b/components/match2/wikis/sideswipe/match_group_input_custom.lua index c1baa7d7f2b..1ea8782f8ea 100644 --- a/components/match2/wikis/sideswipe/match_group_input_custom.lua +++ b/components/match2/wikis/sideswipe/match_group_input_custom.lua @@ -8,275 +8,121 @@ local CustomMatchGroupInput = {} -local DateExt = require('Module:Date/Ext') +local Array = require('Module:Array') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') local Table = require('Module:Table') -local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') -local Streams = require('Module:Links/Stream') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local STATUS_HAS_SCORE = 'S' -local STATUS_DEFAULT_WIN = 'W' -local ALLOWED_STATUSES = { STATUS_DEFAULT_WIN, 'FF', 'DQ', 'L' } -local STATUS_TO_WALKOVER = { FF = 'ff', DQ = 'dq', L = 'l' } -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_PLAYERS = 10 -local RESULT_TYPE_DRAW = 'draw' -local EARNINGS_LIMIT_FOR_FEATURED = 10000 -local CURRENT_YEAR = os.date('%Y') -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_MODE = '2v2' -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} -- called from Module:MatchGroup ---@param match table ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - Table.mergeInto( - match, - matchFunctions.readDate(match) - ) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) - - return match -end + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) - map = mapFunctions.getParticipantsData(map) + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date, {'tournament_enddate'})) - return map -end + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) + local games = CustomMatchGroupInput.extractMaps(match, #opponents) --- called from Module:Match/Subobjects ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }) + end) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) --- --- --- function to sort out winner/placements ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function matchFunctions._placementSortFunction(tbl, key1, key2) - local op1 = tbl[key1] - local op2 = tbl[key2] - local op1norm = op1.status == STATUS_HAS_SCORE - local op2norm = op2.status == STATUS_HAS_SCORE - if op1norm then - if op2norm then - return tonumber(op1.score) > tonumber(op2.score) - else return true end - else - if op2norm then return false - elseif op1.status == STATUS_DEFAULT_WIN then return true - elseif Table.includes(ALLOWED_STATUSES, op1.status) then return false - elseif op2.status == STATUS_DEFAULT_WIN then return false - elseif Table.includes(ALLOWED_STATUSES, op2.status) then return true - else return true end - end -end + match.stream = Streams.processStreams(match) --- --- match related functions --- ----@param matchArgs table ----@return {date: string, dateexact: boolean, timestamp: integer, timezoneId: string?, timezoneOffset: string?} -function matchFunctions.readDate(matchArgs) - return MatchGroupInput.readDate(matchArgs.date, {'tournament_enddate'}) -end + match.games = games + match.opponents = opponents ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', '2v2')) - return MatchGroupInput.getCommonTournamentVars(match) -end - ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) + match.extradata = MatchFunctions.getExtraData(match, opponents) return match end ---@param match table +---@param opponentCount integer ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - isfeatured = matchFunctions.isFeatured(match) - } - return match -end - ----@param match table ----@return boolean -function matchFunctions.isFeatured(match) - local opponent1 = match.opponent1 - local opponent2 = match.opponent2 - if opponent1.type ~= 'team' or opponent2.type ~= 'team' then - return false - end +function CustomMatchGroupInput.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) + end - if - tonumber(match.liquipediatier or '') == 1 - or tonumber(match.liquipediatier or '') == 2 - or not String.isEmpty(Variables.varDefault('match_featured_override')) - then - return true + table.insert(maps, map) + match[key] = nil end - if matchFunctions.currentEarnings(opponent1.name) >= EARNINGS_LIMIT_FOR_FEATURED or - matchFunctions.currentEarnings(opponent2.name) >= EARNINGS_LIMIT_FOR_FEATURED then - return true - end - return false + return maps end ----@param name string? ----@return integer -function matchFunctions.currentEarnings(name) - if String.isEmpty(name) then - return 0 - end - local data = mw.ext.LiquipediaDB.lpdb('team', { - conditions = '[[name::' .. name .. ']]', - query = 'extradata' - }) - - if type(data[1]) == 'table' then - local currentEarnings = (data[1].extradata or {})['earningsin' .. CURRENT_YEAR] - return tonumber(currentEarnings or 0) or 0 - end - - return 0 -end +-- +-- match related functions +-- ----@param args table +---@param match table +---@param opponents table[] ---@return table -function matchFunctions.getOpponents(args) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = args['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, args.timestamp) - - -- apply status - if TypeUtil.isNumeric(opponent.score) then - opponent.status = STATUS_HAS_SCORE - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - - --set Walkover from Opponent status - args.walkover = args.walkover or STATUS_TO_WALKOVER[opponent.status] - - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == 'team' and not Logic.isEmpty(opponent.name) then - args = MatchGroupInput.readPlayersOfTeam(args, opponentIndex, opponent.name) - end - end - end - - --set resulttype to 'default' if walkover is set - if args.walkover then - args.resulttype = 'default' - end - - local autofinished = String.isNotEmpty(args.autofinished) and args.autofinished or true - -- see if match should actually be finished if score is set - if isScoreSet and Logic.readBool(autofinished) and not Logic.readBool(args.finished) then - local threshold = args.dateexact and 30800 or 86400 - if args.timestamp + threshold < NOW then - args.finished = true - end - end +function MatchFunctions.getExtraData(match, opponents) + return { + isfeatured = MatchFunctions.isFeatured(tonumber(match.liquipediatier)), + } +end - -- apply placements and winner if finshed - local winner = tostring(args.winner or '') - if Logic.readBool(args.finished) then - local placement = 1 - local lastScore - local lastPlacement = 1 - local lastStatus - -- luacheck: push ignore - for opponentIndex, opponent in Table.iter.spairs(opponents, matchFunctions._placementSortFunction) do - if opponent.status ~= STATUS_HAS_SCORE and opponent.status ~= STATUS_DEFAULT_WIN and placement == 1 then - placement = 2 - end - if placement == 1 then - args.winner = opponentIndex - end - if opponent.status == STATUS_HAS_SCORE and opponent.score == lastScore then - opponent.placement = lastPlacement - elseif opponent.status ~= STATUS_HAS_SCORE and opponent.status == lastStatus then - opponent.placement = lastPlacement - else - opponent.placement = placement - end - args['opponent' .. opponentIndex] = opponent - placement = placement + 1 - lastScore = opponent.score - lastPlacement = opponent.placement - lastStatus = opponent.status - end - -- luacheck: pop - -- only apply arg changes otherwise - else - for opponentIndex, opponent in pairs(opponents) do - args['opponent' .. opponentIndex] = opponent - end - end - if winner == RESULT_TYPE_DRAW - then args.resulttype = RESULT_TYPE_DRAW - end - return args +---@param tier integer? +---@return boolean +function MatchFunctions.isFeatured(tier) + return tier == 1 or tier == 2 end -- @@ -285,124 +131,12 @@ end ---@param map table ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { +function MapFunctions.getExtraData(map) + return { ot = map.ot, otlength = map.otlength, comment = map.comment, - header = map.header, - t1goals = map.t1goals, - t2goals = map.t2goals, } - return map -end - ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] - local obj = {} - if not Logic.isEmpty(score) then - if TypeUtil.isNumeric(score) then - score = tonumber(score) - obj.status = STATUS_HAS_SCORE - obj.score = score - obj.index = scoreIndex - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - obj.index = scoreIndex - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - local isFinished = map.finished - if not Logic.isEmpty(isFinished) then - isFinished = Logic.readBool(isFinished) - else - isFinished = not Logic.readBool(map.unfinished) - end - if isFinished and not Logic.isEmpty(indexedScores) then - map.winner = mapFunctions.getWinner(indexedScores) - end - - return map -end - ----@param indexedScores table[] ----@return integer? -function mapFunctions.getWinner(indexedScores) - table.sort(indexedScores, mapFunctions.mapWinnerSortFunction) - return indexedScores[1].index -end - ----@param op1 table ----@param op2 table ----@return boolean -function mapFunctions.mapWinnerSortFunction(op1, op2) - local op1norm = op1.status == STATUS_HAS_SCORE - local op2norm = op2.status == STATUS_HAS_SCORE - if op1norm then - if op2norm then - return tonumber(op1.score) > tonumber(op2.score) - else return true end - else - if op2norm then return false - elseif op1.status == STATUS_DEFAULT_WIN then return true - elseif Table.includes(ALLOWED_STATUSES, op1.status) then return false - elseif op2.status == STATUS_DEFAULT_WIN then return false - elseif Table.includes(ALLOWED_STATUSES, op2.status) then return true - else return true end - end -end - ----@param map table ----@return table -function mapFunctions.getParticipantsData(map) - local participants = map.participants or {} - - -- fill in goals from goal progression - local scorers = {} - for goalIndex = 1, 1000 do - local scorer = map['goal' .. goalIndex .. 'player'] - if Logic.isEmpty(scorer) then - break - elseif scorer:match('op%d_p%d') then - scorer = scorer:gsub('op', ''):gsub('p', '') - scorers[scorer] = (scorers[scorer] or 0) + 1 - end - end - for scorer, goals in pairs(scorers) do - participants[scorer] = { - goals = goals - } - end - - -- fill in goals and cars - -- goals are overwritten if set here - for opponentIndex = 1, MAX_NUM_OPPONENTS do - for player = 1, MAX_NUM_PLAYERS do - local participant = participants[opponentIndex .. '_' .. player] or {} - local opstring = 'opponent' .. opponentIndex .. '_p' .. player - local goals = map[opstring .. 'goals'] - local car = map[opstring .. 'car'] - participant.goals = Logic.isEmpty(goals) and participant.goals or goals - participant.car = Logic.isEmpty(car) and participant.car or car - if not Table.isEmpty(participant) then - participants[opponentIndex .. '_' .. player] = participant - end - end - end - - map.participants = participants - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/smite/match_group_input_custom.lua b/components/match2/wikis/smite/match_group_input_custom.lua index 4bbfa2eaf3e..1f2e7a3ea8a 100644 --- a/components/match2/wikis/smite/match_group_input_custom.lua +++ b/components/match2/wikis/smite/match_group_input_custom.lua @@ -7,416 +7,223 @@ -- local Array = require('Module:Array') -local DateExt = require('Module:Date/Ext') local FnUtil = require('Module:FnUtil') local GodNames = mw.loadData('Module:GodNames') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') local Streams = require('Module:Links/Stream') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') - -local STATUS_SCORE = 'S' -local STATUS_DRAW = 'D' -local STATUS_DEFAULT_WIN = 'W' -local STATUS_FORFEIT = 'FF' -local STATUS_DISQUALIFIED = 'DQ' -local STATUS_DEFAULT_LOSS = 'L' -local ALLOWED_STATUSES = { - STATUS_DRAW, - STATUS_DEFAULT_WIN, - STATUS_FORFEIT, - STATUS_DISQUALIFIED, - STATUS_DEFAULT_LOSS, -} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_PLAYERS = 15 -local MAX_NUM_GAMES = 7 +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') + +local DEFAULT_BESTOF = 3 local DEFAULT_MODE = 'team' -local NO_SCORE = -99 -local NP_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local DEFAULT_RESULT_TYPE = 'default' -local NOT_PLAYED_SCORE = -1 -local SECONDS_UNTIL_FINISHED_EXACT = 30800 -local SECONDS_UNTIL_FINISHED_NOT_EXACT = 86400 local DUMMY_MAP = 'default' - -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local MAX_NUM_PLAYERS = 15 +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, + maxNumPlayers = MAX_NUM_PLAYERS, +} -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} -- called from Module:MatchGroup ---@param match table +---@param options table? ---@return table -function CustomMatchGroupInput.processMatch(match) - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) - - return match -end - --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - if map.map == DUMMY_MAP then - map.map = nil - end - map = mapFunctions.getScoresAndWinner(map) - map = mapFunctions.getPicksAndBans(map) - map = mapFunctions.getAdditionalExtraData(map) +function CustomMatchGroupInput.processMatch(match, options) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - return map -end + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) ----@param record table ----@param timestamp number -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + local games = MatchFunctions.extractMaps(match, #opponents) + match.bestof = MatchFunctions.getBestOf(match.bestof) - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if - Table.includes(NP_STATUSES, data.finished) or - Table.includes(NP_STATUSES, data.winner) - then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, STATUS_DRAW) - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.hasDefaultWinner(indexedScores) - data.resulttype = DEFAULT_RESULT_TYPE - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, DEFAULT_RESULT_TYPE) - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - -- set it as finished if we have a winner - if Logic.isNotEmpty(data.winner) then - data.finished = true - end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) - return data, indexedScores -end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) ----@param opponents table[] ----@param winner nil ----@param specialType nil ----@param finished nil ----@return table[], nil -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == STATUS_DRAW then - for key in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == DEFAULT_RESULT_TYPE then - for key in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local lastScore = NO_SCORE - local lastPlacement = NO_SCORE - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and String.isEmpty(winner) then - if finished then - winner = scoreIndex - end - end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or counter - lastPlacement = counter - lastScore = score or NO_SCORE - end - end - end + match.games = games + match.opponents = opponents - return opponents, winner -end + match.extradata = MatchFunctions.getExtraData(match) ---- @param tbl table ---- @param key1 string ---- @param key2 string ---- @return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score or NO_SCORE) or NO_SCORE - local value2 = tonumber(tbl[key2].score or NO_SCORE) or NO_SCORE - return value1 > value2 + return match end -- -- match related functions -- ----@param match table ----@return table -function matchFunctions.getBestOf(match) - match.bestof = #Array.filter(Array.range(1, MAX_NUM_GAMES), function(idx) return match['map'.. idx] end) - return match -end --- Calculate the match scores based on the map results (counting map wins) --- Only update an opponents result if it's --- 1) Not manually added --- 2) At least one map has a winner ---@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local newScores = {} - local setScores = false - - -- If the match has started, we want to use the automatic calculations - if match.dateexact then - if match.timestamp <= NOW then - setScores = true - end - end +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} - for _, map in Table.iter.pairsByPrefix(match, 'map') do - local winner = tonumber(map.winner) - if winner and winner > 0 and winner <= MAX_NUM_OPPONENTS then - setScores = true - newScores[winner] = (newScores[winner] or 0) + 1 - end + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + table.insert(maps, MapFunctions.readMap(map, opponentCount)) + match[key] = nil end - for index = 1, MAX_NUM_OPPONENTS do - if not match['opponent' .. index].score and setScores then - match['opponent' .. index].score = newScores[index] or 0 - end + return maps +end + +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) + + if bestof then + Variables.varDefine('bestof', bestof) + return bestof end - return match + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF end ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - return MatchGroupInput.getCommonTournamentVars(match) +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + end end ---@param match table ---@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - - match.links = { +function MatchFunctions.getLinks(match) + return { stats = match.stats, - smiteesports = match.smiteesports - and ('https://www.smiteesports.com/matches/' .. match.smiteesports) or nil, + smiteesports = match.smiteesports and ('https://www.smiteesports.com/matches/' .. match.smiteesports) or nil, } - return match end ---@param match table ---@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - opponent.score = string.upper(opponent.score or '') - if Logic.isNumeric(opponent.score) then - opponent.score = tonumber(opponent.score) - opponent.status = STATUS_SCORE - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = NOT_PLAYED_SCORE - end - - -- get players from vars for teams - if opponent.type == Opponent.team then - if not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name, { - resolveRedirect = true, - applyUnderScores = true, - maxNumPlayers = MAX_NUM_PLAYERS, - }) - end - elseif opponent.type ~= Opponent.solo and opponent.type ~= Opponent.literal then - error('Unsupported Opponent Type "' .. (opponent.type or '') .. '"') - end - - opponents[opponentIndex] = opponent - end - end +function MatchFunctions.getExtraData(match) + return { + casters = MatchGroupInputUtil.readCasters(match, {noSort = true}), + } +end - --apply walkover input - match.walkover = string.upper(match.walkover or '') - if Logic.isNumeric(match.walkover) then - local winnerIndex = tonumber(match.walkover) - opponents = matchFunctions._makeAllOpponentsLoseByWalkover(opponents, STATUS_DEFAULT_LOSS) - opponents[winnerIndex].status = STATUS_DEFAULT_WIN - match.finished = true - elseif Logic.isNumeric(match.winner) and Table.includes(ALLOWED_STATUSES, match.walkover) then - local winnerIndex = tonumber(match.winner) - opponents = matchFunctions._makeAllOpponentsLoseByWalkover(opponents, match.walkover) - opponents[winnerIndex].status = STATUS_DEFAULT_WIN - match.finished = true - end +-- +-- map related functions +-- - -- see if match should actually be finished if score is set - if not Logic.readBool(match.finished) then - matchFunctions._finishMatch(match, opponents, isScoreSet) - end +---@param map table +---@param opponentCount integer +---@return table? +function MapFunctions.readMap(map, opponentCount) + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] - -- apply placements and winner if finshed - if Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) + if not MapFunctions.keepMap(map) then + map.map = nil end - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent + if Logic.isDeepEmpty(map) then + return nil end - return match -end ----@param match table ----@param opponents table ----@param isScoreSet boolean ----@return table -function matchFunctions._finishMatch(match, opponents, isScoreSet) - -- If a winner has been set - if Logic.isNotEmpty(match.winner) then - match.finished = true - end + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) - -- If special status has been applied to a team - if MatchGroupInput.hasSpecialStatus(opponents) then - match.finished = true - end + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }, MapFunctions.calculateMapScore(map.winner, map.finished)) + return {score = score, status = status} + end) - -- see if match should actually be finished if bestof limit was reached - match.finished = Logic.readBool(match.finished) - or isScoreSet and ( - Array.any(opponents, function(opponent) return (tonumber(opponent.score) or 0) > match.bestof/2 end) - or Array.all(opponents, function(opponent) return (tonumber(opponent.score) or 0) == match.bestof/2 end) - ) - - -- If enough time has passed since match started, it should be marked as finished - if isScoreSet and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and SECONDS_UNTIL_FINISHED_EXACT - or SECONDS_UNTIL_FINISHED_NOT_EXACT - if match.timestamp + threshold < NOW then - match.finished = true - end + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - return match + return map end ----@param opponents table ----@param walkoverType string ----@return table -function matchFunctions._makeAllOpponentsLoseByWalkover(opponents, walkoverType) - for index in pairs(opponents) do - opponents[index].score = NOT_PLAYED_SCORE - opponents[index].status = walkoverType +-- Check if a map should be discarded due to being redundant +-- DUMMY_MAP_NAME needs the match the default value in Template:Map +---@param map table +---@return boolean +function MapFunctions.keepMap(map) + return map.map ~= DUMMY_MAP +end + +---@param winnerInput string|integer|nil +---@param finished boolean +---@return fun(opponentIndex: integer): integer? +function MapFunctions.calculateMapScore(winnerInput, finished) + local winner = tonumber(winnerInput) + return function(opponentIndex) + -- TODO Better to check if map has started, rather than finished, for a more correct handling + if not winner and not finished then + return + end + return winner == opponentIndex and 1 or 0 end - return opponents -end - ----@param match table ----@return table -function matchFunctions.getExtraData(match) - match.extradata = { - casters = MatchGroupInput.readCasters(match, {noSort = true}), - } - return match end --- --- map related functions --- - --- Parse extradata information ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getAdditionalExtraData(map) - map.extradata.comment = map.comment - map.extradata.team1side = string.lower(map.team1side or '') - map.extradata.team2side = string.lower(map.team2side or '') - - return map +function MapFunctions.getExtraData(map, opponentCount) + return Table.merge({ + comment = map.comment, + team1side = string.lower(map.team1side or ''), + team2side = string.lower(map.team2side or ''), + }, MapFunctions.getPicksAndBans(map, opponentCount)) end ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getPicksAndBans(map) +function MapFunctions.getPicksAndBans(map, opponentCount) local godData = {} - local getCharacterName = FnUtil.curry(MatchGroupInput.getCharacterName, GodNames) - for opponentIndex = 1, MAX_NUM_OPPONENTS do + local getCharacterName = FnUtil.curry(MatchGroupInputUtil.getCharacterName, GodNames) + for opponentIndex = 1, opponentCount do for playerIndex = 1, MAX_NUM_PLAYERS do local god = map['t' .. opponentIndex .. 'g' .. playerIndex] godData['team' .. opponentIndex .. 'god' .. playerIndex] = getCharacterName(god) @@ -425,20 +232,8 @@ function mapFunctions.getPicksAndBans(map) godData['team' .. opponentIndex .. 'ban' .. playerIndex] = getCharacterName(ban) end end - map.extradata = godData - return map -end --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - if Logic.isNumeric(map.winner) then - map.winner = tonumber(map.winner) - map.finished = true - end - - return map + return godData end return CustomMatchGroupInput diff --git a/components/match2/wikis/splatoon/match_group_input_custom.lua b/components/match2/wikis/splatoon/match_group_input_custom.lua index b56336d0138..2dee94f7bcf 100644 --- a/components/match2/wikis/splatoon/match_group_input_custom.lua +++ b/components/match2/wikis/splatoon/match_group_input_custom.lua @@ -6,45 +6,23 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- -local DateExt = require('Module:Date/Ext') +local Array = require('Module:Array') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Operator = require('Module:Operator') local Table = require('Module:Table') local Variables = require('Module:Variables') local Streams = require('Module:Links/Stream') local WeaponNames = mw.loadData('Module:WeaponNames') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') - -local STATUS_SCORE = 'S' -local STATUS_DRAW = 'D' -local STATUS_DEFAULT_WIN = 'W' -local STATUS_FORFEIT = 'FF' -local STATUS_DISQUALIFIED = 'DQ' -local STATUS_DEFAULT_LOSS = 'L' -local ALLOWED_STATUSES = { - STATUS_DRAW, - STATUS_DEFAULT_WIN, - STATUS_FORFEIT, - STATUS_DISQUALIFIED, - STATUS_DEFAULT_LOSS, -} -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_PLAYERS = 15 -local MAX_NUM_GAMES = 7 +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') + local DEFAULT_MODE = 'team' -local NO_SCORE = -99 local DUMMY_MAP = 'default' -local NP_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local DEFAULT_RESULT_TYPE = 'default' -local NOT_PLAYED_SCORE = -1 - -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} @@ -53,387 +31,115 @@ local CustomMatchGroupInput = {} ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getLinks(match) - match = matchFunctions.getExtraData(match) - - -- Adjust map data, especially set participants data - match = matchFunctions.adjustMapData(match) - - return match -end - ----@param match table ----@return table -function matchFunctions.adjustMapData(match) - local opponents = {} - for opponentIndex = 1, MAX_NUM_OPPONENTS do - opponents[opponentIndex] = match['opponent' .. opponentIndex] - end - local mapIndex = 1 - while match['map'..mapIndex] do - match['map'..mapIndex] = mapFunctions.getParticipants(match['map'..mapIndex], opponents) - mapIndex = mapIndex + 1 + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) + local games = MatchFunctions.extractMaps(match, opponents) + match.bestof = MatchGroupInputUtil.getBestOf(nil, games) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - return match -end - --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - if map.map == DUMMY_MAP then - map.map = nil - end - map = mapFunctions.getScoresAndWinner(map) - - return map -end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + match.stream = Streams.processStreams(match) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + match.games = games + match.opponents = opponents - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end + match.extradata = MatchFunctions.getExtraData(match) - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) + return match end ----@param data table ----@param indexedScores table[] ----@return table +---@param match table +---@param opponents table[] ---@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if - Table.includes(NP_STATUSES, data.finished) or - Table.includes(NP_STATUSES, data.winner) - then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = DEFAULT_RESULT_TYPE - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, DEFAULT_RESULT_TYPE) - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner +function MatchFunctions.extractMaps(match, opponents) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + if map.map == DUMMY_MAP then + map.map = nil end - end - - -- set it as finished if we have a winner - if not Logic.isEmpty(data.winner) then - data.finished = true - end - return data, indexedScores -end + map.participants = MapFunctions.getParticipants(map, opponents) + map.extradata = MapFunctions.getExtraData(map) + map.finished = MatchGroupInputUtil.mapIsFinished(map) ----@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ----@return table[] ----@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == DEFAULT_RESULT_TYPE then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local lastScore = NO_SCORE - local lastPlacement = NO_SCORE - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and Logic.isEmpty(winner) then - if finished then - winner = scoreIndex - end - end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or counter - lastPlacement = counter - lastScore = score or NO_SCORE + local opponentInfo = Array.map(opponents, function(_, opponentIndex) + local scoreInput = map['score' .. opponentIndex] + if map.maptype == 'Turf War' and scoreInput then + scoreInput = scoreInput:gsub('%%', '') end + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = scoreInput, + }, MapFunctions.calculateMapScore(map.winner, map.finished)) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - return opponents, winner -end + table.insert(maps, map) + match[key] = nil + end ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score) or NO_SCORE - local value2 = tonumber(tbl[key2].score) or NO_SCORE - return value1 > value2 + return maps end -- -- match related functions -- ----@param match table ----@return table -function matchFunctions.getBestOf(match) - if Logic.isNumeric(match.bestof) then - match.bestof = tonumber(match.bestof) - else - local mapCount = 0 - for i = 1, MAX_NUM_GAMES do - if match['map'..i] then - mapCount = mapCount + 1 - else - break - end - end - match.bestof = mapCount - end - - return match -end - --- Calculate the match scores based on the map results (counting map wins) --- Only update an opponents result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local newScores = {} - local setScores = false - - -- If the match has started, we want to use the automatic calculations - if match.dateexact and match.timestamp <= NOW then - setScores = true +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - - local mapIndex = 1 - while match['map'..mapIndex] do - local winner = tonumber(match['map'..mapIndex].winner) - if winner and winner > 0 and winner <= MAX_NUM_OPPONENTS then - setScores = true - newScores[winner] = (newScores[winner] or 0) + 1 - end - mapIndex = mapIndex + 1 - end - - for index = 1, MAX_NUM_OPPONENTS do - if not match['opponent' .. index].score and setScores then - match['opponent' .. index].score = newScores[index] or 0 - end - end - - return match -end - ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) - match.game = Logic.emptyOr(match.game, Variables.varDefault('tournament_game')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - return MatchGroupInput.getCommonTournamentVars(match) -end - ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - return match end ---@param match table ---@return table -function matchFunctions.getLinks(match) - return match -end - ----@param match table ----@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mapveto = MatchGroupInput.getMapVeto(match), - mvp = MatchGroupInput.readMvp(match), +function MatchFunctions.getExtraData(match) + return { + mapveto = MatchGroupInputUtil.getMapVeto(match), + mvp = MatchGroupInputUtil.readMvp(match), } - - return match -end - ----@param match table ----@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - opponent.score = string.upper(opponent.score or '') - if Logic.isNumeric(opponent.score) then - opponent.score = tonumber(opponent.score) - opponent.status = STATUS_SCORE - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = NOT_PLAYED_SCORE - end - - -- get players from vars for teams - if opponent.type == Opponent.team then - if not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name, { - resolveRedirect = true, - applyUnderScores = true, - maxNumPlayers = MAX_NUM_PLAYERS, - }) - end - elseif opponent.type ~= Opponent.solo and opponent.type ~= Opponent.literal then - error('Unsupported Opponent Type "' .. (opponent.type or '') .. '"') - end - - opponents[opponentIndex] = opponent - end - end - - --apply walkover input - match.walkover = string.upper(match.walkover or '') - if Logic.isNumeric(match.walkover) then - local winnerIndex = tonumber(match.walkover) - opponents = matchFunctions._makeAllOpponentsLoseByWalkover(opponents, STATUS_DEFAULT_LOSS) - opponents[winnerIndex].status = STATUS_DEFAULT_WIN - match.finished = true - elseif Logic.isNumeric(match.winner) and Table.includes(ALLOWED_STATUSES, match.walkover) then - local winnerIndex = tonumber(match.winner) - opponents = matchFunctions._makeAllOpponentsLoseByWalkover(opponents, match.walkover) - opponents[winnerIndex].status = STATUS_DEFAULT_WIN - match.finished = true - end - - -- see if match should actually be finished if score is set - if not Logic.readBool(match.finished) then - matchFunctions._finishMatch(match, opponents, isScoreSet) - end - - -- apply placements and winner if finshed - if Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match -end - ----@param match table ----@param opponents table[] ----@param isScoreSet boolean ----@return table -function matchFunctions._finishMatch(match, opponents, isScoreSet) - -- If a winner has been set - if Logic.isNotEmpty(match.winner) then - match.finished = true - end - - -- If special status has been applied to a team - if MatchGroupInput.hasSpecialStatus(opponents) then - match.finished = true - end - - -- Check if all/enough games have been played. If they have, mark as finished - if isScoreSet then - local firstTo = math.floor(match.bestof / 2) - local scoreSum = 0 - for _, item in pairs(opponents) do - local score = tonumber(item.score or 0) - if score > firstTo then - match.finished = true - break - end - scoreSum = scoreSum + score - end - if scoreSum >= match.bestof then - match.finished = true - end - end - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - return match -end - ----@param opponents table[] ----@param walkoverType string? ----@return table[] -function matchFunctions._makeAllOpponentsLoseByWalkover(opponents, walkoverType) - for index, _ in pairs(opponents) do - opponents[index].score = NOT_PLAYED_SCORE - opponents[index].status = walkoverType - end - return opponents end -- @@ -443,85 +149,70 @@ end -- Parse extradata information ---@param map table ---@return table -function mapFunctions.getAdditionalExtraData(map) - map.extradata = Table.merge(map.extradata, { +function MapFunctions.getExtraData(map) + return { comment = map.comment, header = map.header, maptype = map.maptype, - }) - - return map + } end -- Parse participant information + ---@param map table ---@param opponents table[] ---@return table -function mapFunctions.getParticipants(map, opponents) - local participants = {} - local maximumPickIndex = 0 - for opponentIndex = 1, MAX_NUM_OPPONENTS do - for _, player, playerIndex in Table.iter.pairsByPrefix(map, 't' .. opponentIndex .. 'p') do - participants[opponentIndex .. '_' .. playerIndex] = {player = player} - end - - for _, weapon, pickIndex in Table.iter.pairsByPrefix(map, 't' .. opponentIndex .. 'w') do - local participantsKey = opponentIndex .. '_' .. pickIndex - participants[participantsKey] = Table.merge(participants[participantsKey] - or {}, {weapon = mapFunctions._cleanWeaponName(weapon)}) - if maximumPickIndex < pickIndex then - maximumPickIndex = pickIndex +function MapFunctions.getParticipants(map, opponents) + local allParticipants = {} + Array.forEach(opponents, function(opponent, opponentIndex) + local players = Array.mapIndexes(function(playerIndex) + return opponent.match2players[playerIndex] or Logic.nilIfEmpty(map['t' .. opponentIndex .. 'w' .. playerIndex]) + end) + local participants, unattachedParticipants = MatchGroupInputUtil.parseParticipants( + opponent.match2players, + players, + function(playerIndex) + local player = map['t' .. opponentIndex .. 'p' .. playerIndex] + return player and {name = player} or nil + end, + function(playerIndex, playerIdData) + local weapon = map['t' .. opponentIndex .. 'w' .. playerIndex] + return { + player = playerIdData.name, + weapon = MapFunctions._cleanWeaponName(weapon), + } end - end - end + ) + Array.forEach(unattachedParticipants, function(participant) + table.insert(participants, participant) + end) + Table.mergeInto(allParticipants, Table.map(participants, MatchGroupInputUtil.prefixPartcipants(opponentIndex))) + end) - map.extradata = {maximumpickindex = maximumPickIndex} - map.participants = participants - - return mapFunctions.getAdditionalExtraData(map) + return allParticipants end ---@param weaponRaw string ----@return string -function mapFunctions._cleanWeaponName(weaponRaw) - local weapon = WeaponNames[string.lower(weaponRaw)] - if not weapon then - error('Unsupported weapon input: ' .. weaponRaw) - end - - return weapon -end - --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - if Logic.isNumeric(score) then - obj.status = STATUS_SCORE - score = tonumber(score) - map['score' .. scoreIndex] = score - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = NOT_PLAYED_SCORE - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break +---@return string? +function MapFunctions._cleanWeaponName(weaponRaw) + if not weaponRaw then + return nil + end + return assert(WeaponNames[string.lower(weaponRaw)], 'Unsupported weapon input: ' .. weaponRaw) +end + +---@param winnerInput string|integer|nil +---@param finished boolean +---@return fun(opponentIndex: integer): integer? +function MapFunctions.calculateMapScore(winnerInput, finished) + local winner = tonumber(winnerInput) + return function(opponentIndex) + -- TODO Better to check if map has started, rather than finished, for a more correct handling + if not winner and not finished then + return end + return winner == opponentIndex and 1 or 0 end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/splatoon/match_summary.lua b/components/match2/wikis/splatoon/match_summary.lua index f5d96298290..0d9694d1ee4 100644 --- a/components/match2/wikis/splatoon/match_summary.lua +++ b/components/match2/wikis/splatoon/match_summary.lua @@ -10,11 +10,11 @@ local CustomMatchSummary = {} local Array = require('Module:Array') local DateExt = require('Module:Date/Ext') -local Class = require('Module:Class') local Icon = require('Module:Icon') local Logic = require('Module:Logic') local Lua = require('Module:Lua') local MapTypeIcon = require('Module:MapType') +local Operator = require('Module:Operator') local String = require('Module:StringUtils') local Table = require('Module:Table') local WeaponIcon = require('Module:WeaponIcon') @@ -22,147 +22,14 @@ local WeaponIcon = require('Module:WeaponIcon') local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper') local MatchSummary = Lua.import('Module:MatchSummary/Base') -local NUM_OPPONENTS = 2 local GREEN_CHECK = Icon.makeIcon{iconName = 'winner', color = 'forest-green-text', size = '110%'} local NO_CHECK = '[[File:NoCheck.png|link=]]' -local MAP_VETO_START = 'Start Map Veto' -local ARROW_LEFT = '[[File:Arrow sans left.svg|15x15px|link=|Left team starts]]' -local ARROW_RIGHT = '[[File:Arrow sans right.svg|15x15px|link=|Right team starts]]' -local TBD = 'TBD' -- Normal links, from input/lpdb local LINK_DATA = { vod = {icon = 'File:VOD Icon.png', text = 'Watch VOD'}, } local NON_BREAKING_SPACE = ' ' --- Map Veto Class ----@class SplatoonMapVeto: MatchSummaryRowInterface ----@operator call: SplatoonMapVeto ----@field root Html ----@field table Html -local MapVeto = Class.new( - function(self) - self.root = mw.html.create('div'):addClass('brkts-popup-mapveto') - self.table = self.root:tag('table') - :addClass('wikitable-striped'):addClass('collapsible'):addClass('collapsed') - self:createHeader() - end -) - ----@return self -function MapVeto:createHeader() - self.table:tag('tr') - :tag('th'):css('width','33%'):done() - :tag('th'):css('width','34%'):wikitext('Map Veto'):done() - :tag('th'):css('width','33%'):done() - return self -end - ----@param firstVeto number? ----@return self -function MapVeto:vetoStart(firstVeto) - local textLeft - local textCenter - local textRight - if firstVeto == 1 then - textLeft = MAP_VETO_START - textCenter = ARROW_LEFT - elseif firstVeto == 2 then - textCenter = ARROW_RIGHT - textRight = MAP_VETO_START - else return self end - self.table:tag('tr'):addClass('brkts-popup-mapveto-vetostart') - :tag('th'):wikitext(textLeft or ''):done() - :tag('th'):wikitext(textCenter):done() - :tag('th'):wikitext(textRight or ''):done() - return self -end - ----@param map string? ----@return string -function MapVeto._displayMap(map) - if Logic.isEmpty(map) then - map = TBD - else - map = '[[' .. map .. ']]' - end - - return map -end - ----@param map string? ----@return self -function MapVeto:addDecider(map) - map = MapVeto._displayMap(map) - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - self:addColumnVetoMap(row, map) - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - - self.table:node(row) - return self -end - ----@param vetotype string? ----@param map1 string? ----@param map2 string? ----@return self -function MapVeto:addRound(vetotype, map1, map2) - map1 = MapVeto._displayMap(map1) - map2 = MapVeto._displayMap(map2) - - local class - local vetoText - if vetotype == 'ban' then - vetoText = 'BAN' - class = 'brkts-popup-mapveto-ban' - elseif vetotype == 'pick' then - vetoText = 'PICK' - class = 'brkts-popup-mapveto-pick' - elseif vetotype == 'defaultban' then - vetoText = 'DEFAULT BAN' - class = 'brkts-popup-mapveto-defaultban' - else - return self - end - - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoMap(row, map1) - self:addColumnVetoType(row, class, vetoText) - self:addColumnVetoMap(row, map2) - - self.table:node(row) - return self -end - ----@param row Html ----@param styleClass string ----@param vetoText string ----@return self -function MapVeto:addColumnVetoType(row, styleClass, vetoText) - row:tag('td') - :tag('span') - :addClass(styleClass) - :addClass('brkts-popup-mapveto-vetotype') - :wikitext(vetoText) - return self -end - ----@param row Html ----@param map string ----@return self -function MapVeto:addColumnVetoMap(row, map) - row:tag('td'):wikitext(map):done() - return self -end - ----@return Html -function MapVeto:create() - return self.root -end - ---@param args table ---@return Html function CustomMatchSummary.getByMatchId(args) @@ -213,25 +80,7 @@ function CustomMatchSummary.createBody(match) end -- Add the Map Vetoes - if match.extradata.mapveto then - local vetoData = match.extradata.mapveto - if vetoData then - local mapVeto = MapVeto() - - for _,vetoRound in ipairs(vetoData) do - if vetoRound.vetostart then - mapVeto:vetoStart(tonumber(vetoRound.vetostart)) - end - if vetoRound.type == 'decider' then - mapVeto:addDecider(vetoRound.decider) - else - mapVeto:addRound(vetoRound.type, vetoRound.team1, vetoRound.team2) - end - end - - body:addRow(mapVeto) - end - end + body:addRow(MatchSummary.defaultMapVetoDisplay(match)) return body end @@ -251,19 +100,9 @@ function CustomMatchSummary._createGame(game) row:addElement(MatchSummary.Break():create()) end - local extradata = game.extradata or {} - local participants = game.participants or {} - - local numberOfWeapons = extradata.maximumpickindex - - local weaponsData = {} - for opponentIndex = 1, NUM_OPPONENTS do - weaponsData[opponentIndex] = {} - for weaponIndex = 1, numberOfWeapons do - local participantsKey = opponentIndex .. '_' .. weaponIndex - weaponsData[opponentIndex][weaponIndex] = (participants[participantsKey] or {}).weapon or '' - end - end + local weaponsData = Array.map(game.opponents, function(opponent) + return Array.map(opponent.players, Operator.property('weapon')) + end) row:addClass('brkts-popup-body-game') :css('font-size', '90%') @@ -287,7 +126,7 @@ function CustomMatchSummary._createGame(game) :wikitext(CustomMatchSummary._getMapDisplay(game)) ) ) - row:addElement(CustomMatchSummary._gameScore(game, 2, true)) + row:addElement(CustomMatchSummary._gameScore(game, 2)) row:addElement(CustomMatchSummary._createCheckMark(game.winner == 2)) row:addElement(CustomMatchSummary._opponentWeaponsDisplay{ data = weaponsData[2], @@ -321,15 +160,19 @@ end ---@param game MatchGroupUtilGame ---@param opponentIndex integer ----@param flip boolean? ---@return Html -function CustomMatchSummary._gameScore(game, opponentIndex, flip) +function CustomMatchSummary._gameScore(game, opponentIndex) + local score = game.scores[opponentIndex] --[[@as number|string?]] + if score and game.mode == 'Turf War' then + score = score .. '%' + end + local scoreDisplay = DisplayHelper.MapScore(score, opponentIndex, game.resultType, game.walkover, game.winner) return mw.html.create('div') :addClass('brkts-popup-body-element-vertical-centered') :css('min-width', '24px') :node(mw.html.create('div') :css('margin', 'auto') - :wikitext(game.scores[opponentIndex] or '') + :wikitext(scoreDisplay) ) end diff --git a/components/match2/wikis/splitgate/match_group_input_custom.lua b/components/match2/wikis/splitgate/match_group_input_custom.lua index 26c3636bb16..91ceb42aedd 100644 --- a/components/match2/wikis/splitgate/match_group_input_custom.lua +++ b/components/match2/wikis/splitgate/match_group_input_custom.lua @@ -6,28 +6,22 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- -local DateExt = require('Module:Date/Ext') +local Array = require('Module:Array') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') local Table = require('Module:Table') local Variables = require('Module:Variables') -local Streams = require('Module:Links/Stream') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Opponent = Lua.import('Module:Opponent') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local ALLOWED_STATUSES = { 'W', 'FF', 'DQ', 'L', 'D' } -local MAX_NUM_OPPONENTS = 8 -local MAX_NUM_VODGAMES = 9 -local MAX_NUM_MAPS = 9 local DEFAULT_BESTOF = 3 -local NO_SCORE = -99 -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_MODE = 'team' -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} @@ -36,388 +30,143 @@ local CustomMatchGroupInput = {} ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - -- Count number of maps, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) - return match -end + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) + local games = MatchFunctions.extractMaps(match, #opponents) - return map -end + match.bestof = MatchFunctions.getBestOf(match.bestof) ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if - data.finished == 'skip' or - data.finished == 'np' or - data.finished == 'cancelled' or - data.finished == 'canceled' or - data.winner == 'skip' or - data.winner == 'np' or - data.winner == 'cancelled' or - data.winner == 'canceled' - then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = 'default' - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'default') - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end - end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) - --set it as finished if we have a winner - if not Logic.isEmpty(data.winner) then - data.finished = true - end + match.games = games + match.opponents = opponents - return data, indexedScores -end + match.extradata = MatchFunctions.getExtraData(match) ----@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ----@return table[] ----@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == 'default' then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local lastScore = NO_SCORE - local lastPlacement = NO_SCORE - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) - counter = counter + 1 - if counter == 1 and (winner or '') == '' then - if finished then - winner = scoreIndex - end - end - if lastScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or lastPlacement - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement or '') or counter - lastPlacement = counter - lastScore = score or NO_SCORE - end - end - end - - return opponents, winner -end - ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score or NO_SCORE) or NO_SCORE - local value2 = tonumber(tbl[key2].score or NO_SCORE) or NO_SCORE - return value1 > value2 + return match end -- -- match related functions -- ----@param match any ----@return any -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match -end - --- Calculate the match scores based on the map results (counting map wins) --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ---@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local opponentNumber = 0 - for index = 1, MAX_NUM_OPPONENTS do - if String.isEmpty(match['opponent' .. index]) then - break - end - opponentNumber = index - end - local newScores = {} - local foundScores = false - - for i = 1, MAX_NUM_MAPS do - if match['map'..i] then - local winner = tonumber(match['map'..i].winner) - foundScores = true - if winner and winner > 0 and winner <= opponentNumber then - newScores[winner] = (newScores[winner] or 0) + 1 - end - else - break +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - for index = 1, opponentNumber do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end + table.insert(maps, map) + match[key] = nil end - return match + return maps end ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) - return MatchGroupInput.getCommonTournamentVars(match) + if bestof then + Variables.varDefine('bestof', bestof) + return bestof + end + + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF end ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - - match.links = {} - local links = match.links - if match.esl then links.esl = 'https://play.eslgaming.com/match/' .. match.esl end - if match.stats then links.stats = match.stats end - - -- apply vodgames - for index = 1, MAX_NUM_VODGAMES do - local vodgame = match['vodgame' .. index] - if not Logic.isEmpty(vodgame) then - local map = match['map' .. index] or {} - map.vod = map.vod or vodgame - match['map' .. index] = map - end +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - return match end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mapveto = matchFunctions.getMapVeto(match), - mvp = MatchGroupInput.readMvp(match), - isconverted = 0 +function MatchFunctions.getLinks(match) + return { + esl = match.esl and ('https://play.eslgaming.com/match/' .. match.esl) or nil, + stats = match.stats, } - return match -end - --- Parse the mapVeto input ----@param match table ----@return table[] -function matchFunctions.getMapVeto(match) - if not match.vetoes then return {} end - - local vetoes = mw.text.split(match.vetoes or '', ',') - match.vetoes = nil - local vetoesBy = mw.text.split(match.vetoesBy or '', ',') - match.vetoesBy = nil - local index = 1 - local currentVetoMap = mw.text.trim(vetoes[1]) - local vetoData = {} - - while not String.isEmpty(currentVetoMap) do - local by = tonumber(mw.text.trim(vetoesBy[index]) or '') - vetoData[index] = { map = currentVetoMap, by = by } - index = index + 1 - currentVetoMap = mw.text.trim(vetoes[index] or '') - end - - return vetoData end ---@param match table ---@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if Logic.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == 'team' and not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name) - end - end - end - - -- see if match should actually be finished if bestof limit was reached - if isScoreSet and not Logic.readBool(match.finished) then - local firstTo = math.ceil(match.bestof/2) - for _, item in pairs(opponents) do - if (tonumber(item.score or 0) or 0) >= firstTo then - match.finished = true - break - end - end - end - - -- check if match should actually be finished due to a non score status - if not Logic.readBool(match.finished) then - for _, opponent in pairs(opponents) do - if String.isNotEmpty(opponent.status) and opponent.status ~= 'S' then - match.finished = true - break - end - end - end - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if not Logic.isEmpty(match.winner) or Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), + } end -- -- map related functions -- --- Parse extradata information ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { +function MapFunctions.getExtraData(map, opponentCount) + return { comment = map.comment, } - return map -end - --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - if Logic.isNumeric(score) then - obj.status = 'S' - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/splitgate/match_summary.lua b/components/match2/wikis/splitgate/match_summary.lua index e96b1a4db60..1ee62ce7905 100644 --- a/components/match2/wikis/splitgate/match_summary.lua +++ b/components/match2/wikis/splitgate/match_summary.lua @@ -61,14 +61,6 @@ function CustomMatchSummary.createBody(match) return body end ----@param game MatchGroupUtilGame ----@param opponentIndex integer ----@return Html -function CustomMatchSummary._gameScore(game, opponentIndex) - local score = game.scores[opponentIndex] or '' - return htmlCreate('div'):wikitext(score) -end - ---@param game MatchGroupUtilGame ---@return MatchSummaryRow function CustomMatchSummary._createMapRow(game) @@ -97,11 +89,11 @@ function CustomMatchSummary._createMapRow(game) local leftNode = htmlCreate('div') :addClass('brkts-popup-spaced') :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 1, 'check')) - :node(CustomMatchSummary._gameScore(game, 1)) + :node(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) local rightNode = htmlCreate('div') :addClass('brkts-popup-spaced') - :node(CustomMatchSummary._gameScore(game, 2)) + :node(DisplayHelper.MapScore(game.scores[2], 2, game.resultType, game.walkover, game.winner)) :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 2, 'check')) row:addElement(leftNode) diff --git a/components/match2/wikis/starcraft/brkts_wiki_specific.lua b/components/match2/wikis/starcraft/brkts_wiki_specific.lua index daea42e93d4..171219b0417 100644 --- a/components/match2/wikis/starcraft/brkts_wiki_specific.lua +++ b/components/match2/wikis/starcraft/brkts_wiki_specific.lua @@ -62,8 +62,4 @@ WikiSpecific.matchHasDetails = FnUtil.lazilyDefineFunction(function() return StarcraftMatchGroupUtil.matchHasDetails end) --- useless functions that should be present for some default checks --- would get called from Module:Match/Subobjects if we wouldn't circumvent that module completly -WikiSpecific.processMap = FnUtil.identity - return WikiSpecific diff --git a/components/match2/wikis/starcraft2/brkts_wiki_specific.lua b/components/match2/wikis/starcraft2/brkts_wiki_specific.lua index 11d1320d08d..2ef983abaad 100644 --- a/components/match2/wikis/starcraft2/brkts_wiki_specific.lua +++ b/components/match2/wikis/starcraft2/brkts_wiki_specific.lua @@ -62,8 +62,4 @@ WikiSpecific.matchHasDetails = FnUtil.lazilyDefineFunction(function() return StarcraftMatchGroupUtil.matchHasDetails end) --- useless functions that should be present for some default checks --- would get called from Module:Match/Subobjects if we wouldn't circumvent that module completly -WikiSpecific.processMap = FnUtil.identity - return WikiSpecific diff --git a/components/match2/wikis/starcraft2/match_legacy.lua b/components/match2/wikis/starcraft2/match_legacy.lua deleted file mode 100644 index b49d0863c17..00000000000 --- a/components/match2/wikis/starcraft2/match_legacy.lua +++ /dev/null @@ -1,140 +0,0 @@ ---- --- @Liquipedia --- wiki=starcraft2 --- page=Module:Match/Legacy --- --- Please see https://github.com/Liquipedia/Lua-Modules to contribute --- - -local MatchLegacy = {} - -local json = require('Module:Json') -local String = require('Module:StringUtils') -local Table = require('Module:Table') -local _UNKNOWNREASON_DEFAULT_LOSS = 'L' - -local MODES = {solo = '1v1', team = 'team'} - -function MatchLegacy.storeMatch(match2) - local match, doStore = MatchLegacy.convertParameters(match2) - - if not doStore then - return - end - - match.games = MatchLegacy.storeGames(match, match2) - - return mw.ext.LiquipediaDB.lpdb_match( - 'legacymatch_' .. match2.match2id, - match - ) -end - -function MatchLegacy.storeGames(match, match2) - local games = '' - for gameIndex, game in ipairs(match2.match2games or {}) do - game.extradata = json.parseIfString(game.extradata or '{}') or game.extradata - - if - game.mode == '1v1' and - game.winner ~= 'skip' and - game.map ~= 'Definitions' - then - game.opponent1 = game.extradata.opponent1 - game.opponent2 = game.extradata.opponent2 - game.date = match.date - local scores = json.parseIfString(game.scores or '{}') or {} - game.opponent1score = scores[1] or 0 - game.opponent2score = scores[2] or 0 - - game.extradata.winnerrace = game.extradata.winnerfaction - game.extradata.loserrace = game.extradata.loserfaction - - -- participants holds additional playerdata per match, e.g. the faction (=race) - -- participants is stored as opponentID_playerID, so e.g. for opponent2, player1 it is '2_1' - local playerdata = json.parseIfString(game.participants or '{}') or game.participants - for key, item in pairs(playerdata) do - local keyArray = mw.text.split(key or '', '_') - local l = tonumber(keyArray[2]) - local k = tonumber(keyArray[1]) - game.extradata['opponent' .. k .. 'race'] = item.faction - local opp = match2.match2opponents[k] or {} - local pl = opp.match2players or {} - game['opponent' .. k .. 'flag'] = (pl[l] or {}).flag - game.extradata['opponent' .. k .. 'name'] = (pl[l] or {}).displayname - end - game.extradata.gamenumber = gameIndex - - game.extradata = json.stringify(game.extradata) - local res = mw.ext.LiquipediaDB.lpdb_game( - 'legacygame_' .. match2.match2id .. gameIndex, - game - ) - - games = games .. res - end - end - return games -end - -function MatchLegacy.convertParameters(match2) - local doStore = true - local match = Table.deepCopy(match2) - for key, _ in pairs(match) do - if String.startsWith(key, 'match2') then - match[key] = nil - end - end - - match.staticid = match2.match2id - match.extradata = json.parseIfString(match.extradata) or {} - local opponent1 = match2.match2opponents[1] or {} - local opponent1match2players = opponent1.match2players or {} - local opponent2 = match2.match2opponents[2] or {} - local opponent2match2players = opponent2.match2players or {} - - if opponent1.type ~= opponent2.type then - return nil, false - end - match.mode = MODES[opponent1.type] - - if opponent1.type == 'solo' then - local player = opponent1match2players[1] or {} - match.opponent1 = player.name - match.opponent1score = (tonumber(opponent1.score or 0) or 0) >= 0 and opponent1.score or 0 - match.opponent1flag = player.flag - match.extradata.opponent1name = player.displayname - player.extradata = json.parseIfString(player.extradata or '{}') or player.extradata - match.extradata.opponent1race = player.extradata.faction - player = opponent2match2players[1] or {} - match.opponent2 = player.name - match.opponent2score = (tonumber(opponent2.score or 0) or 0) >= 0 and opponent2.score or 0 - match.opponent2flag = player.flag - match.extradata.opponent2name = player.displayname - player.extradata = json.parseIfString(player.extradata or '{}') or player.extradata - match.extradata.opponent2race = player.extradata.faction - elseif opponent1.type == 'team' then - match.opponent1 = (opponent1.name or '') ~= '' and opponent1.name or 'TBD' - match.opponent1score = (tonumber(opponent1.score or 0) or 0) >= 0 and opponent1.score or 0 - match.opponent2 = (opponent2.name or '') ~= '' and opponent2.name or 'TBD' - match.opponent2score = (tonumber(opponent2.score or 0) or 0) >= 0 and opponent2.score or 0 - match.mode = 'team' - else - return nil, false - end - - if match.resulttype == 'default' then - match.resulttype = string.upper(match.walkover or '') - if match.resulttype == _UNKNOWNREASON_DEFAULT_LOSS then - --needs to be converted because in match1 storage it was marked this way - match.resulttype = 'unk' - end - match.walkover = match.winner - end - match.extradata.bestof = match2.bestof ~= 0 and tostring(match2.bestof) or '' - match.extradata = json.stringify(match.extradata) - - return match, doStore -end - -return MatchLegacy diff --git a/components/match2/wikis/stormgate/brkts_wiki_specific.lua b/components/match2/wikis/stormgate/brkts_wiki_specific.lua index e6003defff1..b89952c9bd0 100644 --- a/components/match2/wikis/stormgate/brkts_wiki_specific.lua +++ b/components/match2/wikis/stormgate/brkts_wiki_specific.lua @@ -21,8 +21,6 @@ WikiSpecific.matchFromRecord = FnUtil.lazilyDefineFunction(function() return CustomMatchGroupUtil.matchFromRecord end) -WikiSpecific.processMap = FnUtil.identity - ---Determine if a match has details that should be displayed via popup ---@param match table ---@return boolean diff --git a/components/match2/wikis/stormgate/match_group_input_custom.lua b/components/match2/wikis/stormgate/match_group_input_custom.lua index da7408e805a..9a9475304f9 100644 --- a/components/match2/wikis/stormgate/match_group_input_custom.lua +++ b/components/match2/wikis/stormgate/match_group_input_custom.lua @@ -10,690 +10,443 @@ local Array = require('Module:Array') local Faction = require('Module:Faction') local Flags = require('Module:Flags') local HeroData = mw.loadData('Module:HeroData') -local Json = require('Module:Json') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Operator = require('Module:Operator') local String = require('Module:StringUtils') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') +local OpponentLibraries = require('Module:OpponentLibraries') +local Opponent = OpponentLibraries.Opponent local Streams = Lua.import('Module:Links/Stream') -local OpponentLibrary = require('Module:OpponentLibraries') -local Opponent = OpponentLibrary.Opponent - -local DEFAULT_LOSS_STATUSES = {'FF', 'L', 'DQ'} -local DEFAULT_WIN_STATUS = 'W' -local SCORE_STATUS = 'S' -local ALLOWED_STATUSES = Array.append(DEFAULT_LOSS_STATUSES, DEFAULT_WIN_STATUS) -local RESULT_TYPE_DEFAULT = 'default' -local RESULT_TYPE_NOT_PLAYED = 'np' -local MAX_NUM_OPPONENTS = 2 -local DEFAULT_BEST_OF = 99 -local MODE_MIXED = 'mixed' -local TBD = 'tbd' +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, +} +local TBD = 'TBD' local DEFAULT_HERO_FACTION = HeroData.default.faction -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local MODE_MIXED = 'mixed' + +---@class StormgateParticipant +---@field player string +---@field faction string? +---@field heroes string[]? +---@field position integer? +---@field flag string? +---@field random boolean? local CustomMatchGroupInput = {} +local MatchFunctions = {} +local MapFunctions = {} ---- called from Module:MatchGroup ---@param match table ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) assert(not Logic.readBool(match.ffa), 'FFA is not yet supported in stormgate match2') - Table.mergeInto( - match, - CustomMatchGroupInput._readDate(match) - ) - CustomMatchGroupInput._getTournamentVars(match) - CustomMatchGroupInput._adjustData(match) - CustomMatchGroupInput._updateFinished(match) - match.stream = Streams.processStreams(match) - CustomMatchGroupInput._getExtraData(match) - - return match -end - ----@param matchArgs table ----@return table -function CustomMatchGroupInput._readDate(matchArgs) - local dateProps = MatchGroupInput.readDate(matchArgs.date, { - 'matchDate', - 'tournament_startdate', - 'tournament_enddate' - }) - - if dateProps.dateexact then - Variables.varDefine('matchDate', dateProps.date) - end + Table.mergeInto(match, MatchFunctions.readDate(match)) - return dateProps -end + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) ----@param match table -function CustomMatchGroupInput._updateFinished(match) - match.finished = Logic.nilOr(Logic.readBoolOrNil(match.finished), Logic.isNotEmpty(match.winner)) - if match.finished then - return - end + Array.forEach(opponents, function(opponent) + opponent.extradata = opponent.extradata or {} + Table.mergeInto(opponent.extradata, MatchFunctions.getOpponentExtradata(opponent)) + -- make sure match2players is not nil to avoid indexing nil + opponent.match2players = opponent.match2players or {} + Array.forEach(opponent.match2players, function(player) + player.extradata = player.extradata or {} + player.extradata.faction = MatchFunctions.getPlayerFaction(player) + end) + end) - -- Match is automatically marked finished upon page edit after a - -- certain amount of time (depending on whether the date is exact) - local threshold = match.dateexact and 30800 or 86400 - match.finished = match.timestamp + threshold < NOW -end + local games = MatchFunctions.extractMaps(match, opponents) ----@param match table -function CustomMatchGroupInput._getTournamentVars(match) - match.cancelled = Logic.emptyOr(match.cancelled, Variables.varDefault('tournament_cancelled', 'false')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - match.bestof = tonumber(Logic.emptyOr(match.bestof, Variables.varDefault('match_bestof'))) - Variables.varDefine('match_bestof', match.bestof) + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games, opponents) + or nil - MatchGroupInput.getCommonTournamentVars(match) -end + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) ----@param match table -function CustomMatchGroupInput._getExtraData(match) - match.extradata = { - casters = MatchGroupInput.readCasters(match), - ffa = 'false', - } + match.mode = MatchFunctions.getMode(opponents) - for prefix, mapVeto in Table.iter.pairsByPrefix(match, 'veto') do - match.extradata[prefix] = mapVeto and mw.ext.TeamLiquidIntegration.resolve_redirect(mapVeto) or nil - match.extradata[prefix .. 'by'] = match[prefix .. 'by'] - match.extradata[prefix .. 'displayname'] = match[prefix .. 'displayName'] + match.bestof = MatchFunctions.getBestOf(match.bestof) + local cancelled = Logic.readBool(Logic.emptyOr(match.cancelled, Variables.varDefault('tournament_cancelled'))) + if cancelled then + match.finished = match.finished or 'skip' end - Table.mergeInto(match.extradata, Table.filterByKey(match, function(key, value) - return key:match('subgroup%d+header') end)) -end + local winnerInput = match.winner --[[@as string?]] + local finishedInput = match.finished --[[@as string?]] + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) ----@param match table -function CustomMatchGroupInput._adjustData(match) - --parse opponents + set base sumscores + set mode - CustomMatchGroupInput._opponentInput(match) - - --main processing done here - local subGroupIndex = 0 - for _, _, mapIndex in Table.iter.pairsByPrefix(match, 'map') do - subGroupIndex = CustomMatchGroupInput._mapInput(match, mapIndex, subGroupIndex) + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - CustomMatchGroupInput._matchWinnerProcessing(match) -end - ----@param match table -function CustomMatchGroupInput._matchWinnerProcessing(match) - local bestof = match.bestof or DEFAULT_BEST_OF - local numberofOpponents = 0 - - Array.map(Array.range(1, MAX_NUM_OPPONENTS),function(opponentIndex) - local opponent = match['opponent' .. opponentIndex] - - if Logic.isEmpty(opponent) then return end - - numberofOpponents = numberofOpponents + 1 - - if Table.includes(ALLOWED_STATUSES, string.upper(opponent.score or '')) then - opponent.status = string.upper(opponent.score) - match.resulttype = RESULT_TYPE_DEFAULT - match.finished = true - opponent.score = -1 - - if opponent.status == DEFAULT_WIN_STATUS then - match.winner = opponentIndex - else - match.walkover = opponent.status - end - else - opponent.status = SCORE_STATUS - opponent.score = tonumber(opponent.score) or tonumber(opponent.sumscore) or -1 - if opponent.score > bestof / 2 then - match.finished = Logic.emptyOr(match.finished, true) - match.winner = tonumber(match.winner) or opponentIndex - end - end - - if Logic.readBool(match.cancelled) then - match.finished = true - if String.isEmpty(match.resulttype) and Logic.isEmpty(opponent.score) then - match.resulttype = RESULT_TYPE_NOT_PLAYED - opponent.score = opponent.score or -1 - end - end + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) - -- to not break the loop - return true - end) - - CustomMatchGroupInput._determineWinnerIfMissing(match) - - for opponentIndex = 1, numberofOpponents do - local opponent = match['opponent' .. opponentIndex] - if match.winner == 'draw' or tonumber(match.winner) == 0 or - (match.opponent1.score == bestof / 2 and match.opponent1.score == match.opponent2.score) then - match.finished = true - match.winner = 0 - match.resulttype = 'draw' - end + match.stream = Streams.processStreams(match) + match.vod = Logic.nilIfEmpty(match.vod) + match.extradata = MatchFunctions.getExtraData(match, #games) - if tonumber(match.winner) == opponentIndex or - match.resulttype == 'draw' then - opponent.placement = 1 - elseif Logic.isNumeric(match.winner) then - opponent.placement = 2 - end - end -end - ----@param match table ----@return table -function CustomMatchGroupInput._determineWinnerIfMissing(match) - if Logic.readBool(match.finished) and Logic.isEmpty(match.winner) then - local scores = Array.mapIndexes(function(opponentIndex) - local opponent = match['opponent' .. opponentIndex] - if not opponent then - return nil - end - return match['opponent' .. opponentIndex].score or -1 end - ) - local maxScore = math.max(unpack(scores) or 0) - -- if we have a positive score and the match is finished we also have a winner - if maxScore > 0 then - local maxIndexFound = false - for opponentIndex, score in pairs(scores) do - if maxIndexFound and score == maxScore then - match.winner = 0 - break - elseif score == maxScore then - maxIndexFound = true - match.winner = opponentIndex - end - end - end - end + match.games = games + match.opponents = opponents return match end ---OpponentInput functions - ----@param match table ----@return table -function CustomMatchGroupInput._opponentInput(match) - local opponentTypes = {} - - for opponentKey, opponent, opponentIndex in Table.iter.pairsByPrefix(match, 'opponent') do - opponent = Json.parseIfString(opponent) - - --Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - -- Opponent processing (first part) - -- Sort out extradata - opponent.extradata = { - advantage = opponent.advantage, - penalty = opponent.penalty, - score2 = opponent.score2, - } - - local partySize = Opponent.partySize(opponent.type) - if partySize then - opponent = CustomMatchGroupInput.processPartyOpponentInput(opponent, partySize) - elseif opponent.type == Opponent.team then - opponent = CustomMatchGroupInput.ProcessTeamOpponentInput(opponent, match.date) - opponent = CustomMatchGroupInput._readPlayersOfTeam(match, opponentIndex, opponent) - elseif opponent.type == Opponent.literal then - opponent = CustomMatchGroupInput.ProcessLiteralOpponentInput(opponent) - else - error('Unsupported Opponent Type "' .. (opponent.type or '') .. '"') - end - - --set initial opponent sumscore - opponent.sumscore = tonumber(opponent.extradata.advantage) or (-1 * (tonumber(opponent.extradata.penalty) or 0)) - - table.insert(opponentTypes, opponent.type) +---@param matchArgs table +---@return {date: string, dateexact: boolean, timestamp: integer, timezoneId: string?, timezoneOffset: string?} +function MatchFunctions.readDate(matchArgs) + local dateProps = MatchGroupInputUtil.readDate(matchArgs.date, { + 'match_date', + 'tournament_startdate', + 'tournament_enddate' + }) - match[opponentKey] = opponent + if dateProps.dateexact then + Variables.varDefine('match_date', dateProps.date) end - assert(#opponentTypes <= MAX_NUM_OPPONENTS, 'Too many opponents') - - match.mode = Array.all(opponentTypes, function(opponentType) return opponentType == opponentTypes[1] end) - and opponentTypes[1] or MODE_MIXED - - match.isTeamMatch = Array.any(opponentTypes, function(opponentType) return opponentType == Opponent.team end) - - return match + return dateProps end ----reads the players of a team from input and wiki variables ---@param match table ----@param opponentIndex integer ----@param opponent table ----@return table -function CustomMatchGroupInput._readPlayersOfTeam(match, opponentIndex, opponent) - local players = {} +---@param opponents table[] +---@return table[] +function MatchFunctions.extractMaps(match, opponents) + local maps = {} + local subGroup = 0 + for mapKey, mapInput, mapIndex in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local map + map, subGroup = MapFunctions.readMap(mapInput, subGroup, #opponents) - local teamName = opponent.name + map.participants = MapFunctions.getParticipants(mapInput, opponents) - local insertIntoPlayers = function(player) - if type(player) ~= 'table' or Logic.isEmpty(player) or Logic.isEmpty(player.name) then - return - end + map.mode = MapFunctions.getMode(mapInput, map.participants, opponents) - player.name = mw.ext.TeamLiquidIntegration.resolve_redirect(player.name):gsub(' ', '_') - player.flag = Flags.CountryName(player.flag) - player.displayname = Logic.emptyOr(player.displayname, player.displayName) - player.extradata = {faction = Faction.read(player.faction)} + Table.mergeInto(map.extradata, MapFunctions.getAdditionalExtraData(map, map.participants)) - players[player.name] = players[player.name] or {} - Table.deepMergeInto(players[player.name], player) - end + map.vod = Logic.emptyOr(mapInput.vod, match['vodgame' .. mapIndex]) - local playerIndex = 1 - local varPrefix = teamName .. '_p' .. playerIndex - local name = Variables.varDefault(varPrefix) - while name do - insertIntoPlayers{ - name = name, - displayName = Variables.varDefault(varPrefix .. 'dn'), - faction = Variables.varDefault(varPrefix .. 'faction'), - flag = Variables.varDefault(varPrefix .. 'flag'), - } - playerIndex = playerIndex + 1 - varPrefix = teamName .. '_p' .. playerIndex - name = Variables.varDefault(varPrefix) - end - - --players from manual input as `opponnetX_pY` - for _, player in Table.iter.pairsByPrefix(match, 'opponent' .. opponentIndex .. '_p') do - insertIntoPlayers(Json.parseIfString(player)) + table.insert(maps, map) + match[mapKey] = nil end - opponent.match2players = Array.extractValues(players) - --set default faction for unset factions - Array.forEach(opponent.match2players, function(player) - player.extradata.faction = player.extradata.faction or Faction.defaultFaction - end) - - return opponent + return maps end ----@param opponent table ----@return table -function CustomMatchGroupInput.ProcessLiteralOpponentInput(opponent) - local faction = opponent.faction - local flag = opponent.flag - local name = opponent.name or opponent[1] - local extradata = opponent.extradata - - local players = {} - if String.isNotEmpty(faction) or String.isNotEmpty(flag) then - players[1] = { - displayname = name, - name = TBD:upper(), - flag = Flags.CountryName(flag), - extradata = {faction = Faction.read(faction) or Faction.defaultFaction} - } - extradata.hasFactionOrFlag = true +---@param maps table[] +---@param opponents table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps, opponents) + return function(opponentIndex) + local calculatedScore = MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + if not calculatedScore then return end + local opponent = opponents[opponentIndex] + return calculatedScore + (opponent.extradata.advantage or 0) - (opponent.extradata.penalty or 0) end - - return { - type = opponent.type, - name = name, - score = opponent.score, - extradata = extradata, - match2players = players - } end ---@param opponent table ----@param partySize integer ---@return table -function CustomMatchGroupInput.processPartyOpponentInput(opponent, partySize) - local players = {} - local links = {} - - for playerIndex = 1, partySize do - local name = Logic.emptyOr(opponent['p' .. playerIndex], opponent[playerIndex]) or '' - local link = mw.ext.TeamLiquidIntegration.resolve_redirect(Logic.emptyOr( - opponent['p' .. playerIndex .. 'link'], - Variables.varDefault(name .. '_page') - ) or name):gsub(' ', '_') - table.insert(links, link) - - table.insert(players, { - displayname = name, - name = link, - flag = Flags.CountryName(Logic.emptyOr( - opponent['p' .. playerIndex .. 'flag'], - Variables.varDefault(name .. '_flag') - )), - extradata = {faction = Faction.read(Logic.emptyOr( - opponent['p' .. playerIndex .. 'faction'], - Variables.varDefault(name .. '_faction') - )) or Faction.defaultFaction} - }) - end - - table.sort(links) - +function MatchFunctions.getOpponentExtradata(opponent) return { - type = opponent.type, - name = table.concat(links, ' / '), - score = opponent.score, - extradata = opponent.extradata, - match2players = players + advantage = tonumber(opponent.advantage), + penalty = tonumber(opponent.penalty), } end ----@param opponent table ----@param date string ----@return table -function CustomMatchGroupInput.ProcessTeamOpponentInput(opponent, date) - local template = string.lower(Logic.emptyOr(opponent.template, opponent[1], '')--[[@as string]]):gsub('_', ' ') - - if String.isEmpty(template) or template == 'noteam' then - opponent = Table.merge(opponent, Opponent.blank(Opponent.team)) - opponent.name = Opponent.toName(opponent) - return opponent - end +---@param player table +---@return string +function MatchFunctions.getPlayerFaction(player) + return Faction.read(player.extradata.faction) or Faction.defaultFaction +end - assert(mw.ext.TeamTemplate.teamexists(template), 'Missing team template "' .. template .. '"') +---@param opponents {type: OpponentType} +---@return string +function MatchFunctions.getMode(opponents) + local opponentTypes = Array.map(opponents, Operator.property('type')) + return #Array.unique(opponentTypes) == 1 and opponentTypes[1] or MODE_MIXED +end - local templateData = mw.ext.TeamTemplate.raw(template, date) +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) or tonumber(Variables.varDefault('match_bestof')) - opponent.icon = templateData.image - opponent.icondark = Logic.emptyOr(templateData.imagedark, templateData.image) - opponent.name = templateData.page:gsub(' ', '_') - opponent.template = templateData.templatename or template + if bestof then + Variables.varDefine('match_bestof', bestof) + end - return opponent + return bestof end ---MapInput functions - ---@param match table ----@param mapIndex integer ----@param subGroupIndex integer ----@return integer -function CustomMatchGroupInput._mapInput(match, mapIndex, subGroupIndex) - local map = Json.parseIfString(match['map' .. mapIndex]) - map.map = mw.ext.TeamLiquidIntegration.resolve_redirect(map.map or '') - - -- set initial extradata for maps - map.extradata = { - comment = map.comment, - header = map.header, +---@param numberOfGames integer +---@return table +function MatchFunctions.getExtraData(match, numberOfGames) + local extradata = { + casters = MatchGroupInputUtil.readCasters(match, {noSort = true}), } - -- determine score, resulttype, walkover and winner - map = CustomMatchGroupInput._mapWinnerProcessing(map) - - -- get participants data for the map + get map mode + winnerfaction and loserfaction - --(w/l faction stuff only for 1v1 maps) - CustomMatchGroupInput.ProcessPlayerMapData(map, match, 2) - - --adjust sumscore for winner opponent - if (tonumber(map.winner) or 0) > 0 then - match['opponent' .. map.winner].sumscore = - match['opponent' .. map.winner].sumscore + 1 - end - - -- handle subgroup stuff if team match - if match.isTeamMatch then - map.subgroup = tonumber(map.subgroup) or (subGroupIndex + 1) - subGroupIndex = map.subgroup + for prefix, mapVeto in Table.iter.pairsByPrefix(match, 'veto') do + extradata[prefix] = mapVeto and mw.ext.TeamLiquidIntegration.resolve_redirect(mapVeto) or nil + extradata[prefix .. 'by'] = match[prefix .. 'by'] + extradata[prefix .. 'displayname'] = match[prefix .. 'displayName'] end - match['map' .. mapIndex] = map + Table.mergeInto(extradata, Table.filterByKey(match, function(key) return key:match('subgroup%d+header') end)) - return subGroupIndex + return extradata end ----@param map table +---@param mapInput table +---@param subGroup integer +---@param opponentCount integer ---@return table -function CustomMatchGroupInput._mapWinnerProcessing(map) - map.scores = {} - local hasManualScores = false - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] - local obj = {} - if Logic.isNotEmpty(score) then - hasManualScores = true - score = score - if Logic.isNumeric(score) then - obj.status = 'S' - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end +---@return integer +function MapFunctions.readMap(mapInput, subGroup, opponentCount) + subGroup = tonumber(mapInput.subgroup) or (subGroup + 1) + + local mapName = mapInput.map + if mapName and mapName:upper() ~= TBD then + mapName = mw.ext.TeamLiquidIntegration.resolve_redirect(mapName) + elseif mapName then + mapName = TBD end - local winner = tonumber(map.winner) - if Logic.isNotEmpty(map.walkover) then - local walkoverInput = tonumber(map.walkover) - if walkoverInput == 1 or walkoverInput == 2 or walkoverInput == 0 then - winner = walkoverInput - end - map.walkover = Table.includes(ALLOWED_STATUSES, map.walkover) and map.walkover or 'L' - map.scores = {-1, -1} - map.resulttype = 'default' - map.winner = winner - - return map - end + local map = { + map = mapName, + subgroup = subGroup, + extradata = { + comment = mapInput.comment, + header = mapInput.header, + } + } - if hasManualScores then - map.winner = winner or CustomMatchGroupInput._getWinner(indexedScores) + map.finished = MatchGroupInputUtil.mapIsFinished(mapInput) + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = mapInput.walkover, + winner = mapInput.winner, + opponentIndex = opponentIndex, + score = mapInput['score' .. opponentIndex], + }, MapFunctions.calculateMapScore(mapInput.winner, map.finished)) + return {score = score, status = status} + end) - return map - end + map.scores = Array.map(opponentInfo, Operator.property('score')) - if map.winner == 'skip' then - map.scores = {-1, -1} - map.resulttype = RESULT_TYPE_NOT_PLAYED - elseif winner == 1 then - map.scores = {1, 0} - elseif winner == 2 then - map.scores = {0, 1} - elseif winner == 0 or map.winner == 'draw' then - map.scores = {0.5, 0.5} - map.resulttype = 'draw' + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(mapInput.winner, mapInput.finished, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, mapInput.winner, opponentInfo) end - map.winner = winner + return map, subGroup +end - return map +---@param winnerInput string|integer|nil +---@param finished boolean +---@return fun(opponentIndex: integer): integer? +function MapFunctions.calculateMapScore(winnerInput, finished) + local winner = tonumber(winnerInput) + return function(opponentIndex) + -- TODO Better to check if map has started, rather than finished, for a more correct handling + if not winner and not finished then + return + end + return winner == opponentIndex and 1 or 0 + end end ----@param map table ----@param match table ----@param numberOfOpponents integer -function CustomMatchGroupInput.ProcessPlayerMapData(map, match, numberOfOpponents) +---@param mapInput table +---@param opponents table[] +---@return table +function MapFunctions.getParticipants(mapInput, opponents) local participants = {} - local modeParts = {} - for opponentIndex = 1, numberOfOpponents do - local opponent = match['opponent' .. opponentIndex] - local partySize = Opponent.partySize(opponent.type) - local players = opponent.match2players - if partySize then - table.insert(modeParts, partySize) - CustomMatchGroupInput._processPartyPlayerMapData(players, map, opponentIndex, participants) + Array.forEach(opponents, function(opponent, opponentIndex) + if opponent.type == Opponent.literal then + return elseif opponent.type == Opponent.team then - table.insert(modeParts, CustomMatchGroupInput._processTeamPlayerMapData(players, map, opponentIndex, participants)) - elseif opponent.type == Opponent.literal then - table.insert(modeParts, 'literal') + Table.mergeInto(participants, MapFunctions.getTeamParticipants(mapInput, opponent, opponentIndex)) + return end - end + Table.mergeInto(participants, MapFunctions.getPartyParticipants(mapInput, opponent, opponentIndex)) + end) - map.mode = table.concat(modeParts, 'v') - map.participants = participants + return participants +end - if numberOfOpponents ~= MAX_NUM_OPPONENTS or map.mode ~= '1v1' then - return - end +---@param mapInput table +---@param opponent table +---@param opponentIndex integer +---@return table +function MapFunctions.getTeamParticipants(mapInput, opponent, opponentIndex) + local players = Array.mapIndexes(function(playerIndex) + return Logic.nilIfEmpty(mapInput['t' .. opponentIndex .. 'p' .. playerIndex]) + end) - local opponentFactions, playerNameArray, heroesData - = CustomMatchGroupInput._fetchOpponentMapParticipantData(participants) - map.extradata = Table.merge(map.extradata, heroesData) - if tonumber(map.winner) == 1 then - map.extradata.winnerfaction = opponentFactions[1] - map.extradata.loserfaction = opponentFactions[2] - elseif tonumber(map.winner) == 2 then - map.extradata.winnerfaction = opponentFactions[2] - map.extradata.loserfaction = opponentFactions[1] - end - map.extradata.opponent1 = playerNameArray[1] - map.extradata.opponent2 = playerNameArray[2] -end + local participants, unattachedParticipants = MatchGroupInputUtil.parseParticipants( + opponent.match2players, + players, + function(playerIndex) + local prefix = 't' .. opponentIndex .. 'p' .. playerIndex + return { + name = mapInput[prefix], + link = Logic.nilIfEmpty(mapInput[prefix .. 'link']) or Variables.varDefault(mapInput[prefix] .. '_page'), + } + end, + function(playerIndex, playerIdData, playerInputData) + local prefix = 't' .. opponentIndex .. 'p' .. playerIndex + local faction = Faction.read(mapInput[prefix .. 'faction']) + or (playerIdData.extradata or {}).faction or Faction.defaultFaction + local link = playerIdData.name or playerInputData.link or playerInputData.name:gsub(' ', '_') + return { + faction = faction, + player = link, + flag = Flags.CountryName(playerIdData.flag), + position = playerIndex, + random = Logic.readBool(mapInput[prefix .. 'random']), + heroes = MapFunctions.readHeroes( + mapInput[prefix .. 'heroes'], + faction, + link, + Logic.readBool(mapInput[prefix .. 'noheroescheck']) + ), + } + end + ) ----@param participants table ----@return table ----@return table ----@return table -function CustomMatchGroupInput._fetchOpponentMapParticipantData(participants) - local opponentFactions, playerNameArray, heroesData = {}, {}, {} - for participantKey, participantData in pairs(participants) do - local opponentIndex = tonumber(string.sub(participantKey, 1, 1)) - -- opponentIndex can not be nil due to the format of the participants keys - ---@cast opponentIndex -nil - opponentFactions[opponentIndex] = participantData.faction - playerNameArray[opponentIndex] = participantData.player - Array.forEach(participantData.heroes or {}, function(hero, heroIndex) - heroesData['opponent' .. opponentIndex .. 'hero' .. heroIndex] = hero - end) - end + Array.forEach(unattachedParticipants, function(participant) + local name = mapInput['t' .. opponentIndex .. 'p' .. participant.position] + local nameUpper = name:upper() + local isTBD = nameUpper == TBD - return opponentFactions, playerNameArray, heroesData + table.insert(opponent.match2players, { + name = isTBD and TBD or participant.player, + displayname = isTBD and TBD or name, + flag = participant.flag, + extradata = {faction = participant.faction}, + }) + participants[#opponent.match2players] = participant + end) + + return Table.map(participants, MatchGroupInputUtil.prefixPartcipants(opponentIndex)) end ----@param players table[] ----@param map table +---@param mapInput table +---@param opponent table ---@param opponentIndex integer ----@param participants table ----@return table -function CustomMatchGroupInput._processPartyPlayerMapData(players, map, opponentIndex, participants) +---@return table +function MapFunctions.getPartyParticipants(mapInput, opponent, opponentIndex) + local players = opponent.match2players + + -- resolve the aliases in case they are used local prefix = 't' .. opponentIndex .. 'p' - for playerIndex, player in pairs(players) do - local faction = Logic.emptyOr( - map[prefix .. playerIndex .. 'faction'], - player.extradata.faction, - Faction.defaultFaction - ) - faction = Faction.read(faction) + local participants = {} + + Array.forEach(players, function(player, playerIndex) + local faction = Faction.read(mapInput['t' .. opponentIndex .. 'p' .. playerIndex .. 'faction']) + or player.extradata.faction participants[opponentIndex .. '_' .. playerIndex] = { - faction = faction, + faction = Faction.read(faction or player.extradata.faction), player = player.name, - heroes = CustomMatchGroupInput._readHeroes( - map[prefix .. playerIndex .. 'heroes'], + heroes = MapFunctions.readHeroes( + mapInput[prefix .. playerIndex .. 'heroes'], faction, player.name, - Logic.readBool(map[prefix .. playerIndex .. 'noheroescheck']) + Logic.readBool(mapInput[prefix .. playerIndex .. 'noheroescheck']) ), } - end + end) return participants end ----@param players table[] ----@param map table ----@param opponentIndex integer ----@param participants table ----@return integer -function CustomMatchGroupInput._processTeamPlayerMapData(players, map, opponentIndex, participants) - local amountOfTbds = 0 - local playerData = {} - - local numberOfPlayers = 0 - for prefix, playerInput, playerIndex in Table.iter.pairsByPrefix(map, 't' .. opponentIndex .. 'p') do - numberOfPlayers = numberOfPlayers + 1 - if playerInput:lower() == TBD then - amountOfTbds = amountOfTbds + 1 - else - local link = Logic.emptyOr(map[prefix .. 'link'], Variables.varDefault(playerInput .. '_page')) or playerInput - link = mw.ext.TeamLiquidIntegration.resolve_redirect(link):gsub(' ', '_') - - playerData[link] = { - faction = Faction.read(map[prefix .. 'faction']), - position = playerIndex, - heroes = map[prefix .. 'heroes'], - heroesCheckDisabled = Logic.readBool(map[prefix .. 'noheroescheck']), - playedRandom = Logic.readBool(map[prefix .. 'random']), - displayName = playerInput, - } - end +---@param mapInput table # the input data +---@param participants table +---@param opponents table[] +---@return string +function MapFunctions.getMode(mapInput, participants, opponents) + -- assume we have a min of 2 opponents in a game + local playerCounts = {0, 0} + for key in pairs(participants) do + local parsedOpponentIndex = key:match('(%d+)_%d+') + local opponetIndex = tonumber(parsedOpponentIndex) --[[@as integer]] + playerCounts[opponetIndex] = (playerCounts[opponetIndex] or 0) + 1 end - local addToParticipants = function(currentPlayer, player, playerIndex) - local faction = currentPlayer.faction or (player.extradata or {}).faction or Faction.defaultFaction + local modeParts = Array.map(playerCounts, function(count, opponentIndex) + if count == 0 then + return Opponent.literal + end - participants[opponentIndex .. '_' .. playerIndex] = { - faction = faction, - player = player.name, - position = currentPlayer.position, - flag = Flags.CountryName(player.flag), - heroes = CustomMatchGroupInput._readHeroes( - currentPlayer.heroes, - faction, - player.name, - currentPlayer.heroesCheckDisabled - ), - random = currentPlayer.playedRandom, - } + return count + end) + + return table.concat(modeParts, 'v') +end + +---@param map table +---@param participants table +---@return table +function MapFunctions.getAdditionalExtraData(map, participants) + if map.mode ~= '1v1' then return {} end + + local extradata = MapFunctions.getHeroesExtradata(participants) + + local players = {} + for _, player in Table.iter.spairs(participants) do + table.insert(players, player) end - Array.forEach(players, function(player, playerIndex) - local currentPlayer = playerData[player.name] - if not currentPlayer then return end + extradata.opponent1 = players[1].player + extradata.opponent2 = players[2].player - addToParticipants(currentPlayer, player, playerIndex) - playerData[player.name] = nil - end) + if map.winner ~= 1 and map.winner ~= 2 then + return extradata + end + local loser = 3 - map.winner - -- if we have players not already in the match2players insert them - -- this is to break conditional data loops between match2 and teamCard/HDB - Table.iter.forEachPair(playerData, function(playerLink, player) - local faction = player.faction or Faction.defaultFaction - table.insert(players, { - name = playerLink, - displayname = player.displayName, - extradata = {faction = faction}, - }) - addToParticipants(player, players[#players], #players) - numberOfPlayers = numberOfPlayers + 1 - end) + extradata.winnerfaction = players[map.winner].faction + extradata.loserfaction = players[loser].faction - Array.forEach(Array.range(1, amountOfTbds), function(tbdIndex) - participants[opponentIndex .. '_' .. (#players + tbdIndex)] = { - faction = Faction.defaultFaction, - player = TBD:upper(), - } - end) + return extradata +end - map.participants = participants +--- additionally store heroes in extradata so we can condition on them +---@param participants table +---@return table +function MapFunctions.getHeroesExtradata(participants) + local extradata = {} + for participantKey, participant in Table.iter.spairs(participants) do + local opponentIndex = string.match(participantKey, '^(%d+)_') + Array.forEach(participant.heroes or {}, function(hero, heroIndex) + extradata['opponent' .. opponentIndex .. 'hero' .. heroIndex] = hero + end) + end - return numberOfPlayers + return extradata end ---@param heroesInput string? @@ -701,7 +454,7 @@ end ---@param playerName string ---@param ignoreFactionHeroCheck boolean ---@return string[]? -function CustomMatchGroupInput._readHeroes(heroesInput, faction, playerName, ignoreFactionHeroCheck) +function MapFunctions.readHeroes(heroesInput, faction, playerName, ignoreFactionHeroCheck) if String.isEmpty(heroesInput) then return end @@ -715,38 +468,11 @@ function CustomMatchGroupInput._readHeroes(heroesInput, faction, playerName, ign local isCoreFaction = Table.includes(Faction.coreFactions, faction) assert(ignoreFactionHeroCheck or not isCoreFaction or faction == heroData.faction or heroData.faction == DEFAULT_HERO_FACTION, - 'Invalid hero input "' .. hero .. '" for faction "' - .. Faction.toName(faction) .. '" of player "' .. playerName .. '"') + 'Invalid hero input "' .. hero .. '" for faction "' .. Faction.toName(faction) + .. '" of player "' .. playerName .. '"') return heroData.name end) end ----@param indexedScores table ----@return integer? -function CustomMatchGroupInput._getWinner(indexedScores) - table.sort(indexedScores, CustomMatchGroupInput._mapWinnerSortFunction) - - return indexedScores[1].index -end - ----@param opponent1 table ----@param opponent2 table ----@return boolean -function CustomMatchGroupInput._mapWinnerSortFunction(opponent1, opponent2) - local opponent1Norm = opponent1.status == SCORE_STATUS - local opponent2Norm = opponent2.status == SCORE_STATUS - - if opponent1Norm and opponent2Norm then - return tonumber(opponent1.score) > tonumber(opponent2.score) - elseif opponent1Norm then return true - elseif opponent2Norm then return false - elseif opponent1.status == DEFAULT_WIN_STATUS then return true - elseif Table.includes(ALLOWED_STATUSES, opponent1.status) then return false - elseif opponent2.status == DEFAULT_WIN_STATUS then return false - elseif Table.includes(ALLOWED_STATUSES, opponent2.status) then return true - else return true - end -end - return CustomMatchGroupInput diff --git a/components/match2/wikis/stormgate/match_summary.lua b/components/match2/wikis/stormgate/match_summary.lua index fb59edff5d7..3ae5ba25147 100644 --- a/components/match2/wikis/stormgate/match_summary.lua +++ b/components/match2/wikis/stormgate/match_summary.lua @@ -366,17 +366,21 @@ function CustomMatchSummary._submatchHeader(submatch) } end + ---@param opponentIndex any + ---@return Html local createScore = function(opponentIndex) - local isWinner = opponentIndex == submatch.winner + local isWinner = opponentIndex == submatch.winner or submatch.resultType == 'draw' if submatch.resultType == 'default' then return OpponentDisplay.BlockScore{ isWinner = isWinner, - scoreText = isWinner and 'W' or submatch.walkover, + scoreText = isWinner and 'W' or string.upper(submatch.walkover), } end + + local score = submatch.resultType ~= 'np' and (submatch.scores or {})[opponentIndex] or nil return OpponentDisplay.BlockScore{ isWinner = isWinner, - scoreText = (submatch.scores or {})[opponentIndex] or '', + scoreText = score, } end diff --git a/components/match2/wikis/teamfortress/get_match_group_copy_paste_wiki.lua b/components/match2/wikis/teamfortress/get_match_group_copy_paste_wiki.lua new file mode 100644 index 00000000000..13c980eef7c --- /dev/null +++ b/components/match2/wikis/teamfortress/get_match_group_copy_paste_wiki.lua @@ -0,0 +1,50 @@ +--- +-- @Liquipedia +-- wiki=teamfortress +-- page=Module:GetMatchGroupCopyPaste/wiki +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local Class = require('Module:Class') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') + +local BaseCopyPaste = Lua.import('Module:GetMatchGroupCopyPaste/wiki/Base') + +---@class TeamFortressMatch2CopyPaste: Match2CopyPasteBase +local WikiCopyPaste = Class.new(BaseCopyPaste) + +local INDENT = WikiCopyPaste.Indent + +--returns the Code for a Match, depending on the input +---@param bestof integer +---@param mode string +---@param index integer +---@param opponents integer +---@param args table +---@return string +function WikiCopyPaste.getMatchCode(bestof, mode, index, opponents, args) + local showScore = Logic.readBool(args.score) + local stats = Logic.readBool(args.stats) + local streams = Logic.readBool(args.streams) + + local lines = Array.extend({}, + '{{Match2', + Array.map(Array.range(1, opponents), function(opponentIndex) + return INDENT .. '|opponent' .. opponentIndex .. '=' .. WikiCopyPaste.getOpponent(mode, showScore) + end), + INDENT .. '|date= |finished=', + streams and (INDENT .. '|twitch=|vod=') or nil, + stats and (INDENT .. '|etf2l=|rgl=|ozf=|tftv=') or nil, + Array.map(Array.range(1, bestof), function(mapIndex) + return INDENT .. '|map' .. mapIndex .. '={{Map|map=|score1=|score2=|finished= |logstf= |logstfgold=}}' + end), + '}}' + ) + + return table.concat(lines, '\n') +end + +return WikiCopyPaste diff --git a/components/match2/wikis/teamfortress/match_group_input_custom.lua b/components/match2/wikis/teamfortress/match_group_input_custom.lua new file mode 100644 index 00000000000..d18db788421 --- /dev/null +++ b/components/match2/wikis/teamfortress/match_group_input_custom.lua @@ -0,0 +1,159 @@ +--- +-- @Liquipedia +-- wiki=teamfortress +-- page=Module:MatchGroup/Input/Custom +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') +local Table = require('Module:Table') +local Variables = require('Module:Variables') + +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') + +local DEFAULT_MODE = 'team' + +-- containers for process helper functions +local MatchFunctions = {} +local MapFunctions = {} + +local CustomMatchGroupInput = {} + +---@param match table +---@param options table? +---@return table +function CustomMatchGroupInput.processMatch(match, options) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) + local games = CustomMatchGroupInput.extractMaps(match, #opponents) + match.bestof = MatchGroupInputUtil.getBestOf(match.bestof, games) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) + end + + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) + + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match, games) + match.games = games + match.opponents = opponents + + return match +end + +---@param match table +---@param opponentCount integer +---@return table[] +function CustomMatchGroupInput.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) + end + + table.insert(maps, map) + match[key] = nil + end + + return maps +end + +-- +-- match related functions +-- + +---@param maps table[] +---@return fun(opponentIndex: integer): integer +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + end +end + +---@param match table +---@param games table[] +---@return table +function MatchFunctions.getLinks(match, games) + local links = { + rgl = match.rgl and 'https://rgl.gg/Public/Match.aspx?m=' .. match.rgl or nil, + ozf = match.ozf and 'https://warzone.ozfortress.com/matches/' .. match.ozf or nil, + etf2l = match.etf2l and 'http://etf2l.org/matches/' .. match.etf2l or nil, + tftv = match.tftv and'http://tf.gg/' .. match.tftv or nil, + esl = match.esl and 'https://play.eslgaming.com/match/' .. match.esl or nil, + esea = match.esea and 'https://play.esea.net/match/' .. match.esea or nil, + logstf = {}, + logstfgold = {}, + } + + Array.forEach(games or {}, function(game, gameIndex) + links.logstf[gameIndex] = game.logstf and ('https://logs.tf/' .. game.logstf) or nil + links.logstfgold[gameIndex] = game.logstfgold and ('https://logs.tf/' .. game.logstfgold) or nil + end) + + return links +end + +-- +-- map related functions +-- + +-- Parse extradata information +---@param map table +---@return table +function MapFunctions.getExtraData(map) + return { + comment = map.comment, + } +end + +return CustomMatchGroupInput diff --git a/components/match2/wikis/teamfortress/match_legacy.lua b/components/match2/wikis/teamfortress/match_legacy.lua new file mode 100644 index 00000000000..25606c4cccd --- /dev/null +++ b/components/match2/wikis/teamfortress/match_legacy.lua @@ -0,0 +1,71 @@ +--- +-- @Liquipedia +-- wiki=teamfortress +-- page=Module:Match/Legacy +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local MatchLegacy = {} + +local Json = require('Module:Json') +local Opponent = require('Module:Opponent') +local String = require('Module:StringUtils') +local Table = require('Module:Table') + +function MatchLegacy.storeMatch(match2) + return mw.ext.LiquipediaDB.lpdb_match( + 'legacymatch_' .. match2.match2id, + MatchLegacy._convertParameters(match2) + ) +end + +function MatchLegacy._convertParameters(match2) + local match = Table.deepCopy(match2) + for key, _ in pairs(match) do + if String.startsWith(key, 'match2') then + match[key] = nil + end + end + + if match.walkover == 'ff' or match.walkover == 'dq' then + match.walkover = match.winner + elseif match.walkover == 'l' then + match.walkover = nil + end + + match.staticid = match2.match2id + + -- Handle Opponents + local handleOpponent = function (index) + local prefix = 'opponent'..index + local opponent = match2.match2opponents[index] or {} + local opponentmatch2players = opponent.match2players or {} + if opponent.type == Opponent.team then + match[prefix] = opponent.name + match[prefix..'score'] = (tonumber(opponent.score) or 0) > 0 and opponent.score or 0 + local opponentplayers = {} + for i = 1, 6 do + local player = opponentmatch2players[i] or {} + opponentplayers['p' .. i] = player.name or '' + opponentplayers['p' .. i .. 'flag'] = player.flag or '' + opponentplayers['p' .. i .. 'dn'] = player.displayname or '' + end + match[prefix..'players'] = opponentplayers + elseif opponent.type == Opponent.solo then + local player = opponentmatch2players[1] or {} + match[prefix] = player.name + match[prefix..'score'] = (tonumber(opponent.score) or 0) > 0 and opponent.score or 0 + match[prefix..'flag'] = player.flag + elseif opponent.type == Opponent.literal then + match[prefix] = 'TBD' + end + end + + handleOpponent(1) + handleOpponent(2) + + return Json.stringifySubTables(match) +end + +return MatchLegacy diff --git a/components/match2/wikis/teamfortress/match_summary.lua b/components/match2/wikis/teamfortress/match_summary.lua new file mode 100644 index 00000000000..df2b8d263a6 --- /dev/null +++ b/components/match2/wikis/teamfortress/match_summary.lua @@ -0,0 +1,145 @@ +--- +-- @Liquipedia +-- wiki=teamfortress +-- page=Module:MatchSummary +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local DateExt = require('Module:Date/Ext') +local Icon = require('Module:Icon') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') +local Page = require('Module:Page') + +local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper') +local MatchSummary = Lua.import('Module:MatchSummary/Base') + +local htmlCreate = mw.html.create + +---@enum TFMatchIcons +local Icons = { + CHECK = Icon.makeIcon{iconName = 'winner', color = 'forest-green-text', size = 'initial'}, + EMPTY = '[[File:NoCheck.png|link=]]', +} + +local LINK_DATA = { + logstf = {icon = 'File:Logstf_icon.png', text = 'logs.tf Match Page '}, + logstfgold = {icon = 'File:Logstf_gold_icon.png', text = 'logs.tf Match Page (Golden Cap) '}, + esl = { + icon = 'File:ESL 2019 icon lightmode.png', + iconDark = 'File:ESL 2019 icon darkmode.png', + text = 'ESL matchpage' + }, + esea = {icon = 'File:ESEA icon allmode.png', text = 'ESEA Match Page'}, + etf2l = {icon = 'File:ETF2L.png', text = 'ETF2L Match Page'}, + rgl = {icon = 'File:RGL_Logo.png', text = 'RGL Match Page'}, + ozf = {icon = 'File:ozfortress-icon.png', text = 'ozfortress Match Page'}, + tftv = {icon = 'File:Teamfortress.tv.png', text = 'TFTV Match Page'}, +} + +local CustomMatchSummary = {} + +---@param args table +---@return Html +function CustomMatchSummary.getByMatchId(args) + return MatchSummary.defaultGetByMatchId(CustomMatchSummary, args) +end + +---@param match MatchGroupUtilMatch +---@param footer MatchSummaryFooter +---@return MatchSummaryFooter +function CustomMatchSummary.addToFooter(match, footer) + footer = MatchSummary.addVodsToFooter(match, footer) + :addLinks(LINK_DATA, match.links) + + return footer +end + +---@param match MatchGroupUtilMatch +---@return MatchSummaryBody +function CustomMatchSummary.createBody(match) + local body = MatchSummary.Body() + + if not DateExt.isDefaultTimestamp(match.timestamp) then + -- dateIsExact means we have both date and time. Show countdown + -- if match is not default timestamp, we have a date, so display the date + body:addRow(MatchSummary.Row():addElement( + DisplayHelper.MatchCountdownBlock(match) + )) + end + + -- Iterate each map + Array.forEach(match.games, function(game) + body:addRow(CustomMatchSummary._createMapRow(game)) + end) + + return body +end + +---@param game MatchGroupUtilGame +---@param opponentIndex integer +---@return Html +function CustomMatchSummary._gameScore(game, opponentIndex) + local score = game.scores[opponentIndex] + local scoreDisplay = DisplayHelper.MapScore(score, opponentIndex, game.resultType, game.walkover, game.winner) + return htmlCreate('div'):wikitext(scoreDisplay) +end + +---@param game MatchGroupUtilGame +---@return MatchSummaryRow +function CustomMatchSummary._createMapRow(game) + local row = MatchSummary.Row() + + local centerNode = mw.html.create('div') + :addClass('brkts-popup-spaced') + :wikitext(Page.makeInternalLink(game.map)) + :css('text-align', 'center') + + if game.resultType == 'np' then + centerNode:addClass('brkts-popup-spaced-map-skip') + end + + local leftNode = mw.html.create('div') + :addClass('brkts-popup-spaced') + :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 1, Icons.CHECK)) + :node(CustomMatchSummary._gameScore(game, 1)) + :css('width', '20%') + + local rightNode = mw.html.create('div') + :addClass('brkts-popup-spaced') + :node(CustomMatchSummary._gameScore(game, 2)) + :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 2, Icons.CHECK)) + :css('width', '20%') + + row:addElement(leftNode) + :addElement(centerNode) + :addElement(rightNode) + + row:addClass('brkts-popup-body-game') + :css('overflow', 'hidden') + + -- Add Comment + if Logic.isNotEmpty(game.comment) then + row:addElement(MatchSummary.Break():create()) + local comment = mw.html.create('div') + :wikitext(game.comment) + :css('margin', 'auto') + row:addElement(comment) + end + + return row +end + +---@param showIcon boolean? +---@param icon string? +---@return Html +function CustomMatchSummary._createCheckMarkOrCross(showIcon, icon) + return mw.html.create('div') + :addClass('brkts-popup-spaced') + :css('line-height', '27px') + :node(showIcon and icon or Icons.EMPTY) +end + +return CustomMatchSummary diff --git a/components/match2/wikis/tetris/match_group_input_custom.lua b/components/match2/wikis/tetris/match_group_input_custom.lua index e070b6d977b..6f10d28a17f 100644 --- a/components/match2/wikis/tetris/match_group_input_custom.lua +++ b/components/match2/wikis/tetris/match_group_input_custom.lua @@ -7,36 +7,26 @@ -- local Array = require('Module:Array') -local DateExt = require('Module:Date/Ext') -local Json = require('Module:Json') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') local Table = require('Module:Table') local Variables = require('Module:Variables') +local Streams = require('Module:Links/Stream') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') -local Streams = Lua.import('Module:Links/Stream') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') local OpponentLibrary = require('Module:OpponentLibraries') local Opponent = OpponentLibrary.Opponent -local UNKNOWN_REASON_LOSS_STATUS = 'L' -local DEFAULT_WIN_STATUS = 'W' -local DEFAULT_WIN_RESULTTYPE = 'default' -local NO_SCORE = -1 -local SCORE_STATUS = 'S' -local ALLOWED_STATUSES = {DEFAULT_WIN_STATUS, 'FF', 'DQ', UNKNOWN_REASON_LOSS_STATUS} -local MAX_NUM_OPPONENTS = 2 -local DEFAULT_BEST_OF = 99 -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) -local BYE = 'bye' -local MAX_NUM_MAPS = 30 +local DEFAULT_BESTOF = 99 -local CustomMatchGroupInput = {} +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, +} -CustomMatchGroupInput.walkoverProcessing = {} -local walkoverProcessing = CustomMatchGroupInput.walkoverProcessing +local CustomMatchGroupInput = {} -- called from Module:MatchGroup ---@param match table @@ -44,407 +34,118 @@ local walkoverProcessing = CustomMatchGroupInput.walkoverProcessing function CustomMatchGroupInput.processMatch(match) if Logic.readBool(match.ffa) then error('FFA matches are not yet supported') - -- later call ffa processing from here - elseif match['opponent' .. (MAX_NUM_OPPONENTS + 1)] then - error('Unexpected number of opponents in a non-FFA match') end - - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = CustomMatchGroupInput._getExtraData(match) - match = CustomMatchGroupInput._getTournamentVars(match) - match = CustomMatchGroupInput._adjustData(match) - match = CustomMatchGroupInput._getVodStuff(match) - - return match -end - ----@param match table ----@return table -function CustomMatchGroupInput._getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'solo')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - return MatchGroupInput.getCommonTournamentVars(match) -end - ----@param match table ----@return table -function CustomMatchGroupInput._getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod) - - return match -end - ----@param match table ----@return table -function CustomMatchGroupInput._getExtraData(match) - match.extradata = { - casters = MatchGroupInput.readCasters(match, {noSort = true}), - } - - for subGroupIndex = 1, MAX_NUM_MAPS do - local prefix = 'subgroup' .. subGroupIndex - - match.extradata[prefix .. 'header'] = String.nilIfEmpty(match['set' .. subGroupIndex .. 'header']) - end - - return match -end - ----@param match table ----@return table -function CustomMatchGroupInput._adjustData(match) - --parse opponents + set base sumscores - match = CustomMatchGroupInput._opponentInput(match) - - --main processing done here - local subGroupIndex = 0 - for _, _, mapIndex in Table.iter.pairsByPrefix(match, 'map') do - match, subGroupIndex = CustomMatchGroupInput._mapInput(match, mapIndex, subGroupIndex) - end - - match = CustomMatchGroupInput._matchWinnerProcessing(match) - - CustomMatchGroupInput._setPlacements(match) - if CustomMatchGroupInput._hasTeamOpponent(match) then error('Team opponents are currently not yet supported on tetris wiki') - -- todo (sep PR): - -- Each set contains various 1v1 matches with different players, when a team wins 3 1v1s matches they win the set - -- First team to get 2 set, wins the complete series - -- match = CustomMatchGroupInput._subMatchStructure(match) - end - - if Logic.isNumeric(match.winner) then - match.finished = true end - return match -end - ----@param match table ----@return table -function CustomMatchGroupInput._matchWinnerProcessing(match) - local bestof = tonumber(match.bestof) or Variables.varDefault('bestof', DEFAULT_BEST_OF) - match.bestof = bestof - Variables.varDefine('bestof', bestof) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) - local scores = Array.map(Array.range(1, MAX_NUM_OPPONENTS), function(opponentIndex) - local opponent = match['opponent' .. opponentIndex] - if not opponent then - return NO_SCORE - end - - -- set the score either from manual input or sumscore - opponent.score = Table.includes(ALLOWED_STATUSES, string.upper(opponent.score or '')) - and string.upper(opponent.score) - or tonumber(opponent.score) or tonumber(opponent.sumscore) or NO_SCORE - - return opponent.score + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) + local games = CustomMatchGroupInput.extractMaps(match, opponents) + match.bestof = CustomMatchGroupInput.getBestOf(match) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and CustomMatchGroupInput.calculateMatchScore(games) + or nil + + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) end) - walkoverProcessing.walkover(match, scores) - - if match.resulttype == DEFAULT_WIN_RESULTTYPE then - walkoverProcessing.applyMatchWalkoverToOpponents(match) - return match - end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) - if match.winner == 'draw' then - match.winner = 0 + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local opponent = match['opponent' .. opponentIndex] - if Logic.isNotEmpty(opponent) then - opponent.status = SCORE_STATUS - if opponent.score > bestof / 2 then - match.finished = Logic.emptyOr(match.finished, true) - match.winner = tonumber(match.winner) or opponentIndex - elseif match.winner == 0 or (opponent.score == bestof / 2 and match.opponent1.score == match.opponent2.score) then - match.finished = Logic.emptyOr(match.finished, true) - match.winner = 0 - match.resulttype = 'draw' - end - end - end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'solo')) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) - match.winner = tonumber(match.winner) + match.stream = Streams.processStreams(match) - CustomMatchGroupInput._checkFinished(match) + match.games = games + match.opponents = opponents - if match.finished and not match.winner then - CustomMatchGroupInput._determineWinnerIfMissing(match, scores) - end + match.extradata = CustomMatchGroupInput.getExtraData(match) return match end ---@param match table -function CustomMatchGroupInput._checkFinished(match) - if Logic.readBoolOrNil(match.finished) == false then - match.finished = false - elseif Logic.readBool(match.finished) or match.winner then - match.finished = true - end - - -- Match is automatically marked finished upon page edit after a - -- certain amount of time (depending on whether the date is exact) - if not match.finished and match.timestamp > DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end -end - ----@param match table ----@param scores integer[] -function CustomMatchGroupInput._determineWinnerIfMissing(match, scores) - local maxScore = math.max(unpack(scores) or 0) - -- if we have a positive score and the match is finished we also have a winner - if maxScore > 0 then - if Array.all(scores, function(score) return score == maxScore end) then - match.winner = 0 - return - end - - for opponentIndex, score in pairs(scores) do - if score == maxScore then - match.winner = opponentIndex - return - end - end - end -end - ----@param match table -function CustomMatchGroupInput._setPlacements(match) - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local opponent = match['opponent' .. opponentIndex] - - if match.winner == opponentIndex or match.winner == 0 then - opponent.placement = 1 - elseif match.winner then - opponent.placement = 2 - end - end -end - ---[[ - -OpponentInput functions - -]]-- - ----@param match table ----@return table -function CustomMatchGroupInput._opponentInput(match) - local opponentIndex = 1 - local opponent = match['opponent' .. opponentIndex] - - while opponentIndex <= MAX_NUM_OPPONENTS and Logic.isNotEmpty(opponent) do - opponent = Json.parseIfString(opponent) or Opponent.blank() - - -- Convert byes to literals - if - string.lower(opponent.template or '') == BYE - or string.lower(opponent.name or '') == BYE - then - opponent = {type = Opponent.literal, name = BYE:upper()} - end - - --process input - if opponent.type == Opponent.team or opponent.type == Opponent.solo or - opponent.type == Opponent.literal then - - opponent = CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - else - error('Unsupported Opponent Type') +---@param opponents table[] +---@return table[] +function CustomMatchGroupInput.extractMaps(match, opponents) + local maps = {} + for key, map, mapIndex in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = { + comment = map.comment, + } + map.map = 'Game ' .. mapIndex + map.mode = Opponent.toMode(opponents[1].type, opponents[2].type) + + map.finished = MatchGroupInputUtil.mapIsFinished(map) + local opponentInfo = Array.map(opponents, function(_, opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }, CustomMatchGroupInput.calculateMapScore(map.winner, map.finished)) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - --set initial opponent sumscore - opponent.sumscore = 0 - - match['opponent' .. opponentIndex] = opponent - - opponentIndex = opponentIndex + 1 - opponent = match['opponent' .. opponentIndex] + table.insert(maps, map) + match[key] = nil end - return match + return maps end ----@param record table ----@param timestamp integer ----@return table -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() - - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end - - Opponent.resolve(opponent, teamTemplateDate, {syncPlayer = true}) - - MatchGroupInput.mergeRecordWithOpponent(record, opponent) - - for _, player in pairs(record.players or {}) do - player.name = player.name:gsub(' ', '_') - end - - if record.name then - record.name = record.name:gsub(' ', '_') +---@param maps table[] +---@return fun(opponentIndex: integer): integer +function CustomMatchGroupInput.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end - - return record end ---[[ - -MapInput functions - -]]-- - ---@param match table ----@param mapIndex integer ----@param subGroupIndex integer ----@return table ---@return integer -function CustomMatchGroupInput._mapInput(match, mapIndex, subGroupIndex) - local map = Json.parseIfString(match['map' .. mapIndex]) - - if Table.isEmpty(map) then - match['map' .. mapIndex] = nil - return match, subGroupIndex - end - - -- Tetris has no map names, use generic one instead - map.map = 'Game ' .. mapIndex - - -- set initial extradata for maps - map.extradata = { - comment = map.comment, - } - - -- determine score, resulttype, walkover and winner - map = CustomMatchGroupInput._mapWinnerProcessing(map) - - -- get participants data for the map + get map mode - map = CustomMatchGroupInput._processPlayerMapData(map, match) - - -- set sumscore to 0 if it isn't a number - if Logic.isEmpty(match.opponent1.sumscore) then - match.opponent1.sumscore = 0 - end - if Logic.isEmpty(match.opponent2.sumscore) then - match.opponent2.sumscore = 0 - end - - --adjust sumscore for winner opponent - if (tonumber(map.winner) or 0) > 0 then - match['opponent' .. map.winner].sumscore = - match['opponent' .. map.winner].sumscore + 1 - end - - match['map' .. mapIndex] = map - - return match, subGroupIndex +function CustomMatchGroupInput.getBestOf(match) + local bestOf = tonumber(Logic.emptyOr(match.bestof, Variables.varDefault('match_bestof'))) + Variables.varDefine('match_bestof', bestOf) + return bestOf or DEFAULT_BESTOF end ----@param map table ----@return table -function CustomMatchGroupInput._mapWinnerProcessing(map) - if map.winner == 'skip' then - map.scores = {NO_SCORE, NO_SCORE} - map.resulttype = 'np' - - return map - end - - map.scores = {} - local hasManualScores = false - - local scores = Array.map(Array.range(1, MAX_NUM_OPPONENTS), function(opponentIndex) - local score = map['score' .. opponentIndex] - map.scores[opponentIndex] = tonumber(score) or NO_SCORE - - if String.isNotEmpty(score) then - hasManualScores = true - end - - return Table.includes(ALLOWED_STATUSES, string.upper(score or '')) - and score:upper() - or map.scores[opponentIndex] - end) - - if not hasManualScores then - local winnerInput = tonumber(map.winner) - if winnerInput == 1 then - map.scores = {1, 0} - elseif winnerInput == 2 then - map.scores = {0, 1} - end - - return map - end - - walkoverProcessing.walkover(map, scores) - - return map -end - ----@param map table ---@param match table ---@return table -function CustomMatchGroupInput._processPlayerMapData(map, match) - local participants = {} - - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local opponent = match['opponent' .. opponentIndex] - if Opponent.typeIsParty(opponent.type) then - CustomMatchGroupInput._processDefaultPlayerMapData( - opponent.match2players or {}, - opponentIndex, - map, - participants - ) - elseif opponent.type == Opponent.team then - error('Team opponents are currently not yet supported on tetris wiki') - --[[ todo in a sep PR: - CustomMatchGroupInput._processTeamPlayerMapData( - opponent.match2players or {}, - opponentIndex, - map, - participants - ) - ]] - end - end - - map.mode = Opponent.toMode(match.opponent1.type, match.opponent2.type) - - map.participants = participants - - return map -end - ----@param players table[] ----@param opponentIndex integer ----@param map table ----@param participants table -function CustomMatchGroupInput._processDefaultPlayerMapData(players, opponentIndex, map, participants) - for playerIndex = 1, #players do - participants[opponentIndex .. '_' .. playerIndex] = { - played = true, - } - end +function CustomMatchGroupInput.getExtraData(match) + return { + casters = MatchGroupInputUtil.readCasters(match, {noSort = true}), + } end ---@param match table @@ -453,107 +154,17 @@ function CustomMatchGroupInput._hasTeamOpponent(match) return match.opponent1.type == Opponent.team or match.opponent2.type == Opponent.team end ----@param obj table ----@param scores (string|number)[] -function walkoverProcessing.walkover(obj, scores) - local walkover = obj.walkover - - if Logic.isNumeric(walkover) then - walkoverProcessing.numericWalkover(obj, walkover) - elseif walkover then - walkoverProcessing.nonNumericWalkover(obj, walkover) - elseif #scores ~= 2 then -- since we always have 2 opponents outside of ffa - error('Unexpected number of opponents when calculating winner') - elseif Array.all(scores, function(score) - return Table.includes(ALLOWED_STATUSES, score) and score ~= DEFAULT_WIN_STATUS - end) then - - walkoverProcessing.scoreDoubleWalkover(obj, scores) - elseif Array.any(scores, function(score) return Table.includes(ALLOWED_STATUSES, score) end) then - walkoverProcessing.scoreWalkover(obj, scores) - end -end - ----@param obj table ----@param walkover number -function walkoverProcessing.numericWalkover(obj, walkover) - local winner = tonumber(walkover) - - obj.winner = winner - obj.finished = true - obj.walkover = UNKNOWN_REASON_LOSS_STATUS - obj.resulttype = DEFAULT_WIN_RESULTTYPE -end - ----@param obj table ----@param walkover string -function walkoverProcessing.nonNumericWalkover(obj, walkover) - if not Table.includes(ALLOWED_STATUSES, string.upper(walkover)) then - error('Invalid walkover "' .. walkover .. '"') - elseif not Logic.isNumeric(obj.winner) then - error('Walkover without winner specified') - end - - obj.winner = tonumber(obj.winner) - obj.finished = true - obj.walkover = walkover:upper() - obj.resulttype = DEFAULT_WIN_RESULTTYPE -end - ----@param obj table ----@param scores string[] -function walkoverProcessing.scoreDoubleWalkover(obj, scores) - obj.winner = -1 - obj.finished = true - obj.walkover = scores[1] - obj.resulttype = DEFAULT_WIN_RESULTTYPE -end - ----@param obj table ----@param scores (string|number)[] -function walkoverProcessing.scoreWalkover(obj, scores) - local winner, status - - for scoreIndex, score in pairs(scores) do - score = string.upper(score) - if score == DEFAULT_WIN_STATUS then - winner = scoreIndex - elseif Table.includes(ALLOWED_STATUSES, score) then - status = score - else - status = UNKNOWN_REASON_LOSS_STATUS - end - end - - if not winner then - error('Invalid score combination "{' .. scores[1] .. ', ' .. scores[2] .. '}"') - end - - obj.winner = winner - obj.finished = true - obj.walkover = status - obj.resulttype = DEFAULT_WIN_RESULTTYPE -end - ----@param match table -function walkoverProcessing.applyMatchWalkoverToOpponents(match) - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local score = match['opponent' .. opponentIndex].score - - if Logic.isNumeric(score) or String.isEmpty(score) then - match['opponent' .. opponentIndex].score = String.isNotEmpty(score) and score or NO_SCORE - match['opponent' .. opponentIndex].status = match.walkover - elseif score and Table.includes(ALLOWED_STATUSES, score:upper()) then - match['opponent' .. opponentIndex].score = NO_SCORE - match['opponent' .. opponentIndex].status = score - else - error('Invalid score "' .. score .. '"') +---@param winnerInput string|integer|nil +---@param finished boolean +---@return fun(opponentIndex: integer): integer? +function CustomMatchGroupInput.calculateMapScore(winnerInput, finished) + local winner = tonumber(winnerInput) + return function(opponentIndex) + -- TODO Better to check if map has started, rather than finished, for a more correct handling + if not winner and not finished then + return end - end - - -- neither match.opponent0 nor match['opponent-1'] does exist hence the if - if match['opponent' .. match.winner] then - match['opponent' .. match.winner].status = DEFAULT_WIN_STATUS + return winner == opponentIndex and 1 or 0 end end diff --git a/components/match2/wikis/tetris/match_summary.lua b/components/match2/wikis/tetris/match_summary.lua index b30e54463fc..5d313409ef3 100644 --- a/components/match2/wikis/tetris/match_summary.lua +++ b/components/match2/wikis/tetris/match_summary.lua @@ -44,8 +44,6 @@ function CustomMatchSummary.createBody(match) if Array.any(match.opponents, function(opponent) return opponent.type == Opponent.team end) then error('Team matches not yet supported') - -- todo (in sep PR): team match submatch support - --return CustomMatchSummary._createTeamMatchBody(body, match) end -- Iterate each map diff --git a/components/match2/wikis/tft/get_match_group_copy_paste_wiki.lua b/components/match2/wikis/tft/get_match_group_copy_paste_wiki.lua new file mode 100644 index 00000000000..5329807af16 --- /dev/null +++ b/components/match2/wikis/tft/get_match_group_copy_paste_wiki.lua @@ -0,0 +1,51 @@ +--- +-- @Liquipedia +-- wiki=tft +-- page=Module:GetMatchGroupCopyPaste/wiki +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local Class = require('Module:Class') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') + +local BaseCopyPaste = Lua.import('Module:GetMatchGroupCopyPaste/wiki/Base') + +---@class TftMatch2CopyPaste: Match2CopyPasteBase +local WikiCopyPaste = Class.new(BaseCopyPaste) + +local INDENT = WikiCopyPaste.Indent + +--returns the Code for a Match, depending on the input +---@param bestof integer +---@param mode string +---@param index integer +---@param opponents integer +---@param args table +---@return string +function WikiCopyPaste.getMatchCode(bestof, mode, index, opponents, args) + local showScore = Logic.nilOr(Logic.readBool(args.score), bestof == 0) + local opponent = WikiCopyPaste.getOpponent(mode, showScore) + + local lines = Array.extendWith({}, + '{{Match', + index == 1 and (INDENT .. '|bestof=' .. (bestof ~= 0 and bestof or '')) or nil, + Logic.readBool(args.needsWinner) and (INDENT .. '|winner=') or nil, + INDENT .. '|date=', + Logic.readBool(args.streams) and (INDENT .. '|twitch=|youtube=|vod=') or nil, + Logic.readBool(args.casters) and (INDENT .. '|caster1=|caqster2=') or nil, + Array.map(Array.range(1, opponents), function(opponentIndex) + return INDENT .. '|opponent' .. opponentIndex .. '=' .. opponent + end), + bestof ~= 0 and Array.map(Array.range(1, bestof), function(mapIndex) + return INDENT .. '|map' .. mapIndex .. '={{Map|map=|score1=|score2=|finished=|winner=}}' + end) or nil, + INDENT .. '}}' + ) + + return table.concat(lines, '\n') +end + +return WikiCopyPaste diff --git a/components/match2/wikis/tft/match_group_input_custom.lua b/components/match2/wikis/tft/match_group_input_custom.lua new file mode 100644 index 00000000000..537fc4ba4a2 --- /dev/null +++ b/components/match2/wikis/tft/match_group_input_custom.lua @@ -0,0 +1,160 @@ +--- +-- @Liquipedia +-- wiki=tft +-- page=Module:MatchGroup/Input/Custom +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local Lua = require('Module:Lua') +local Operator = require('Module:Operator') +local Table = require('Module:Table') +local Variables = require('Module:Variables') + +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') +local Streams = Lua.import('Module:Links/Stream') + +local DEFAULT_BESTOF = 3 +local DEFAULT_MODE = 'team' + +local CustomMatchGroupInput = {} +local MatchFunctions = {} +local MapFunctions = {} + +---@param match table +---@param options table? +---@return table +function CustomMatchGroupInput.processMatch(match, options) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex) + end) + + local games = MatchFunctions.extractMaps(match, opponents) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) + end + + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) + match.mode = Variables.varDefault('tournament_mode', DEFAULT_MODE) + + match.stream = Streams.processStreams(match) + + match.games = games + match.opponents = opponents + + match.extradata = MatchFunctions.getExtraData(match) + + return match +end + +---@param match table +---@return table +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), + casters = MatchGroupInputUtil.readCasters(match), + } +end + +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + end +end + +---@param bestofInput string|integer? +---@return integer +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) or tonumber(Variables.varDefault('match_bestof')) or DEFAULT_BESTOF + Variables.varDefine('match_bestof', bestof) + return bestof +end + +---@param match table +---@param opponents table[] +---@return table[] +function MatchFunctions.extractMaps(match, opponents) + local maps = {} + for mapKey, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, #opponents), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }, MapFunctions.calculateMapScore(map.winner, map.finished)) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) + end + + map.extradata = MapFunctions.getExtradata(map, opponents) + + table.insert(maps, map) + match[mapKey] = nil + end + + return maps +end + +---@param mapInput table +---@param opponents table[] +---@return table +function MapFunctions.getExtradata(mapInput, opponents) + return { + comment = mapInput.comment, + } +end + +---@param winnerInput string|integer|nil +---@param finished boolean +---@return fun(opponentIndex: integer): integer? +function MapFunctions.calculateMapScore(winnerInput, finished) + local winner = tonumber(winnerInput) + return function(opponentIndex) + -- TODO Better to check if map has started, rather than finished, for a more correct handling + if not winner and not finished then + return + end + return winner == opponentIndex and 1 or 0 + end +end + +return CustomMatchGroupInput diff --git a/components/match2/wikis/tft/match_summary.lua b/components/match2/wikis/tft/match_summary.lua new file mode 100644 index 00000000000..7d39b788b5b --- /dev/null +++ b/components/match2/wikis/tft/match_summary.lua @@ -0,0 +1,130 @@ +--- +-- @Liquipedia +-- wiki=tft +-- page=Module:MatchSummary +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local DateExt = require('Module:Date/Ext') +local Icon = require('Module:Icon') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') +local Page = require('Module:Page') +local Table = require('Module:Table') + +local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper') +local MatchSummary = Lua.import('Module:MatchSummary/Base') + +local Icons = { + CHECK = Icon.makeIcon{iconName = 'winner', color = 'forest-green-text', size = '110%'}, + EMPTY = '[[File:NoCheck.png|link=]]', +} + +local CustomMatchSummary = {} + +---@param args table +---@return Html +function CustomMatchSummary.getByMatchId(args) + return MatchSummary.defaultGetByMatchId(CustomMatchSummary, args) +end + +---@param match MatchGroupUtilMatch +---@return MatchSummaryBody +function CustomMatchSummary.createBody(match) + local body = MatchSummary.Body() + + if match.dateIsExact or match.timestamp ~= DateExt.defaultTimestamp then + -- dateIsExact means we have both date and time. Show countdown + -- if match is not epoch=0, we have a date, so display the date + body:addRow(MatchSummary.Row():addElement( + DisplayHelper.MatchCountdownBlock(match) + )) + end + + -- Iterate each map + for _, game in ipairs(match.games) do + if game.map then + body:addRow(CustomMatchSummary._createMapRow(game)) + end + end + + -- Add Match MVP(s) + if match.extradata.mvp then + local mvpData = match.extradata.mvp + if not Table.isEmpty(mvpData) and mvpData.players then + local mvp = MatchSummary.Mvp() + for _, player in ipairs(mvpData.players) do + mvp:addPlayer(player) + end + mvp:setPoints(mvpData.points) + + body:addRow(mvp) + end + + end + + -- casters + body:addRow(MatchSummary.makeCastersRow(match.extradata.casters)) + + return body +end + +---@param game MatchGroupUtilGame +---@return MatchSummaryRow +function CustomMatchSummary._createMapRow(game) + local row = MatchSummary.Row() + + local centerNode = mw.html.create('div') + :addClass('brkts-popup-spaced') + :wikitext(Page.makeInternalLink(game.map)) + :css('text-align', 'center') + + if game.resultType == 'np' then + centerNode:addClass('brkts-popup-spaced-map-skip') + end + + local leftNode = mw.html.create('div') + :addClass('brkts-popup-spaced') + :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 1, Icons.CHECK)) + :node(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) + :css('width', '20%') + + local rightNode = mw.html.create('div') + :addClass('brkts-popup-spaced') + :node(DisplayHelper.MapScore(game.scores[2], 2, game.resultType, game.walkover, game.winner)) + :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 2, Icons.CHECK)) + :css('width', '20%') + + row:addElement(leftNode) + :addElement(centerNode) + :addElement(rightNode) + + row:addClass('brkts-popup-body-game') + :css('overflow', 'hidden') + + -- Add Comment + if Logic.isNotEmpty(game.comment) then + row:addElement(MatchSummary.Break():create()) + local comment = mw.html.create('div') + :wikitext(game.comment) + :css('margin', 'auto') + row:addElement(comment) + end + + return row +end + +---@param showIcon boolean? +---@param iconType string? +---@return Html +function CustomMatchSummary._createCheckMarkOrCross(showIcon, iconType) + local container = mw.html.create('div'):addClass('brkts-popup-spaced'):css('line-height', '27px') + + if showIcon then + return container:node(iconType) + end + return container:node(Icons.EMPTY) +end + +return CustomMatchSummary diff --git a/components/match2/wikis/trackmania/match_group_input_custom.lua b/components/match2/wikis/trackmania/match_group_input_custom.lua index 8e4b9d2d016..6995e7f6a6b 100644 --- a/components/match2/wikis/trackmania/match_group_input_custom.lua +++ b/components/match2/wikis/trackmania/match_group_input_custom.lua @@ -8,399 +8,162 @@ local CustomMatchGroupInput = {} -local DateExt = require('Module:Date/Ext') +local Array = require('Module:Array') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Operator = require('Module:Operator') local Table = require('Module:Table') -local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') local Streams = require('Module:Links/Stream') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') local Opponent = Lua.import('Module:Opponent') -local STATUS_HAS_SCORE = 'S' -local STATUS_DEFAULT_WIN = 'W' -local ALLOWED_STATUSES = { STATUS_DEFAULT_WIN, 'FF', 'DQ', 'L' } -local STATUS_TO_WALKOVER = { FF = 'ff', DQ = 'dq', L = 'l' } -local MAX_NUM_OPPONENTS = 2 -local RESULT_TYPE_DRAW = 'draw' -local BYE_OPPONENT_NAME = 'bye' -local RESULT_TYPE_WALKOVER = 'default' -local WINNER_FIRST_OPPONENT = '0' -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) - -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} -- called from Module:MatchGroup ---@param match table ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) - Table.mergeInto( - match, - matchFunctions.readDate(match) - ) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - return match -end + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date, {'tournament_enddate'})) --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) - - return map -end + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) + local games = CustomMatchGroupInput.extractMaps(match, #opponents) ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() - - -- Convert byes to literals - if (opponent.template or ''):lower() == BYE_OPPONENT_NAME then - opponent = {type = Opponent.literal, name = 'BYE'} + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.extradata = CustomMatchGroupInput.getOpponentExtradata(opponent) + if opponent.extradata.additionalScores then + opponent.score = CustomMatchGroupInput._getSetWins(opponent) + end + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }) + end) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', '2v2')) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) + match.stream = Streams.processStreams(match) - --score2 & score3 support for every match - local score2 = tonumber(record.score2) - local score3 = tonumber(record.score3) - if score2 then - record.extradata = { - score2 = score2, - score3 = score3, - set1win = Logic.readBool(record.set1win), - set2win = Logic.readBool(record.set2win), - set3win = Logic.readBool(record.set3win), - additionalScores = true - } - end -end + match.games = games + match.opponents = opponents ----@param op1 table ----@param op2 table ----@param op1norm boolean ----@param op2norm boolean ----@return boolean -function CustomMatchGroupInput._sortOpponents(op1, op2, op1norm, op2norm) - if op1norm then return true - elseif op2norm then return false - elseif op1.status == STATUS_DEFAULT_WIN then return true - elseif Table.includes(ALLOWED_STATUSES, op1.status) then return false - elseif op2.status == STATUS_DEFAULT_WIN then return false - elseif Table.includes(ALLOWED_STATUSES, op2.status) then return true - else return true - end -end + match.extradata = MatchFunctions.getExtraData(match) --- --- --- function to sort out winner/placements ----@param opponents table[] ----@param opponentKey1 integer ----@param opponentKey2 integer ----@return boolean -function CustomMatchGroupInput._placementSortFunction(opponents, opponentKey1, opponentKey2) - local op1 = opponents[opponentKey1] - local op2 = opponents[opponentKey2] - local op1norm = op1.status == STATUS_HAS_SCORE - local op2norm = op2.status == STATUS_HAS_SCORE - if op1norm and op2norm then - local op1setwins = CustomMatchGroupInput._getSetWins(op1) - local op2setwins = CustomMatchGroupInput._getSetWins(op2) - if op1setwins + op2setwins > 0 then - return op1setwins > op2setwins - else - return tonumber(op1.score) > tonumber(op2.score) - end - else return CustomMatchGroupInput._sortOpponents(op1, op2, op1norm, op2norm) end + return match end ----@param opp table ----@return integer -function CustomMatchGroupInput._getSetWins(opp) - local extradata = opp.extradata or {} - local set1win = extradata.set1win and 1 or 0 - local set2win = extradata.set2win and 1 or 0 - local set3win = extradata.set3win and 1 or 0 - return set1win + set2win + set3win -end +---@param match table +---@param opponentCount integer +---@return table +function CustomMatchGroupInput.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) + end --- --- match related functions --- + table.insert(maps, map) + match[key] = nil + end ----@param matchArgs table ----@return {date: string, dateexact: boolean, timestamp: integer, timezoneId: string?, timezoneOffset: string?} -function matchFunctions.readDate(matchArgs) - return MatchGroupInput.readDate(matchArgs.date, {'tournament_enddate'}) + return maps end ----@param match table +---@param opponent table ---@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', '2v2')) - match.showh2h = Logic.emptyOr(match.showh2h, Variables.varDefault('showh2h')) - return MatchGroupInput.getCommonTournamentVars(match) -end +function CustomMatchGroupInput.getOpponentExtradata(opponent) + if not Logic.isNumeric(opponent.score2) then + return {} + end ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) + return { + score1 = tonumber(opponent.score), + score2 = tonumber(opponent.score2), + score3 = tonumber(opponent.score3), + set1win = Logic.readBool(opponent.set1win), + set2win = Logic.readBool(opponent.set2win), + set3win = Logic.readBool(opponent.set3win), + additionalScores = true + } +end - return match +---@param opponent table +---@return integer +function CustomMatchGroupInput._getSetWins(opponent) + local setWin = function(setIndex) + return opponent.extradata['set' .. setIndex .. 'win'] and 1 or 0 + end + return setWin(1) + setWin(2) + setWin(3) end ---@param match table ---@return boolean -function matchFunctions.isFeatured(match) +function MatchFunctions.isFeatured(match) return tonumber(match.liquipediatier) == 1 or tonumber(match.liquipediatier) == 2 end ---@param match table ---@return table -function matchFunctions.getExtraData(match) +function MatchFunctions.getExtraData(match) local opponent1 = match.opponent1 or {} local opponent2 = match.opponent2 or {} - local showh2h = Logic.readBool(match.showh2h) - and opponent1.type == Opponent.team - and opponent2.type == Opponent.team - - match.extradata = { - showh2h = showh2h, - isfeatured = matchFunctions.isFeatured(match), - casters = MatchGroupInput.readCasters(match), + return { + isfeatured = MatchFunctions.isFeatured(match), + casters = MatchGroupInputUtil.readCasters(match), hasopponent1 = Logic.isNotEmpty(opponent1.name) and opponent1.type ~= Opponent.literal, hasopponent2 = Logic.isNotEmpty(opponent2.name) and opponent2.type ~= Opponent.literal, } - return match -end - ----@param match table ----@return table[] ----@return boolean ----@return table -function matchFunctions.readOpponents(match) - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if TypeUtil.isNumeric(opponent.score) then - opponent.status = STATUS_HAS_SCORE - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - - --set Walkover from Opponent status - match.walkover = match.walkover or STATUS_TO_WALKOVER[opponent.status] - - opponents[opponentIndex] = opponent - - -- get players from vars for teams - if opponent.type == Opponent.team and not Logic.isEmpty(opponent.name) then - match = MatchGroupInput.readPlayersOfTeam(match, opponentIndex, opponent.name) - end - end - end - - return opponents, isScoreSet, match -end - ----@param opponents table[] ----@param match table ----@return table -function matchFunctions.applyMatchPlacement(opponents, match) - local placement = 1 - local lastScore - local lastPlacement = 1 - local lastStatus - for opponentIndex, opponent in Table.iter.spairs(opponents, CustomMatchGroupInput._placementSortFunction) do - if opponent.status ~= STATUS_HAS_SCORE and opponent.status ~= STATUS_DEFAULT_WIN and placement == 1 then - placement = 2 - elseif placement == 1 then - match.winner = opponentIndex - end - if opponent.status == STATUS_HAS_SCORE and opponent.score == lastScore then - opponent.placement = lastPlacement - elseif opponent.status ~= STATUS_HAS_SCORE and opponent.status == lastStatus then - opponent.placement = lastPlacement - else - opponent.placement = placement - end - match['opponent' .. opponentIndex] = opponent - placement = placement + 1 - lastScore = opponent.score - lastPlacement = opponent.placement - lastStatus = opponent.status - end - - return match end ----@param winner string|integer? ----@param opponents table[] ----@param match table ----@return table -function matchFunctions.setMatchWinner(winner, opponents, match) - if - winner == RESULT_TYPE_DRAW or - winner == WINNER_FIRST_OPPONENT or ( - Logic.readBool(match.finished) and - #opponents == MAX_NUM_OPPONENTS and - opponents[1].status == STATUS_HAS_SCORE and - opponents[2].status == STATUS_HAS_SCORE and - opponents[1].score == opponents[2].score - ) - then - match.winner = tonumber(WINNER_FIRST_OPPONENT) - match.resulttype = RESULT_TYPE_DRAW - elseif - Logic.readBool(match.finished) and - #opponents == MAX_NUM_OPPONENTS and - opponents[1].status ~= STATUS_HAS_SCORE and - opponents[1].status == opponents[2].status - then - match.winner = tonumber(WINNER_FIRST_OPPONENT) - end - - return match -end - ----@param match table ----@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents - local isScoreSet - opponents, isScoreSet, match = matchFunctions.readOpponents(match) - - --set resulttype to 'default' if walkover is set - if match.walkover then - match.resulttype = RESULT_TYPE_WALKOVER - end - - local autoFinished = Logic.readBool(Logic.emptyOr(match.autofinished, true)) - -- see if match should actually be finished if score is set - if isScoreSet and autoFinished and not Logic.readBool(match.finished) then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - local winner = tostring(match.winner or '') - if Logic.readBool(match.finished) then - match = matchFunctions.applyMatchPlacement(opponents, match) - -- only apply arg changes otherwise - else - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - end - - -- set the match winner - match = matchFunctions.setMatchWinner(winner, opponents, match) - return match -end - --- --- map related functions --- - ---@param map table ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { +function MapFunctions.getExtraData(map) + return { comment = map.comment, - header = map.header, overtime = Logic.readBool(map.overtime) } - return map -end - ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] - local obj = {} - if not Logic.isEmpty(score) then - if TypeUtil.isNumeric(score) then - score = tonumber(score) - obj.status = STATUS_HAS_SCORE - obj.score = score - obj.index = scoreIndex - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - obj.index = scoreIndex - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - if not Logic.isEmpty(indexedScores) then - map.winner = mapFunctions.getWinner(indexedScores) - end - - return map -end - ----@param indexedScores table[] ----@return integer? -function mapFunctions.getWinner(indexedScores) - table.sort(indexedScores, mapFunctions.mapWinnerSortFunction) - return indexedScores[1].index -end - ----@param op1 table ----@param op2 table ----@return boolean -function mapFunctions.mapWinnerSortFunction(op1, op2) - local op1norm = op1.status == STATUS_HAS_SCORE - local op2norm = op2.status == STATUS_HAS_SCORE - if op1norm and op2norm then - return tonumber(op1.score) > tonumber(op2.score) - else return CustomMatchGroupInput._sortOpponents(op1, op2, op1norm, op2norm) end end return CustomMatchGroupInput diff --git a/components/match2/wikis/trackmania/match_summary.lua b/components/match2/wikis/trackmania/match_summary.lua index 11786e1c31e..66792a6c15b 100644 --- a/components/match2/wikis/trackmania/match_summary.lua +++ b/components/match2/wikis/trackmania/match_summary.lua @@ -11,7 +11,6 @@ local Class = require('Module:Class') local Icon = require('Module:Icon') local Lua = require('Module:Lua') local String = require('Module:StringUtils') -local Table = require('Module:Table') local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper') local MatchSummary = Lua.import('Module:MatchSummary/Base') @@ -22,8 +21,6 @@ local GREEN_CHECK = Icon.makeIcon{iconName = 'winner', color = 'forest-green-tex local NO_CHECK = '[[File:NoCheck.png|link=]]' local OVERTIME = '[[File:Cooldown_Clock.png|14x14px|link=]]' -local HEADTOHEAD = '[[File:Match Info Stats.png|14x14px|link=%s|Head to Head history]]' - -- Custom Header Class ---@class TrackmaniaMatchSummaryHeader: MatchSummaryHeader ---@field leftElementAdditional Html @@ -150,35 +147,7 @@ end ---@param footer MatchSummaryFooter ---@return MatchSummaryFooter function CustomMatchSummary.addToFooter(match, footer) - footer = MatchSummary.addVodsToFooter(match, footer) - - return footer:addElement(match.extradata.showh2h and CustomMatchSummary._getHeadToHead(match) or nil) -end - ----@param match MatchGroupUtilMatch ----@return string -function CustomMatchSummary._getHeadToHead(match) - local opponents = match.opponents - local team1, team2 = mw.uri.encode(opponents[1].name), mw.uri.encode(opponents[2].name) - local buildQueryFormLink = function(form, template, arguments) - return tostring(mw.uri.fullUrl('Special:RunQuery/' .. form, - mw.uri.buildQueryString(Table.map(arguments, function(key, value) return template .. key, value end)) - .. '&_run' - )) - end - - local headtoheadArgs = { - ['[team1]'] = team1, - ['[team2]'] = team2, - ['[games][is_list]'] = 1, - ['[tiers][is_list]'] = 1, - ['[fromdate][day]'] = '01', - ['[fromdate][month]'] = '01', - ['[fromdate][year]'] = string.sub(match.date,1,4) - } - - local link = buildQueryFormLink('Head2head', 'Headtohead', headtoheadArgs) - return HEADTOHEAD:format(link) + return MatchSummary.addVodsToFooter(match, footer) end ---@param match MatchGroupUtilMatch diff --git a/components/match2/wikis/valorant/legacy/match_group_legacy_default.lua b/components/match2/wikis/valorant/legacy/match_group_legacy_default.lua index e0afcb2a1bf..060e4e546f9 100644 --- a/components/match2/wikis/valorant/legacy/match_group_legacy_default.lua +++ b/components/match2/wikis/valorant/legacy/match_group_legacy_default.lua @@ -54,4 +54,24 @@ function MatchGroupLegacyDefault.run(frame) return MatchGroupLegacyDefault(frame):build() end +---@param isReset boolean +---@param match table +function MatchGroupLegacyDefault:handleOtherMatchParams(isReset, match) + local opp1score, opp2score = (match.opponent1 or {}).score, (match.opponent2 or {}).score + -- Maps are >Bo9, while >Bo5 in legacy matches are non existent + -- Let's assume that if the sum of the scores is less than 10, it's a match, otherwise it's a map + if (tonumber(opp1score) or 0) + (tonumber(opp2score) or 0) < 10 then + return + end + + (match.opponent1 or {}).score = nil + (match.opponent2 or {}).score = nil + match.map1 = match.map1 or { + map = 'Unknown', + finished = true, + score1 = opp1score, + score2 = opp2score, + } +end + return MatchGroupLegacyDefault diff --git a/components/match2/wikis/valorant/legacy/match_maps_legacy.lua b/components/match2/wikis/valorant/legacy/match_maps_legacy.lua index a3df726ea92..e894529352c 100644 --- a/components/match2/wikis/valorant/legacy/match_maps_legacy.lua +++ b/components/match2/wikis/valorant/legacy/match_maps_legacy.lua @@ -259,6 +259,20 @@ function MatchMapsLegacy.convertMatch(frame) args = MatchMapsLegacy._setHeaderIfEmpty(args, details) args = MatchMapsLegacy._copyDetailsToArgs(args, details) + local opp1score, opp2score = args.opponent1.score, args.opponent2.score + -- Maps are >Bo9, while >Bo5 in legacy matches are non existent + -- Let's assume that if the sum of the scores is less than 10, it's a match, otherwise it's a map + if (tonumber(opp1score) or 0) + (tonumber(opp2score) or 0) >= 10 then + args.opponent1.score = nil + args.opponent2.score = nil + args.map1 = args.map1 or { + map = 'Unknown', + finished = true, + score1 = opp1score, + score2 = opp2score, + } + end + Template.stashReturnValue(args, 'LegacyMatchlist') return mw.html.create('div'):css('display', 'none') end diff --git a/components/match2/wikis/valorant/match_group_input_custom.lua b/components/match2/wikis/valorant/match_group_input_custom.lua index ab9083556c0..6ff61262d51 100644 --- a/components/match2/wikis/valorant/match_group_input_custom.lua +++ b/components/match2/wikis/valorant/match_group_input_custom.lua @@ -40,12 +40,12 @@ function CustomMatchGroupInput.processMatch(match, options) local opponents = Array.mapIndexes(function(opponentIndex) return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) end) - local games = CustomMatchGroupInput.extractMaps(match, #opponents) + local games = CustomMatchGroupInput.extractMaps(match, opponents) match.bestof = MatchGroupInputUtil.getBestOf(nil, games) games = MatchFunctions.removeUnsetMaps(games) local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) - and MatchFunctions.calculateMatchScore(games, match.bestof) + and MatchFunctions.calculateMatchScore(games) or nil Array.forEach(opponents, function(opponent, opponentIndex) opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ @@ -65,7 +65,8 @@ function CustomMatchGroupInput.processMatch(match, options) MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) match.stream = Streams.processStreams(match) match.links = MatchFunctions.getLinks(match) @@ -80,19 +81,19 @@ end ---@param match table ----@param opponentCount integer +---@param opponents MGIParsedOpponent[] ---@return table[] -function CustomMatchGroupInput.extractMaps(match, opponentCount) +function CustomMatchGroupInput.extractMaps(match, opponents) local maps = {} for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do local finishedInput = map.finished --[[@as string?]] local winnerInput = map.winner --[[@as string?]] - map.participants = MapFunctions.getParticipants(map, opponentCount) - map.extradata = MapFunctions.getExtraData(map, map.participants) + map.opponents = MapFunctions.getParticipants(map, opponents) + map.extradata = MapFunctions.getExtraData(map, map.opponents) map.finished = MatchGroupInputUtil.mapIsFinished(map) - local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local opponentInfo = Array.map(opponents, function(_, opponentIndex) local score, status = MatchGroupInputUtil.computeOpponentScore({ walkover = map.walkover, winner = map.winner, @@ -116,8 +117,6 @@ function CustomMatchGroupInput.extractMaps(match, opponentCount) return maps end -CustomMatchGroupInput.processMap = FnUtil.identity - -- -- match related functions -- @@ -131,33 +130,14 @@ function MatchFunctions.removeUnsetMaps(games) return Array.filter(games, MapFunctions.keepMap) end --- Calculate the match scores based on the map results. --- If it's a Best of 1, we'll take the exact score of that map --- If it's not a Best of 1, we should count the map wins ---@param maps table[] ----@param bestOf integer ---@return fun(opponentIndex: integer): integer? -function MatchFunctions.calculateMatchScore(maps, bestOf) +function MatchFunctions.calculateMatchScore(maps) return function(opponentIndex) - if bestOf == 1 then - if not maps[1] or not maps[1].scores then - return - end - return maps[1].scores[opponentIndex] - end return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end end ----@param match table ----@return table -function MatchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) - match.patch = Logic.emptyOr(match.patch, Variables.varDefault('patch')) - - return MatchGroupInputUtil.getCommonTournamentVars(match) -end - ---@param match table ---@return table function MatchFunctions.getLinks(match) @@ -188,8 +168,8 @@ function MapFunctions.keepMap(map) end ---@param map table ----@param participants table ----@return table +---@param participants {players: {player: string?, agent: string?}[]}[] +---@return table function MapFunctions.getExtraData(map, participants) ---@type table local extraData = { @@ -199,40 +179,52 @@ function MapFunctions.getExtraData(map, participants) t2halfs = {atk = map.t2atk, def = map.t2def, otatk = map.t2otatk, otdef = map.t2otdef}, } - for key, participant in pairs(participants) do - local opponentIdx, playerIdx = unpack(mw.text.split(key, '_', true)) - extraData['t' .. opponentIdx .. 'p' .. playerIdx] = participant.player - extraData['t' .. opponentIdx .. 'p' .. playerIdx .. 'agent'] = participant.agent + for opponentIdx, opponent in ipairs(participants) do + for playerIdx, player in pairs(opponent.players) do + extraData['t' .. opponentIdx .. 'p' .. playerIdx] = player.player + extraData['t' .. opponentIdx .. 'p' .. playerIdx .. 'agent'] = player.agent + end end return extraData end ---@param map table ----@param opponentCount integer ----@return table -function MapFunctions.getParticipants(map, opponentCount) - local participants = {} +---@param opponents MGIParsedOpponent[] +---@return {players: table[]}[] +function MapFunctions.getParticipants(map, opponents) local getCharacterName = FnUtil.curry(MatchGroupInputUtil.getCharacterName, AgentNames) - for opponentIdx = 1, opponentCount do - for _, stats, playerIdx in Table.iter.pairsByPrefix(map, 't' .. opponentIdx .. 'p', {requireIndex = true}) do - stats = Json.parseIfString(stats) - - local participant = { - kills = stats.kills, - deaths = stats.deaths, - assists = stats.assists, - acs = stats.acs, - player = stats.player, - agent = getCharacterName(stats.agent), - } - - participants[opponentIdx .. '_' .. playerIdx] = participant - end - end - - return participants + return Array.map(opponents, function(opponent, opponentIndex) + local players = Array.mapIndexes(function(playerIndex) + return opponent.match2players[playerIndex] or + (map['t' .. opponentIndex .. 'p' .. playerIndex] and {}) or + nil + end) + local participants, unattachedParticipants = MatchGroupInputUtil.parseParticipants( + opponent.match2players, + players, + function(playerIndex) + local data = Json.parseIfString(map['t' .. opponentIndex .. 'p' .. playerIndex]) + return data and {name = data.player} or nil + end, + function(playerIndex, playerIdData, playerInputData) + local stats = Json.parseIfString(map['t'.. opponentIndex .. 'p' .. playerIndex]) or {} + return { + kills = stats.kills, + deaths = stats.deaths, + assists = stats.assists, + acs = stats.acs, + player = playerIdData.name or playerInputData.name, + agent = getCharacterName(stats.agent), + } + end + ) + Array.forEach(unattachedParticipants, function(participant) + table.insert(participants, participant) + end) + return {players = participants} + end) end ---@param map table diff --git a/components/match2/wikis/valorant/match_legacy.lua b/components/match2/wikis/valorant/match_legacy.lua index 5008ac4565e..4c059488345 100644 --- a/components/match2/wikis/valorant/match_legacy.lua +++ b/components/match2/wikis/valorant/match_legacy.lua @@ -72,20 +72,24 @@ function MatchLegacy.storeGames(match, match2) game.extradata.opponent2scores = table.concat(team2, ', ') end end - local participants = Json.parseIfString(game2.participants) - if participants then - for team = 1, 2 do - for player = 1, 5 do - local data = participants[team..'_'..player] - if data then - local playerPage = data.player and mw.ext.TeamLiquidIntegration.resolve_redirect(data.player) or '' - game.extradata['t'..team..'p'..player] = playerPage - if data.kills and data.deaths and data.assists then - game.extradata['t'..team..'kda'..player] = data.kills..'/'..data.deaths..'/'..data.assists - end - game.extradata['t'..team..'acs'..player] = data.acs - game.extradata['t'..team..'a'..player] = data.agent - end + local addPlayer = function (teamId, playerId, data) + if data then + local playerPage = data.player and mw.ext.TeamLiquidIntegration.resolve_redirect(data.player) or '' + game.extradata['t' .. teamId .. 'p' .. playerId] = playerPage + if data.kills and data.deaths and data.assists then + game.extradata['t' .. teamId .. 'kda' .. playerId] = data.kills .. '/' .. data.deaths .. '/' .. data.assists + end + game.extradata['t' .. teamId .. 'acs' .. playerId] = data.acs + game.extradata['t' .. teamId .. 'a' .. playerId] = data.agent + end + end + local opponents = Json.parseIfString(game2.opponents) or {} + for teamId, opponent in ipairs(opponents) do + local counter = 0 + for _, player in pairs(opponent.players) do + if Logic.isNotEmpty(player) then + counter = counter + 1 + addPlayer(teamId, counter, player) end end end @@ -95,12 +99,11 @@ function MatchLegacy.storeGames(match, match2) game.opponent1flag = match.opponent1flag game.opponent2flag = match.opponent2flag game.date = match.date - local scores = game2.scores or {} - if type(scores) == 'string' then - scores = Json.parse(scores) - end + + local scores = Json.parseIfString(game2.scores) or {} game.opponent1score = scores[1] or 0 game.opponent2score = scores[2] or 0 + local res = mw.ext.LiquipediaDB.lpdb_game( 'legacygame_' .. match2.match2id .. gameIndex, Json.stringifySubTables(game) @@ -182,7 +185,14 @@ function MatchLegacy._convertParameters(match2) local opponentmatch2players = opponent.match2players or {} if opponent.type == 'team' then match[prefix] = opponent.name - match[prefix..'score'] = tonumber(opponent.score) or 0 >= 0 and opponent.score or 0 + if match2.bestof == 1 then + if ((match2.match2games or {})[1] or {}).scores then + match[prefix..'score'] = Json.parseIfString(match2.match2games[1].scores)[index] + end + end + if not match[prefix..'score'] then + match[prefix..'score'] = (tonumber(opponent.score) or 0) > 0 and opponent.score or 0 + end local opponentplayers = {} for i = 1, 5 do local player = opponentmatch2players[i] or {} diff --git a/components/match2/wikis/valorant/match_summary.lua b/components/match2/wikis/valorant/match_summary.lua index 01ced7729f8..0620e666f1a 100644 --- a/components/match2/wikis/valorant/match_summary.lua +++ b/components/match2/wikis/valorant/match_summary.lua @@ -17,9 +17,6 @@ local Table = require('Module:Table') local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper') local MatchSummary = Lua.import('Module:MatchSummary/Base') -local ARROW_LEFT = '[[File:Arrow sans left.svg|15x15px|link=|Left team starts]]' -local ARROW_RIGHT = '[[File:Arrow sans right.svg|15x15px|link=|Right team starts]]' - local GREEN_CHECK = Icon.makeIcon{iconName = 'winner', color = 'forest-green-text', size = '110%'} local NO_CHECK = '[[File:NoCheck.png|link=]]' @@ -55,7 +52,7 @@ function Agents:setRight() return self end ----@param agent string +---@param agent string? ---@return self function Agents:add(agent) if Logic.isEmpty(agent) then @@ -150,132 +147,6 @@ function Score:create() self.table:node(self.top):node(self.bottom) return self.root end --- Map Veto Class ----@class ValorantMapVeto: MatchSummaryRowInterface ----@operator call: ValorantMapVeto ----@field root Html ----@field table Html -local MapVeto = Class.new( - function(self) - self.root = mw.html.create('div'):addClass('brkts-popup-mapveto') - self.table = self.root:tag('table') - :addClass('wikitable-striped'):addClass('collapsible'):addClass('collapsed') - self:createHeader() - end -) - ----@return self -function MapVeto:createHeader() - self.table:tag('tr') - :tag('th'):css('width','33%'):done() - :tag('th'):css('width','34%'):wikitext('Map Veto'):done() - :tag('th'):css('width','33%'):done() - return self -end - ----@param firstVeto number? ----@return self -function MapVeto:vetoStart(firstVeto) - local textLeft - local textCenter - local textRight - if firstVeto == 1 then - textLeft = 'Start Map Veto' - textCenter = ARROW_LEFT - elseif firstVeto == 2 then - textCenter = ARROW_RIGHT - textRight = 'Start Map Veto' - else return self end - self.table:tag('tr'):addClass('brkts-popup-mapveto-vetostart') - :tag('th'):wikitext(textLeft or ''):done() - :tag('th'):wikitext(textCenter):done() - :tag('th'):wikitext(textRight or ''):done() - return self -end - ----@param map string? ----@return self -function MapVeto:addDecider(map) - if Logic.isEmpty(map) then - map = 'TBD' - else - map = '[[' .. map .. ']]' - end - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - self:addColumnVetoMap(row, map) - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - - self.table:node(row) - return self -end - ----@param vetotype string? ----@param map1 string? ----@param map2 string? ----@return self -function MapVeto:addRound(vetotype, map1, map2) - if Logic.isEmpty(map1) then - map1 = 'TBD' - else - map1 = '[[' .. map1 .. ']]' - end - if Logic.isEmpty(map2) then - map2 = 'TBD' - else - map2 = '[[' .. map2 .. ']]' - end - local class - local vetoText - if vetotype == 'ban' then - vetoText = 'BAN' - class = 'brkts-popup-mapveto-ban' - elseif vetotype == 'pick' then - vetoText = 'PICK' - class = 'brkts-popup-mapveto-pick' - elseif vetotype == 'defaultban' then - vetoText = 'DEFAULT BAN' - class = 'brkts-popup-mapveto-defaultban' - else - return self - end - - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoMap(row, map1) - self:addColumnVetoType(row, class, vetoText) - self:addColumnVetoMap(row, map2) - - self.table:node(row) - return self -end - ----@param row Html ----@param styleClass string? ----@param vetoText string ----@return self -function MapVeto:addColumnVetoType(row, styleClass, vetoText) - row:tag('td') - :tag('span') - :addClass(styleClass) - :addClass('brkts-popup-mapveto-vetotype') - :wikitext(vetoText) - return self -end - ----@param row Html ----@param map string ----@return self -function MapVeto:addColumnVetoMap(row, map) - row:tag('td'):wikitext(map):done() - return self -end - ----@return Html -function MapVeto:create() - return self.root -end local CustomMatchSummary = {} @@ -329,25 +200,7 @@ function CustomMatchSummary.createBody(match) end -- Add Map Veto - if match.extradata.mapveto then - local vetoData = match.extradata.mapveto - if vetoData then - local mapVeto = MapVeto() - - for _, vetoRound in ipairs(vetoData) do - if vetoRound.vetostart then - mapVeto:vetoStart(tonumber(vetoRound.vetostart)) - end - if vetoRound.type == 'decider' then - mapVeto:addDecider(vetoRound.decider) - else - mapVeto:addRound(vetoRound.type, vetoRound.team1, vetoRound.team2) - end - end - - body:addRow(mapVeto) - end - end + body:addRow(MatchSummary.defaultMapVetoDisplay(match)) body:addRow(MatchSummary.makeCastersRow(match.extradata.casters)) @@ -359,33 +212,18 @@ end function CustomMatchSummary._createMap(game) local row = MatchSummary.Row() - local team1Agents, team2Agents - - if not Table.isEmpty(game.participants) then - team1Agents = Agents():setLeft() - team2Agents = Agents():setRight() - - for player = 1, 5 do - local playerStats = game.participants['1_' .. player] - if playerStats ~= nil then - team1Agents:add(playerStats['agent']) - end - end - - for player = 1, 5 do - local playerStats = game.participants['2_' .. player] - if playerStats ~= nil then - team2Agents:add(playerStats['agent']) - end - end - + local team1Agents = Agents():setLeft() + local team2Agents = Agents():setRight() + for _, playerStats in ipairs((game.opponents[1] or {}).players) do + team1Agents:add(playerStats.agent) + end + for _, playerStats in ipairs((game.opponents[2] or {}).players) do + team2Agents:add(playerStats.agent) end - - local score1, score2 local extradata = game.extradata or {} - score1 = Score():setLeft() - score2 = Score():setRight() + local score1 = Score():setLeft() + local score2 = Score():setRight() score1:setMapScore(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) diff --git a/components/match2/wikis/warcraft/brkts_wiki_specific.lua b/components/match2/wikis/warcraft/brkts_wiki_specific.lua index f3ffa7e8e23..8866d35f830 100644 --- a/components/match2/wikis/warcraft/brkts_wiki_specific.lua +++ b/components/match2/wikis/warcraft/brkts_wiki_specific.lua @@ -19,8 +19,6 @@ WikiSpecific.matchFromRecord = FnUtil.lazilyDefineFunction(function() return CustomMatchGroupUtil.matchFromRecord end) -WikiSpecific.processMap = FnUtil.identity - ---Determine if a match has details that should be displayed via popup ---@param match table ---@return boolean diff --git a/components/match2/wikis/warcraft/legacy/legacy_match_maps.lua b/components/match2/wikis/warcraft/legacy/legacy_match_maps.lua index ce5ab030285..38dd4b0736f 100644 --- a/components/match2/wikis/warcraft/legacy/legacy_match_maps.lua +++ b/components/match2/wikis/warcraft/legacy/legacy_match_maps.lua @@ -144,6 +144,7 @@ function LegacyMatchMaps._readMaps(args) end) map.vod = args['vodgame' .. mapIndex] args['vodgame' .. mapIndex] = nil + args[prefix .. 'finished'] = true if Table.isNotEmpty(map) then args[prefix] = map diff --git a/components/match2/wikis/warcraft/legacy/match_group_legacy_convert_map_data.lua b/components/match2/wikis/warcraft/legacy/match_group_legacy_convert_map_data.lua index 97e0e02a084..d18d5119754 100644 --- a/components/match2/wikis/warcraft/legacy/match_group_legacy_convert_map_data.lua +++ b/components/match2/wikis/warcraft/legacy/match_group_legacy_convert_map_data.lua @@ -134,6 +134,7 @@ function ConvertMapData._convertSubmatch(opponentPlayers, parsedArgs, args, pref local parsedPrefix = 'map' .. mapIndex parsedArgs[parsedPrefix] = map parsedArgs[parsedPrefix .. 'subgroup'] = submatchIndex + parsedArgs[parsedPrefix .. 'finished'] = true local winner = tonumber(args[prefix .. 'win' .. submatchMapIndex]) parsedArgs[parsedPrefix .. 'win'] = winner @@ -169,6 +170,7 @@ function ConvertMapData._convertSubmatch(opponentPlayers, parsedArgs, args, pref parsedArgs[parsedPrefix .. 'p2score'] = score2 parsedArgs[parsedPrefix .. 'walkover'] = score1 == DEFAULT_WIN and 1 or score2 == DEFAULT_WIN and 2 or nil parsedArgs[parsedPrefix] = submatchIsNotDefaultWin and 'Submatch Score Fix' or 'Submatch' + parsedArgs[parsedPrefix .. 'finished'] = true Array.forEach(playersArrays, function(players, opponentIndex) Array.forEach(players, function(player, playerIndex) diff --git a/components/match2/wikis/warcraft/legacy/match_group_legacy_default.lua b/components/match2/wikis/warcraft/legacy/match_group_legacy_default.lua index 1c8e8e767d8..316fab4ab4e 100644 --- a/components/match2/wikis/warcraft/legacy/match_group_legacy_default.lua +++ b/components/match2/wikis/warcraft/legacy/match_group_legacy_default.lua @@ -8,11 +8,14 @@ local Array = require('Module:Array') local Class = require('Module:Class') +local Logic = require('Module:Logic') local Lua = require('Module:Lua') local MatchGroupLegacy = Lua.import('Module:MatchGroup/Legacy') local MAX_NUM_PLAYERS_IN_TEAM_SUBMATCH = 4 +local TBD = 'TBD' +local SKIP = 'skip' ---@class WarcraftMatchGroupLegacyDefault: MatchGroupLegacy local MatchGroupLegacyDefault = Class.new(MatchGroupLegacy) @@ -25,14 +28,36 @@ function MatchGroupLegacyDefault:getOpponent(prefix, scoreKey) ['$notEmpty$'] = self.bracketType == 'team' and (prefix .. 'team') or prefix, name = prefix, template = prefix .. 'team', - p1flag = prefix .. 'flag', - p1race = prefix .. 'race', - p1link = prefix .. 'link', + flag = prefix .. 'flag', + race = prefix .. 'race', + link = prefix .. 'link', score = prefix .. scoreKey, win = prefix .. 'win' } end +---@param isReset boolean +---@param match1params match1Keys +---@param match table +function MatchGroupLegacyDefault:handleOpponents(isReset, match1params, match) + local scoreKey = isReset and 'score2' or 'score' + Array.forEach(Array.range(1, 2), function (opponentIndex) + local opp = self:getOpponent(match1params['opp' .. opponentIndex], scoreKey) + local notEmptyPrefix = opp['$notEmpty$'] + + if Logic.isNotEmpty(self.args[notEmptyPrefix]) then + match['opponent' .. opponentIndex] = self:readOpponent(opp) + return + elseif self.bracketType ~= 'solo' then + return + elseif Logic.isEmpty(self.args[notEmptyPrefix .. 'flag']) and Logic.isEmpty(self.args[notEmptyPrefix .. 'race']) then + return + end + + match['opponent' .. opponentIndex] = self:readOpponent(opp) + end) +end + ---@return table function MatchGroupLegacyDefault:getMap() local map = { @@ -50,6 +75,7 @@ function MatchGroupLegacyDefault:getMap() vod = 'vodgame$1$', subgroup = 'map$1$subgroup', walkover = 'map$1$walkover', + finished = 'map$1$finished', } if self.bracketType == 'team' then @@ -71,12 +97,25 @@ end ---@param opponentData table ---@return table function MatchGroupLegacyDefault:readOpponent(opponentData) + opponentData['$notEmpty$'] = nil local opponent = self:_copyAndReplace(opponentData, self.args) if self.bracketType == 'solo' then - opponent[1] = opponent.name + opponent[1] = opponent.name or TBD opponent.name = nil end opponent.type = self.bracketType + + -- `score=skip` can have different meaning + --- walkover with it being input for winner + --- walkover with it being input for loser + --- not played with only 1 opponent in the match (usually it being input on the player that was in the match) + --- not played with it being input for any of the 2 players + --- double walkover with it being input for any of the 2 players + -- in the old brackets it did not have any display at all, hence nil it here + if string.lower(opponent.score or '') == SKIP then + opponent.score = nil + end + return opponent end diff --git a/components/match2/wikis/warcraft/match_group_input_custom.lua b/components/match2/wikis/warcraft/match_group_input_custom.lua index a774f0a2e14..5574335d52b 100644 --- a/components/match2/wikis/warcraft/match_group_input_custom.lua +++ b/components/match2/wikis/warcraft/match_group_input_custom.lua @@ -10,742 +10,463 @@ local Array = require('Module:Array') local Faction = require('Module:Faction') local Flags = require('Module:Flags') local HeroData = mw.loadData('Module:HeroData') -local Json = require('Module:Json') local Logic = require('Module:Logic') local Lua = require('Module:Lua') local MapsData = mw.loadData('Module:Maps/data') +local Operator = require('Module:Operator') local PatchAuto = require('Module:PatchAuto') local String = require('Module:StringUtils') local Table = require('Module:Table') local Variables = require('Module:Variables') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') +local OpponentLibraries = require('Module:OpponentLibraries') +local Opponent = OpponentLibraries.Opponent local Streams = Lua.import('Module:Links/Stream') -local OpponentLibrary = require('Module:OpponentLibraries') -local Opponent = OpponentLibrary.Opponent - -local ALLOWED_STATUSES = {'W', 'FF', 'DQ', 'L'} -local CONVERT_STATUS_INPUT = {W = 'W', FF = 'FF', L = 'L', DQ = 'DQ', ['-'] = 'L'} -local DEFAULT_WIN_STATUS = 'W' -local SCORE_STATUS = 'S' -local MAX_NUM_OPPONENTS = 2 -local DEFAULT_BEST_OF = 99 -local LINKS_KEYS = {'preview', 'preview2', 'interview', 'interview2', 'review', 'recap'} -local MODE_MIXED = 'mixed' -local TBD = 'tbd' +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, +} +local TBD = 'TBD' local NEUTRAL_HERO_FACTION = 'neutral' -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local MODE_MIXED = 'mixed' + +---@class WarcraftParticipant +---@field player string +---@field faction string? +---@field heroes string[]? +---@field position integer? +---@field flag string? +---@field random boolean? local CustomMatchGroupInput = {} +local MatchFunctions = {} +local MapFunctions = {} ---- called from Module:MatchGroup ---@param match table ---@param options table? ---@return table function CustomMatchGroupInput.processMatch(match, options) assert(not Logic.readBool(match.ffa), 'FFA is not yet supported in warcraft match2') - Table.mergeInto( - match, - CustomMatchGroupInput._readDate(match) - ) + Table.mergeInto(match, MatchFunctions.readDate(match)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, OPPONENT_CONFIG) + end) + match.patch = PatchAuto.retrieve{date = match.date} - CustomMatchGroupInput._getTournamentVars(match) - CustomMatchGroupInput._adjustData(match) - CustomMatchGroupInput._updateFinished(match) + + Array.forEach(opponents, function(opponent) + opponent.extradata = opponent.extradata or {} + Table.mergeInto(opponent.extradata, MatchFunctions.getOpponentExtradata(opponent)) + -- make sure match2players is not nil to avoid indexing nil + opponent.match2players = opponent.match2players or {} + Array.forEach(opponent.match2players, function(player) + player.extradata = player.extradata or {} + player.extradata.faction = MatchFunctions.getPlayerFaction(player) + end) + end) + + local games = MatchFunctions.extractMaps(match, opponents) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games, opponents) + or nil + + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) + + match.mode = MatchFunctions.getMode(opponents) + + match.bestof = MatchFunctions.getBestOf(match.bestof) + local cancelled = Logic.readBool(Logic.emptyOr(match.cancelled, Variables.varDefault('cancelled tournament'))) + if cancelled then + match.finished = match.finished or 'skip' + end + + local winnerInput = match.winner --[[@as string?]] + local finishedInput = match.finished --[[@as string?]] + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) + end + + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) + match.stream = Streams.processStreams(match) - CustomMatchGroupInput._getLinks(match) - CustomMatchGroupInput._getExtraData(match) + match.vod = Logic.nilIfEmpty(match.vod) + match.links = MatchFunctions.getLinks(match) + match.extradata = MatchFunctions.getExtraData(match, #games) + + match.games = games + match.opponents = opponents return match end ---@param matchArgs table ----@return table -function CustomMatchGroupInput._readDate(matchArgs) +---@return {date: string, dateexact: boolean, timestamp: integer, timezoneId: string?, timezoneOffset: string?} +function MatchFunctions.readDate(matchArgs) local suggestedDate = Variables.varDefault('matchDate') local tournamentStartTime = Variables.varDefault('tournament_starttimeraw') if matchArgs.date or (not suggestedDate and tournamentStartTime) then - local dateProps = MatchGroupInput.readDate(matchArgs.date or tournamentStartTime) + local dateProps = MatchGroupInputUtil.readDate(matchArgs.date or tournamentStartTime) dateProps.dateexact = Logic.nilOr( Logic.readBoolOrNil(matchArgs.dateexact), matchArgs.date and dateProps.dateexact or false ) - Variables.varDefine('matchDate', dateProps.date) + Variables.varDefine('match_date', dateProps.date) return dateProps end - return MatchGroupInput.readDate(nil, { - 'matchDate', + return MatchGroupInputUtil.readDate(nil, { + 'match_date', 'tournament_startdate', 'tournament_enddate', }) end ---@param match table -function CustomMatchGroupInput._updateFinished(match) - match.finished = Logic.nilOr(Logic.readBoolOrNil(match.finished), Logic.isNotEmpty(match.winner)) - if match.finished then - return - end +---@param opponents table[] +---@return table[] +function MatchFunctions.extractMaps(match, opponents) + local maps = {} + local subGroup = 0 + for mapKey, mapInput, mapIndex in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local map + map, subGroup = MapFunctions.readMap(mapInput, subGroup, #opponents) - -- Match is automatically marked finished upon page edit after a - -- certain amount of time (depending on whether the date is exact) - local threshold = match.dateexact and 30800 or 86400 - match.finished = match.timestamp + threshold < NOW -end + map.participants = MapFunctions.getParticipants(mapInput, opponents) ----@param match table -function CustomMatchGroupInput._getTournamentVars(match) - match.cancelled = Logic.emptyOr(match.cancelled, Variables.varDefault('cancelled tournament', 'false')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - match.bestof = tonumber(Logic.emptyOr(match.bestof, Variables.varDefault('bestof'))) - Variables.varDefine('bestof', match.bestof) + map.mode = MapFunctions.getMode(mapInput, map.participants, opponents) - MatchGroupInput.getCommonTournamentVars(match) -end - ----@param match table -function CustomMatchGroupInput._getLinks(match) - match.links = {} - for _, linkKey in pairs(LINKS_KEYS) do - match.links[linkKey] = match[linkKey] - end -end + Table.mergeInto(map.extradata, MapFunctions.getAdditionalExtraData(map, map.participants)) ----@param match table -function CustomMatchGroupInput._getExtraData(match) - match.extradata = { - casters = MatchGroupInput.readCasters(match), - ffa = 'false', - } + map.vod = Logic.emptyOr(mapInput.vod, match['vodgame' .. mapIndex]) - for prefix, mapVeto in Table.iter.pairsByPrefix(match, 'veto') do - match.extradata[prefix] = (MapsData[mapVeto:lower()] or {}).name or mapVeto - match.extradata[prefix .. 'by'] = match[prefix .. 'by'] + table.insert(maps, map) + match[mapKey] = nil end - Table.mergeInto(match.extradata, Table.filterByKey(match, function(key, value) - return key:match('subgroup%d+header') end)) + return maps end ----@param match table -function CustomMatchGroupInput._adjustData(match) - --parse opponents + set base sumscores + set mode - CustomMatchGroupInput._opponentInput(match) - - --main processing done here - local subGroupIndex = 0 - for _, _, mapIndex in Table.iter.pairsByPrefix(match, 'map') do - subGroupIndex = CustomMatchGroupInput._mapInput(match, mapIndex, subGroupIndex) +---@param maps table[] +---@param opponents table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps, opponents) + return function(opponentIndex) + local calculatedScore = MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + if not calculatedScore then return end + local opponent = opponents[opponentIndex] + return calculatedScore + (opponent.extradata.advantage or 0) - (opponent.extradata.penalty or 0) end - - CustomMatchGroupInput._matchWinnerProcessing(match) end ----@param match table -function CustomMatchGroupInput._matchWinnerProcessing(match) - local bestof = match.bestof or DEFAULT_BEST_OF - local walkover = match.walkover - local numberofOpponents = 0 - for opponentIndex = 1, MAX_NUM_OPPONENTS do - local opponent = match['opponent' .. opponentIndex] - if Logic.isNotEmpty(opponent) then - numberofOpponents = numberofOpponents + 1 - --determine opponent scores, status and placement - --determine MATCH winner, resulttype and walkover - --the following ignores the possibility of > 2 opponents - --as > 2 opponents is only possible in ffa - if Logic.isNotEmpty(walkover) then - if Logic.isNumeric(walkover) then - local numericWalkover = tonumber(walkover) - if numericWalkover == opponentIndex then - match.winner = opponentIndex - match.walkover = 'L' - opponent.status = 'W' - elseif numericWalkover == 0 then - match.winner = 0 - match.walkover = 'L' - opponent.status = 'L' - else - local score = string.upper(opponent.score or '') - opponent.status = CONVERT_STATUS_INPUT[score] or 'L' - end - elseif Table.includes(ALLOWED_STATUSES, string.upper(walkover)) then - if tonumber(match.winner or 0) == opponentIndex then - opponent.status = 'W' - else - opponent.status = CONVERT_STATUS_INPUT[string.upper(walkover)] or 'L' - end - else - local score = string.upper(opponent.score or '') - opponent.status = CONVERT_STATUS_INPUT[score] or 'L' - match.walkover = 'L' - end - opponent.score = -1 - match.finished = true - match.resulttype = 'default' - elseif CONVERT_STATUS_INPUT[string.upper(opponent.score or '')] then - if string.upper(opponent.score) == 'W' then - match.winner = opponentIndex - match.resulttype = 'default' - match.finished = true - opponent.score = -1 - opponent.status = 'W' - else - match.resulttype = 'default' - match.finished = true - match.walkover = CONVERT_STATUS_INPUT[string.upper(opponent.score)] - local score = string.upper(opponent.score) - opponent.status = CONVERT_STATUS_INPUT[score] - opponent.score = -1 - end - else - opponent.status = SCORE_STATUS - opponent.score = tonumber(opponent.score) or - tonumber(opponent.sumscore) or -1 - if opponent.score > bestof / 2 then - match.finished = Logic.emptyOr(match.finished, true) - match.winner = tonumber(match.winner or '') or opponentIndex - end - end - - if Logic.readBool(match.cancelled) then - match.finished = true - if String.isEmpty(match.resulttype) and Logic.isEmpty(opponent.score) then - match.resulttype = 'np' - opponent.score = opponent.score or -1 - end - end - else - break - end - end - - CustomMatchGroupInput._determineWinnerIfMissing(match) - - for opponentIndex = 1, numberofOpponents do - local opponent = match['opponent' .. opponentIndex] - if match.winner == 'draw' or tonumber(match.winner) == 0 or - (match.opponent1.score == bestof / 2 and match.opponent1.score == match.opponent2.score) then - match.finished = true - match.winner = 0 - match.resulttype = 'draw' - end - - if tonumber(match.winner) == opponentIndex or - match.resulttype == 'draw' then - opponent.placement = 1 - elseif Logic.isNumeric(match.winner) then - opponent.placement = 2 - end - end -end - ----@param match table +---@param opponent table ---@return table -function CustomMatchGroupInput._determineWinnerIfMissing(match) - if Logic.readBool(match.finished) and Logic.isEmpty(match.winner) then - local scores = Array.mapIndexes(function(opponentIndex) - local opponent = match['opponent' .. opponentIndex] - if not opponent then - return nil - end - return match['opponent' .. opponentIndex].score or -1 end - ) - local maxScore = math.max(unpack(scores) or 0) - -- if we have a positive score and the match is finished we also have a winner - if maxScore > 0 then - local maxIndexFound = false - for opponentIndex, score in pairs(scores) do - if maxIndexFound and score == maxScore then - match.winner = 0 - break - elseif score == maxScore then - maxIndexFound = true - match.winner = opponentIndex - end - end - end - end - - return match +function MatchFunctions.getOpponentExtradata(opponent) + return { + advantage = tonumber(opponent.advantage), + penalty = tonumber(opponent.penalty), + score2 = opponent.score2, + } end ---OpponentInput functions - ----@param match table ----@return table -function CustomMatchGroupInput._opponentInput(match) - local opponentTypes = {} - - for opponentKey, opponent, opponentIndex in Table.iter.pairsByPrefix(match, 'opponent') do - opponent = Json.parseIfString(opponent) - - --Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - -- Opponent processing (first part) - -- Sort out extradata - opponent.extradata = { - advantage = opponent.advantage, - penalty = opponent.penalty, - score2 = opponent.score2, - } - - local partySize = Opponent.partySize(opponent.type) - if partySize then - opponent = CustomMatchGroupInput.processPartyOpponentInput(opponent, partySize) - elseif opponent.type == Opponent.team then - opponent = CustomMatchGroupInput.ProcessTeamOpponentInput(opponent, match.date) - opponent = CustomMatchGroupInput._readPlayersOfTeam(match, opponentIndex, opponent) - elseif opponent.type == Opponent.literal then - opponent = CustomMatchGroupInput.ProcessLiteralOpponentInput(opponent) - else - error('Unsupported Opponent Type "' .. (opponent.type or '') .. '"') - end - - --set initial opponent sumscore - opponent.sumscore = tonumber(opponent.extradata.advantage) or (-1 * (tonumber(opponent.extradata.penalty) or 0)) - - table.insert(opponentTypes, opponent.type) - - match[opponentKey] = opponent - end - - assert(#opponentTypes <= MAX_NUM_OPPONENTS, 'Too many opponents') - - match.mode = Array.all(opponentTypes, function(opponentType) return opponentType == opponentTypes[1] end) - and opponentTypes[1] or MODE_MIXED - - match.isTeamMatch = Array.any(opponentTypes, function(opponentType) return opponentType == Opponent.team end) - - return match +---@param player table +---@return string +function MatchFunctions.getPlayerFaction(player) + return Faction.read(player.extradata.faction) or Faction.defaultFaction end ----reads the players of a team from input and wiki variables ----@param match table ----@param opponentIndex integer ----@param opponent table ----@return table -function CustomMatchGroupInput._readPlayersOfTeam(match, opponentIndex, opponent) - local players = {} - - local teamName = opponent.name - local playersData = Json.parseIfString(opponent.players) or {} - - local insertIntoPlayers = function(player) - if type(player) ~= 'table' or Logic.isEmpty(player) or Logic.isEmpty(player.name) then - return - end - - player.name = mw.ext.TeamLiquidIntegration.resolve_redirect(player.name):gsub(' ', '_') - player.flag = Flags.CountryName(player.flag) - player.displayname = Logic.emptyOr(player.displayname, player.displayName) - player.extradata = {faction = Faction.read(player.race or player.faction)} - - players[player.name] = players[player.name] or {} - Table.deepMergeInto(players[player.name], player) - end - - local playerIndex = 1 - local varPrefix = teamName .. '_p' .. playerIndex - local name = Variables.varDefault(varPrefix) - while name do - insertIntoPlayers{ - name = name, - displayName = Variables.varDefault(varPrefix .. 'dn'), - faction = Variables.varDefault(varPrefix .. 'faction'), - flag = Variables.varDefault(varPrefix .. 'flag'), - } - playerIndex = playerIndex + 1 - varPrefix = teamName .. '_p' .. playerIndex - name = Variables.varDefault(varPrefix) - end - - --players from manual input as `opponnetX_pY` - for _, player in Table.iter.pairsByPrefix(match, 'opponent' .. opponentIndex .. '_p') do - insertIntoPlayers(Json.parseIfString(player)) - end - - --players from manual input in `opponent.players` - for _, playerName, playerPrefix in Table.iter.pairsByPrefix(playersData, 'p') do - insertIntoPlayers({ - name = playerName, - displayName = playersData[playerPrefix .. 'dn'], - faction = playersData[playerPrefix .. 'faction'], - flag = playersData[playerPrefix .. 'flag'], - }) - end - - opponent.match2players = Array.extractValues(players) - --set default faction for unset factions - Array.forEach(opponent.match2players, function(player) - player.extradata.faction = player.extradata.faction or Faction.defaultFaction - end) - - return opponent +---@param opponents {type: OpponentType} +---@return string +function MatchFunctions.getMode(opponents) + local opponentTypes = Array.map(opponents, Operator.property('type')) + return #Array.unique(opponentTypes) == 1 and opponentTypes[1] or MODE_MIXED end ----@param opponent table ----@return table -function CustomMatchGroupInput.ProcessLiteralOpponentInput(opponent) - local faction = opponent.race - local flag = opponent.flag - local name = opponent.name or opponent[1] - local extradata = opponent.extradata +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) or tonumber(Variables.varDefault('bestof')) - local players = {} - if String.isNotEmpty(faction) or String.isNotEmpty(flag) then - players[1] = { - displayname = name, - name = TBD:upper(), - flag = Flags.CountryName(flag), - extradata = {faction = Faction.read(faction) or Faction.defaultFaction} - } - extradata.hasFactionOrFlag = true + if bestof then + Variables.varDefine('bestof', bestof) end - return { - type = opponent.type, - name = name, - score = opponent.score, - extradata = extradata, - match2players = players - } + return bestof end ----@param opponent table ----@param partySize integer +---@param match table ---@return table -function CustomMatchGroupInput.processPartyOpponentInput(opponent, partySize) - local players = {} - local links = {} - - for playerIndex = 1, partySize do - local name = Logic.emptyOr(opponent['p' .. playerIndex], opponent[playerIndex]) or '' - local link = mw.ext.TeamLiquidIntegration.resolve_redirect(Logic.emptyOr( - opponent['p' .. playerIndex .. 'link'], - Variables.varDefault(name .. '_page') - ) or name):gsub(' ', '_') - table.insert(links, link) - - table.insert(players, { - displayname = name, - name = link, - flag = Flags.CountryName(Logic.emptyOr( - opponent['p' .. playerIndex .. 'flag'], - Variables.varDefault(name .. '_flag') - )), - extradata = {faction = Faction.read(Logic.emptyOr( - opponent['p' .. playerIndex .. 'race'], - Variables.varDefault(name .. '_faction') - )) or Faction.defaultFaction} - }) - end - - table.sort(links) - +function MatchFunctions.getLinks(match) return { - type = opponent.type, - name = table.concat(links, ' / '), - score = opponent.score, - extradata = opponent.extradata, - match2players = players + preview = match.preview, + review = match.review, + recap = match.recap, } end ----@param opponent table ----@param date string ----@return table -function CustomMatchGroupInput.ProcessTeamOpponentInput(opponent, date) - local template = string.lower(Logic.emptyOr(opponent.template, opponent[1], '')--[[@as string]]):gsub('_', ' ') - - if String.isEmpty(template) or template == 'noteam' then - opponent = Table.merge(opponent, Opponent.blank(Opponent.team)) - opponent.name = Opponent.toName(opponent) - return opponent - end - - assert(mw.ext.TeamTemplate.teamexists(template), 'Missing team template "' .. template .. '"') - - local templateData = mw.ext.TeamTemplate.raw(template, date) - - opponent.icon = templateData.image - opponent.icondark = Logic.emptyOr(templateData.imagedark, templateData.image) - opponent.name = templateData.page:gsub(' ', '_') - opponent.template = templateData.templatename or template - - return opponent -end - ---MapInput functions - ---@param match table ----@param mapIndex integer ----@param subGroupIndex integer ----@return integer -function CustomMatchGroupInput._mapInput(match, mapIndex, subGroupIndex) - local map = Json.parseIfString(match['map' .. mapIndex]) - map.map = (MapsData[(map.map or ''):lower()] or {}).name or map.map - - -- set initial extradata for maps - map.extradata = { - comment = map.comment or '', - header = map.header or '', +---@param numberOfGames integer +---@return table +function MatchFunctions.getExtraData(match, numberOfGames) + local extradata = { + casters = MatchGroupInputUtil.readCasters(match, {noSort = true}), } - -- determine score, resulttype, walkover and winner - map = CustomMatchGroupInput._mapWinnerProcessing(map) - - -- get participants data for the map + get map mode + winnerfaction and loserfaction - --(w/l faction stuff only for 1v1 maps) - CustomMatchGroupInput.ProcessPlayerMapData(map, match, 2) - - --adjust sumscore for winner opponent - if (tonumber(map.winner) or 0) > 0 then - match['opponent' .. map.winner].sumscore = - match['opponent' .. map.winner].sumscore + 1 - end - - -- handle subgroup stuff if team match - if match.isTeamMatch then - map.subgroup = tonumber(map.subgroup) or (subGroupIndex + 1) - subGroupIndex = map.subgroup + for prefix, mapVeto in Table.iter.pairsByPrefix(match, 'veto') do + extradata[prefix] = (MapsData[mapVeto:lower()] or {}).name or mapVeto + extradata[prefix .. 'by'] = match[prefix .. 'by'] end - match['map' .. mapIndex] = map + Table.mergeInto(extradata, Table.filterByKey(match, function(key) return key:match('subgroup%d+header') end)) - return subGroupIndex + return extradata end ----@param map table +---@param mapInput table +---@param subGroup integer +---@param opponentCount integer ---@return table -function CustomMatchGroupInput._mapWinnerProcessing(map) - map.scores = {} - local hasManualScores = false - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] - local obj = {} - if Logic.isNotEmpty(score) then - hasManualScores = true - score = CONVERT_STATUS_INPUT[score] or score - if Logic.isNumeric(score) then - obj.status = SCORE_STATUS - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end +---@return integer +function MapFunctions.readMap(mapInput, subGroup, opponentCount) + subGroup = tonumber(mapInput.subgroup) or (subGroup + 1) - local winner = tonumber(map.winner) - if Logic.isNotEmpty(map.walkover) then - local walkoverInput = tonumber(map.walkover) - if walkoverInput == 1 or walkoverInput == 2 or walkoverInput == 0 then - winner = walkoverInput - end - map.walkover = Table.includes(ALLOWED_STATUSES, map.walkover) and map.walkover or 'L' - map.scores = {-1, -1} - map.resulttype = 'default' - map.winner = winner + local mapName = (MapsData[(mapInput.map or ''):lower()] or {}).name or mapInput.map - return map - end + local map = { + map = mapName, + subgroup = subGroup, + extradata = { + comment = mapInput.comment, + header = mapInput.header, + } + } - if hasManualScores then - map.winner = winner or CustomMatchGroupInput._getWinner(indexedScores) + map.finished = MatchGroupInputUtil.mapIsFinished(mapInput) + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = mapInput.walkover, + winner = mapInput.winner, + opponentIndex = opponentIndex, + score = mapInput['score' .. opponentIndex], + }, MapFunctions.calculateMapScore(mapInput.winner, map.finished)) + return {score = score, status = status} + end) - return map - end + map.scores = Array.map(opponentInfo, Operator.property('score')) - if map.winner == 'skip' then - map.scores = {-1, -1} - map.resulttype = 'np' - elseif winner == 1 then - map.scores = {1, 0} - elseif winner == 2 then - map.scores = {0, 1} - elseif winner == 0 or map.winner == 'draw' then - winner = 0 - map.scores = {0.5, 0.5} - map.resulttype = 'draw' + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(mapInput.winner, mapInput.finished, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, mapInput.winner, opponentInfo) end - map.winner = winner - - return map + return map, subGroup end ----@param indexedScores table ----@return integer? -function CustomMatchGroupInput._getWinner(indexedScores) - table.sort(indexedScores, CustomMatchGroupInput._mapWinnerSortFunction) - - return indexedScores[1].index +---@param winnerInput string|integer|nil +---@param finished boolean +---@return fun(opponentIndex: integer): integer? +function MapFunctions.calculateMapScore(winnerInput, finished) + local winner = tonumber(winnerInput) + return function(opponentIndex) + -- TODO Better to check if map has started, rather than finished, for a more correct handling + if not winner and not finished then + return + end + return winner == opponentIndex and 1 or 0 + end end ----@param map table ----@param match table ----@param numberOfOpponents integer -function CustomMatchGroupInput.ProcessPlayerMapData(map, match, numberOfOpponents) +---@param mapInput table +---@param opponents table[] +---@return table +function MapFunctions.getParticipants(mapInput, opponents) local participants = {} - local modeParts = {} - for opponentIndex = 1, numberOfOpponents do - local opponent = match['opponent' .. opponentIndex] - local partySize = Opponent.partySize(opponent.type) - local players = opponent.match2players - if partySize then - table.insert(modeParts, partySize) - CustomMatchGroupInput._processPartyPlayerMapData(players, map, opponentIndex, participants) + Array.forEach(opponents, function(opponent, opponentIndex) + if opponent.type == Opponent.literal then + return elseif opponent.type == Opponent.team then - table.insert(modeParts, CustomMatchGroupInput._processTeamPlayerMapData(players, map, opponentIndex, participants)) - elseif opponent.type == Opponent.literal then - table.insert(modeParts, 'literal') + Table.mergeInto(participants, MapFunctions.getTeamParticipants(mapInput, opponent, opponentIndex)) + return end - end + Table.mergeInto(participants, MapFunctions.getPartyParticipants(mapInput, opponent, opponentIndex)) + end) - map.mode = table.concat(modeParts, 'v') - map.participants = participants + return participants +end - if numberOfOpponents ~= MAX_NUM_OPPONENTS or map.mode ~= '1v1' then - return - end +---@param mapInput table +---@param opponent table +---@param opponentIndex integer +---@return table +function MapFunctions.getTeamParticipants(mapInput, opponent, opponentIndex) + local players = Array.mapIndexes(function(playerIndex) + return Logic.nilIfEmpty(mapInput['t' .. opponentIndex .. 'p' .. playerIndex]) + end) - local opponentFactions, playerNameArray, heroesData - = CustomMatchGroupInput._fetchOpponentMapParticipantData(participants) - map.extradata = Table.merge(map.extradata, heroesData) - if tonumber(map.winner) == 1 then - map.extradata.winnerfaction = opponentFactions[1] - map.extradata.loserfaction = opponentFactions[2] - elseif tonumber(map.winner) == 2 then - map.extradata.winnerfaction = opponentFactions[2] - map.extradata.loserfaction = opponentFactions[1] - end - map.extradata.opponent1 = playerNameArray[1] - map.extradata.opponent2 = playerNameArray[2] -end + local participants, unattachedParticipants = MatchGroupInputUtil.parseParticipants( + opponent.match2players, + players, + function(playerIndex) + local prefix = 't' .. opponentIndex .. 'p' .. playerIndex + return { + name = mapInput[prefix], + link = Logic.nilIfEmpty(mapInput[prefix .. 'link']) or Variables.varDefault(mapInput[prefix] .. '_page'), + } + end, + function(playerIndex, playerIdData, playerInputData) + local prefix = 't' .. opponentIndex .. 'p' .. playerIndex + local faction = Faction.read(mapInput[prefix .. 'race']) + or (playerIdData.extradata or {}).faction or Faction.defaultFaction + local link = playerIdData.name or playerInputData.link or playerInputData.name:gsub(' ', '_') + return { + faction = faction, + player = link, + flag = Flags.CountryName(playerIdData.flag), + position = playerIndex, + random = Logic.readBool(mapInput[prefix .. 'random']), + heroes = MapFunctions.readHeroes( + mapInput[prefix .. 'heroes'], + faction, + link, + Logic.readBool(mapInput[prefix .. 'heroesNoCheck']) + ), + } + end + ) ----@param participants table ----@return table ----@return table ----@return table -function CustomMatchGroupInput._fetchOpponentMapParticipantData(participants) - local opponentFactions, playerNameArray, heroesData = {}, {}, {} - for participantKey, participantData in pairs(participants) do - local opponentIndex = tonumber(string.sub(participantKey, 1, 1)) - -- opponentIndex can not be nil due to the format of the participants keys - ---@cast opponentIndex -nil - opponentFactions[opponentIndex] = participantData.faction - playerNameArray[opponentIndex] = participantData.player - Array.forEach(participantData.heroes or {}, function(hero, heroIndex) - heroesData['opponent' .. opponentIndex .. 'hero' .. heroIndex] = hero - end) - end + Array.forEach(unattachedParticipants, function(participant) + local name = mapInput['t' .. opponentIndex .. 'p' .. participant.position] + local nameUpper = name:upper() + local isTBD = nameUpper == TBD - return opponentFactions, playerNameArray, heroesData + table.insert(opponent.match2players, { + name = isTBD and TBD or participant.player, + displayname = isTBD and TBD or name, + flag = participant.flag, + extradata = {faction = participant.faction}, + }) + participants[#opponent.match2players] = participant + end) + + return Table.map(participants, MatchGroupInputUtil.prefixPartcipants(opponentIndex)) end ----@param players table[] ----@param map table +---@param mapInput table +---@param opponent table ---@param opponentIndex integer ----@param participants table ----@return table -function CustomMatchGroupInput._processPartyPlayerMapData(players, map, opponentIndex, participants) +---@return table +function MapFunctions.getPartyParticipants(mapInput, opponent, opponentIndex) + local players = opponent.match2players + + -- resolve the aliases in case they are used local prefix = 't' .. opponentIndex .. 'p' - map[prefix .. '1race'] = Logic.emptyOr(map[prefix .. '1race'], map['race' .. opponentIndex]) - map[prefix .. '1heroes'] = Logic.emptyOr(map[prefix .. '1heroes'], map['heroes' .. opponentIndex]) + mapInput[prefix .. '1race'] = Logic.emptyOr(mapInput[prefix .. '1race'], mapInput['race' .. opponentIndex]) + mapInput[prefix .. '1heroes'] = Logic.emptyOr(mapInput[prefix .. '1heroes'], mapInput['heroes' .. opponentIndex]) + + local participants = {} - for playerIndex, player in pairs(players) do - local faction = Logic.emptyOr(map[prefix .. playerIndex .. 'race'], player.extradata.faction, Faction.defaultFaction) - faction = Faction.read(faction) + Array.forEach(players, function(player, playerIndex) + local faction = Faction.read(mapInput['t' .. opponentIndex .. 'p' .. playerIndex .. 'race']) + or player.extradata.faction participants[opponentIndex .. '_' .. playerIndex] = { - faction = faction, + faction = Faction.read(faction or player.extradata.faction), player = player.name, - heroes = CustomMatchGroupInput._readHeroes( - map[prefix .. playerIndex .. 'heroes'], + heroes = MapFunctions.readHeroes( + mapInput[prefix .. playerIndex .. 'heroes'], faction, player.name, - Logic.readBool(map[prefix .. playerIndex .. 'heroesNoCheck']) + Logic.readBool(mapInput[prefix .. playerIndex .. 'heroesNoCheck']) ), } - end + end) return participants end ----@param players table[] ----@param map table ----@param opponentIndex integer ----@param participants table ----@return integer -function CustomMatchGroupInput._processTeamPlayerMapData(players, map, opponentIndex, participants) - local amountOfTbds = 0 - local playerData = {} - - local numberOfPlayers = 0 - for prefix, playerInput, playerIndex in Table.iter.pairsByPrefix(map, 't' .. opponentIndex .. 'p') do - numberOfPlayers = numberOfPlayers + 1 - if playerInput:lower() == TBD then - amountOfTbds = amountOfTbds + 1 - else - local link = Logic.emptyOr(map[prefix .. 'link'], Variables.varDefault(playerInput .. '_page')) or playerInput - link = mw.ext.TeamLiquidIntegration.resolve_redirect(link):gsub(' ', '_') - - playerData[link] = { - faction = Faction.read(map[prefix .. 'race']), - position = playerIndex, - heroes = map[prefix .. 'heroes'], - heroesCheckDisabled = Logic.readBool(map[prefix .. 'heroesNoCheck']), - playedRandom = Logic.readBool(map[prefix .. 'random']), - } +---@param mapInput table # the input data +---@param participants table +---@param opponents table[] +---@return string +function MapFunctions.getMode(mapInput, participants, opponents) + -- assume we have a min of 2 opponents in a game + local playerCounts = {0, 0} + for key in pairs(participants) do + local parsedOpponentIndex = key:match('(%d+)_%d+') + local opponetIndex = tonumber(parsedOpponentIndex) --[[@as integer]] + playerCounts[opponetIndex] = (playerCounts[opponetIndex] or 0) + 1 + end + + local modeParts = Array.map(playerCounts, function(count, opponentIndex) + if count == 0 then + return Opponent.literal end - end - for playerIndex, player in pairs(players) do - local currentPlayer = playerData[player.name] - if currentPlayer then - local faction = currentPlayer.faction or (player.extradata or {}).faction or Faction.defaultFaction + return count + end) - participants[opponentIndex .. '_' .. playerIndex] = { - faction = faction, - player = player.name, - position = currentPlayer.position, - flag = Flags.CountryName(player.flag), - heroes = CustomMatchGroupInput._readHeroes( - currentPlayer.heroes, - faction, - player.name, - currentPlayer.heroesCheckDisabled - ), - random = currentPlayer.playedRandom, - } - end + return table.concat(modeParts, 'v') +end + +---@param map table +---@param participants table +---@return table +function MapFunctions.getAdditionalExtraData(map, participants) + if map.mode ~= '1v1' then return {} end + + local extradata = MapFunctions.getHeroesExtradata(participants) + + local players = {} + for _, player in Table.iter.spairs(participants) do + table.insert(players, player) end - for tbdIndex = 1, amountOfTbds do - participants[opponentIndex .. '_' .. (#players + tbdIndex)] = { - faction = Faction.defaultFaction, - player = TBD:upper(), - } + extradata.opponent1 = players[1].player + extradata.opponent2 = players[2].player + + if map.winner ~= 1 and map.winner ~= 2 then + return extradata end + local loser = 3 - map.winner - map.participants = participants + extradata.winnerfaction = players[map.winner].faction + extradata.loserfaction = players[loser].faction - return numberOfPlayers + return extradata +end + +--- additionally store heroes in extradata so we can condition on them +---@param participants table +---@return table +function MapFunctions.getHeroesExtradata(participants) + local extradata = {} + for participantKey, participant in Table.iter.spairs(participants) do + local opponentIndex = string.match(participantKey, '^(%d+)_') + Array.forEach(participant.heroes or {}, function(hero, heroIndex) + extradata['opponent' .. opponentIndex .. 'hero' .. heroIndex] = hero + end) + end + + return extradata end ---@param heroesInput string? @@ -753,7 +474,7 @@ end ---@param playerName string ---@param ignoreFactionHeroCheck boolean ---@return string[]? -function CustomMatchGroupInput._readHeroes(heroesInput, faction, playerName, ignoreFactionHeroCheck) +function MapFunctions.readHeroes(heroesInput, faction, playerName, ignoreFactionHeroCheck) if String.isEmpty(heroesInput) then return end @@ -773,24 +494,4 @@ function CustomMatchGroupInput._readHeroes(heroesInput, faction, playerName, ign end) end - ----@param opponent1 table ----@param opponent2 table ----@return boolean -function CustomMatchGroupInput._mapWinnerSortFunction(opponent1, opponent2) - local opponent1Norm = opponent1.status == SCORE_STATUS - local opponent2Norm = opponent2.status == SCORE_STATUS - - if opponent1Norm and opponent2Norm then - return tonumber(opponent1.score) > tonumber(opponent2.score) - elseif opponent1Norm then return true - elseif opponent2Norm then return false - elseif opponent1.status == DEFAULT_WIN_STATUS then return true - elseif Table.includes(ALLOWED_STATUSES, opponent1.status) then return false - elseif opponent2.status == DEFAULT_WIN_STATUS then return false - elseif Table.includes(ALLOWED_STATUSES, opponent2.status) then return true - else return true - end -end - return CustomMatchGroupInput diff --git a/components/match2/wikis/warcraft/match_summary.lua b/components/match2/wikis/warcraft/match_summary.lua index 1319c4423b3..372f9717249 100644 --- a/components/match2/wikis/warcraft/match_summary.lua +++ b/components/match2/wikis/warcraft/match_summary.lua @@ -371,17 +371,21 @@ function CustomMatchSummary._submatchHeader(submatch) } end + ---@param opponentIndex any + ---@return Html local createScore = function(opponentIndex) - local isWinner = opponentIndex == submatch.winner + local isWinner = opponentIndex == submatch.winner or submatch.resultType ~= 'draw' if submatch.resultType == 'default' then return OpponentDisplay.BlockScore{ isWinner = isWinner, - scoreText = isWinner and 'W' or submatch.walkover, + scoreText = isWinner and 'W' or string.upper(submatch.walkover), } end + + local score = submatch.resultType ~= 'np' and (submatch.scores or {})[opponentIndex] or '' return OpponentDisplay.BlockScore{ isWinner = isWinner, - scoreText = (submatch.scores or {})[opponentIndex] or '', + scoreText = score, } end diff --git a/components/match2/wikis/wildrift/match_group_input_custom.lua b/components/match2/wikis/wildrift/match_group_input_custom.lua index c7fe36b8bc6..4a6abb8b3ac 100644 --- a/components/match2/wikis/wildrift/match_group_input_custom.lua +++ b/components/match2/wikis/wildrift/match_group_input_custom.lua @@ -26,7 +26,6 @@ local DUMMY_MAP = 'default' local OPPONENT_CONFIG = { resolveRedirect = true, pagifyTeamNames = false, - pagifyPlayerNames = true, } -- containers for process helper functions @@ -75,7 +74,8 @@ function CustomMatchGroupInput.processMatch(match, options) MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - MatchFunctions.getTournamentVars(match) + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) match.stream = Streams.processStreams(match) match.links = MatchFunctions.getLinks(match) @@ -128,8 +128,6 @@ function CustomMatchGroupInput.extractMaps(match, opponentCount) return maps end -CustomMatchGroupInput.processMap = FnUtil.identity - -- -- match related functions -- @@ -150,13 +148,6 @@ function MatchFunctions.getBestOf(match) return bestOf or DEFAULT_BESTOF end ----@param match table ----@return table -function MatchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', DEFAULT_MODE)) - return MatchGroupInputUtil.getCommonTournamentVars(match) -end - ---@param match table ---@return table function MatchFunctions.getLinks(match) diff --git a/components/match2/wikis/worldoftanks/match_group_input_custom.lua b/components/match2/wikis/worldoftanks/match_group_input_custom.lua index b7cdcf5c402..b665907c111 100644 --- a/components/match2/wikis/worldoftanks/match_group_input_custom.lua +++ b/components/match2/wikis/worldoftanks/match_group_input_custom.lua @@ -6,182 +6,77 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- +local Array = require('Module:Array') local Logic = require('Module:Logic') local Lua = require('Module:Lua') -local Opponent = require('Module:Opponent') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') local Table = require('Module:Table') -local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') -local DateExt = require('Module:Date/Ext') -local Streams = require('Module:Links/Stream') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') -local NP_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local ALLOWED_VETOES = {'decider', 'pick', 'ban', 'defaultban', 'protect'} -local ALLOWED_STATUSES = { 'W', 'FF', 'DQ', 'L', 'D' } -local MAX_NUM_OPPONENTS = 2 -local MAX_NUM_MAPS = 20 +local ALLOWED_VETOES = Array.append(MatchGroupInputUtil.DEFAULT_ALLOWED_VETOES, 'protect') local DEFAULT_BESTOF = 3 - -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) +local DEFAULT_MODE = 'team' -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} -- called from Module:MatchGroup ---@param match table +---@param options table? ---@return table -function CustomMatchGroupInput.processMatch(match) - -- Count number of maps, check for empty maps to remove, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getScoreFromMapWinners(match) - - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.getExtraData(match) - - return match -end - --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) - - return map -end - ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() - - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end - - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end - - Opponent.resolve(opponent, teamTemplateDate, {syncPlayer = true}) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end - ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match wasn't played, set not played - if - Table.includes(NP_STATUSES, data.finished) or - Table.includes(NP_STATUSES, data.winner) - then - data.resulttype = 'np' - data.finished = true - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - elseif Logic.readBool(data.finished) then - if MatchGroupInput.isDraw(indexedScores) then - data.winner = 0 - data.resulttype = 'draw' - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'draw') - elseif MatchGroupInput.hasSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = 'default' - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = 'ff' - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = 'dq' - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = 'l' - end - indexedScores = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, 'default') - else - local winner - indexedScores, winner = CustomMatchGroupInput.setPlacement(indexedScores, data.winner, nil, data.finished) - data.winner = data.winner or winner - end +function CustomMatchGroupInput.processMatch(match, options) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] + + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) + + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) + + local games = MatchFunctions.extractMaps(match, #opponents) + + match.bestof = MatchFunctions.getBestOf(match.bestof) + + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) + + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) + + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - --set it as finished if we have a winner - if Logic.isNotEmpty(data.winner) then - data.finished = true - end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) - return data, indexedScores -end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) ----@param opponents table[] ----@param winner integer? ----@param specialType string? ----@param finished boolean|string? ----@return table[] ----@return integer? -function CustomMatchGroupInput.setPlacement(opponents, winner, specialType, finished) - if specialType == 'draw' then - for key, _ in pairs(opponents) do - opponents[key].placement = 1 - end - elseif specialType == 'default' then - for key, _ in pairs(opponents) do - if key == winner then - opponents[key].placement = 1 - else - opponents[key].placement = 2 - end - end - else - local temporaryScore - local temporaryPlace = -99 - local counter = 0 - for scoreIndex, opp in Table.iter.spairs(opponents, CustomMatchGroupInput.placementSortFunction) do - local score = tonumber(opp.score) or '' - counter = counter + 1 - if counter == 1 and Logic.isEmpty(winner) then - if finished then - winner = scoreIndex - end - end - if temporaryScore == score then - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or temporaryPlace - else - opponents[scoreIndex].placement = tonumber(opponents[scoreIndex].placement) or counter - temporaryPlace = counter - temporaryScore = score - end - end - end + match.games = games + match.opponents = opponents - return opponents, winner -end + match.extradata = MatchFunctions.getExtraData(match) ----@param tbl table[] ----@param key1 integer ----@param key2 integer ----@return boolean -function CustomMatchGroupInput.placementSortFunction(tbl, key1, key2) - local value1 = tonumber(tbl[key1].score) or -99 - local value2 = tonumber(tbl[key2].score) or -99 - return value1 > value2 + return match end -- @@ -189,181 +84,90 @@ end -- ---@param match table ----@return table -function matchFunctions.getBestOf(match) - match.bestof = Logic.emptyOr(match.bestof, Variables.varDefault('bestof', DEFAULT_BESTOF)) - Variables.varDefine('bestof', match.bestof) - return match -end - --- Calculate the match scores based on the map results (counting map wins) --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - local opponentNumber = 0 - for index = 1, MAX_NUM_OPPONENTS do - if String.isEmpty(match['opponent' .. index]) then - break - end - opponentNumber = index - end - local newScores = {} - local foundScores = false - - for i = 1, MAX_NUM_MAPS do - if match['map'..i] then - local winner = tonumber(match['map'..i].winner) - foundScores = true - if winner and winner > 0 and winner <= opponentNumber then - newScores[winner] = (newScores[winner] or 0) + 1 - end - else - break +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - for index = 1, opponentNumber do - if not match['opponent' .. index].score and foundScores then - match['opponent' .. index].score = newScores[index] or 0 - end + table.insert(maps, map) + match[key] = nil end - return match + return maps end ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - match.publishertier = Logic.emptyOr(match.publishertier, Variables.varDefault('tournament_publishertier')) - return MatchGroupInput.getCommonTournamentVars(match) -end +---@param bestofInput string|integer? +---@return integer? +function MatchFunctions.getBestOf(bestofInput) + local bestof = tonumber(bestofInput) ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) + if bestof then + Variables.varDefine('bestof', bestof) + return bestof + end - match.links = {} - local links = match.links - if match.preview then links.preview = match.preview end + return tonumber(Variables.varDefault('bestof')) or DEFAULT_BESTOF +end - return match +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) + end end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mvp = MatchGroupInput.readMvp(match), - mapveto = MatchGroupInput.getMapVeto(match, ALLOWED_VETOES), - casters = MatchGroupInput.readCasters(match), - } - return match +function MatchFunctions.getLinks(match) + return {} end ---@param match table ---@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if TypeUtil.isNumeric(opponent.score) then - opponent.status = 'S' - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = -1 - end - opponents[opponentIndex] = opponent - end - end - - -- see if match should actually be finished if bestof limit was reached - if isScoreSet and not Logic.readBool(match.finished) then - local firstTo = math.ceil(match.bestof/2) - for _, item in pairs(opponents) do - if (tonumber(item.score) or 0) >= firstTo then - match.finished = true - break - end - end - end - - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - -- apply placements and winner if finshed - if not String.isEmpty(match.winner) or Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match +function MatchFunctions.getExtraData(match) + return { + mvp = MatchGroupInputUtil.readMvp(match), + mapveto = MatchGroupInputUtil.getMapVeto(match, ALLOWED_VETOES), + casters = MatchGroupInputUtil.readCasters(match), + } end -- -- map related functions -- --- Parse extradata information ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { +function MapFunctions.getExtraData(map, opponentCount) + return { comment = map.comment, header = map.header, } - return map -end - --- Calculate Score and Winner of the map ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score = map['score' .. scoreIndex] or map['t' .. scoreIndex .. 'score'] - local obj = {} - if not Logic.isEmpty(score) then - if TypeUtil.isNumeric(score) then - obj.status = 'S' - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = -1 - end - table.insert(map.scores, score) - indexedScores[scoreIndex] = obj - else - break - end - end - - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - - return map end return CustomMatchGroupInput diff --git a/components/match2/wikis/worldoftanks/match_summary.lua b/components/match2/wikis/worldoftanks/match_summary.lua index 3cf1e6d8cd0..49db4a97070 100644 --- a/components/match2/wikis/worldoftanks/match_summary.lua +++ b/components/match2/wikis/worldoftanks/match_summary.lua @@ -18,9 +18,6 @@ local Table = require('Module:Table') local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper') local MatchSummary = Lua.import('Module:MatchSummary/Base') -local MAP_VETO_START = 'Start Map Veto' -local ARROW_LEFT = '[[File:Arrow sans left.svg|15x15px|link=|Left team starts]]' -local ARROW_RIGHT = '[[File:Arrow sans right.svg|15x15px|link=|Right team starts]]' local NONE = '-' local TBD = Abbreviation.make('TBD', 'To Be Determined') @@ -34,68 +31,16 @@ local LINK_DATA = { preview = {icon = 'File:Preview Icon32.png', text = 'Preview'}, } -local VETO_TYPE_TO_TEXT = { - ban = 'BAN', - pick = 'PICK', - decider = 'DECIDER', - defaultban = 'DEFAULT BAN', - -} local CustomMatchSummary = {} --- Map Veto Class ----@class WoTMapVeto: MatchSummaryRowInterface ----@operator call: self ----@field root Html ----@field table Html -local MapVeto = Class.new( - function(self) - self.root = mw.html.create('div'):addClass('brkts-popup-mapveto') - self.table = self.root:tag('table') - :addClass('wikitable-striped'):addClass('collapsible'):addClass('collapsed') - self:createHeader() - end -) - ----@return self -function MapVeto:createHeader() - self.table:tag('tr') - :tag('th'):css('width','33%'):done() - :tag('th'):css('width','34%'):wikitext('Map Veto'):done() - :tag('th'):css('width','33%'):done() - return self -end - ----@param firstVeto number? ----@param format string? ----@return self -function MapVeto:vetoStart(firstVeto, format) - format = format and ('Veto format: ' .. format) or nil - local textLeft - local textCenter - local textRight - if firstVeto == 1 then - textLeft = MAP_VETO_START - textCenter = ARROW_LEFT - textRight = format - elseif firstVeto == 2 then - textLeft = format - textCenter = ARROW_RIGHT - textRight = MAP_VETO_START - else return self end - - self.table:tag('tr'):addClass('brkts-popup-mapveto-vetostart') - :tag('th'):wikitext(textLeft):done() - :tag('th'):wikitext(textCenter):done() - :tag('th'):wikitext(textRight):done() - - return self -end +---@class WoTMapVeto: VetoDisplay +local MapVeto = Class.new(MatchSummary.MapVeto) ---@param map1 string? ---@param map2 string? ----@return string, string -function MapVeto._displayMaps(map1, map2) +---@return string +---@return string +function MapVeto:displayMaps(map1, map2) if Logic.isEmpty(map1) and Logic.isEmpty(map2) then return TBD, TBD end @@ -104,69 +49,6 @@ function MapVeto._displayMaps(map1, map2) Page.makeInternalLink(map2) or NONE end ----@param map string? ----@return self -function MapVeto:addDecider(map) - map = Page.makeInternalLink(map) or TBD - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - self:addColumnVetoMap(row, map) - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - - self.table:node(row) - return self -end - ----@param vetoType string? ----@param map1 string? ----@param map2 string? ----@return self -function MapVeto:addRound(vetoType, map1, map2) - map1, map2 = MapVeto._displayMaps(map1, map2) - - local vetoText = VETO_TYPE_TO_TEXT[vetoType] - - if not vetoText then return self end - - local class = 'brkts-popup-mapveto-' .. vetoType - - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoMap(row, map1) - self:addColumnVetoType(row, class, vetoText) - self:addColumnVetoMap(row, map2) - - self.table:node(row) - return self -end - ----@param row Html ----@param styleClass string ----@param vetoText string ----@return self -function MapVeto:addColumnVetoType(row, styleClass, vetoText) - row:tag('td') - :tag('span') - :addClass(styleClass) - :addClass('brkts-popup-mapveto-vetotype') - :wikitext(vetoText) - return self -end - ----@param row Html ----@param map string ----@return self -function MapVeto:addColumnVetoMap(row, map) - row:tag('td'):wikitext(map):done() - return self -end - ----@return Html -function MapVeto:create() - return self.root -end - ---@param args table ---@return Html function CustomMatchSummary.getByMatchId(args) @@ -221,34 +103,10 @@ function CustomMatchSummary.createBody(match) body:addRow(MatchSummary.makeCastersRow(match.extradata.casters)) -- Add the Map Vetoes - if match.extradata.mapveto then - local vetoData = match.extradata.mapveto - if vetoData then - local mapVeto = MapVeto() - - for _,vetoRound in ipairs(vetoData) do - if vetoRound.vetostart then - mapVeto:vetoStart(tonumber(vetoRound.vetostart)) - end - if vetoRound.type == 'decider' then - mapVeto:addDecider(vetoRound.decider) - else - mapVeto:addRound(vetoRound.type, vetoRound.team1, vetoRound.team2) - end - end - - body:addRow(mapVeto) - end - end + body:addRow(MatchSummary.defaultMapVetoDisplay(match, MapVeto())) return body end ----@param game MatchGroupUtilGame ----@param opponentIndex integer ----@return Html -function CustomMatchSummary._gameScore(game, opponentIndex) - return mw.html.create('div'):wikitext(game.scores[opponentIndex]) -end ---@param game MatchGroupUtilGame ---@return MatchSummaryRow @@ -278,12 +136,12 @@ function CustomMatchSummary._createMapRow(game) local leftNode = mw.html.create('div') :addClass('brkts-popup-spaced') :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 1, Icons.CHECK)) - :node(CustomMatchSummary._gameScore(game, 1)) + :node(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) :css('width', '20%') local rightNode = mw.html.create('div') :addClass('brkts-popup-spaced') - :node(CustomMatchSummary._gameScore(game, 2)) + :node(DisplayHelper.MapScore(game.scores[2], 2, game.resultType, game.walkover, game.winner)) :node(CustomMatchSummary._createCheckMarkOrCross(game.winner == 2, Icons.CHECK)) :css('width', '20%') diff --git a/components/match2/wikis/zula/match_group_input_custom.lua b/components/match2/wikis/zula/match_group_input_custom.lua index eb236eb148f..91b228ea4f4 100644 --- a/components/match2/wikis/zula/match_group_input_custom.lua +++ b/components/match2/wikis/zula/match_group_input_custom.lua @@ -6,178 +6,78 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- +local Array = require('Module:Array') local Logic = require('Module:Logic') -local MathUtil = require('Module:MathUtil') local Lua = require('Module:Lua') -local String = require('Module:StringUtils') +local Operator = require('Module:Operator') +local Streams = require('Module:Links/Stream') local Table = require('Module:Table') -local TypeUtil = require('Module:TypeUtil') local Variables = require('Module:Variables') -local DateExt = require('Module:Date/Ext') -local Streams = require('Module:Links/Stream') -local Opponent = Lua.import('Module:Opponent') -local MatchGroupInput = Lua.import('Module:MatchGroup/Input/Util') - -local SIDE_DEF = 'ct' -local SIDE_ATK = 't' -local STATUS_SCORE = 'S' -local STATUS_DRAW = 'D' -local STATUS_DEFAULT_WIN = 'W' -local STATUS_FORFEIT = 'FF' -local STATUS_DISQUALIFIED = 'DQ' -local STATUS_DEFAULT_LOSS = 'L' -local ALLOWED_STATUSES = { - STATUS_DRAW, - STATUS_DEFAULT_WIN, - STATUS_FORFEIT, - STATUS_DISQUALIFIED, - STATUS_DEFAULT_LOSS, -} -local NOT_PLAYED_MATCH_STATUSES = {'skip', 'np', 'canceled', 'cancelled'} -local NOT_PLAYED_RESULT_TYPE = 'np' -local DRAW_RESULT_TYPE = 'draw' -local NOW = os.time(os.date('!*t') --[[@as osdateparam]]) -local NOT_PLAYED_SCORE = -1 -local MAX_NUM_OPPONENTS = 2 -local DEFAULT_RESULT_TYPE = 'default' -local DUMMY_MAP_NAME = 'null' -- Is set in Template:Map when |map= is empty. +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') + +local DEFAULT_MODE = 'team' +local DUMMY_MAP = 'null' -- Is set in Template:Map when |map= is empty. -- containers for process helper functions -local matchFunctions = {} -local mapFunctions = {} +local MatchFunctions = {} +local MapFunctions = {} local CustomMatchGroupInput = {} -- called from Module:MatchGroup ---@param match table +---@param options table? ---@return table -function CustomMatchGroupInput.processMatch(match) - -- Count number of maps, check for empty maps to remove, and automatically count score - match = matchFunctions.getBestOf(match) - match = matchFunctions.getVodStuff(match) - match = matchFunctions.removeUnsetMaps(match) - match = matchFunctions.getScoreFromMapWinners(match) - - -- process match - Table.mergeInto(match, MatchGroupInput.readDate(match.date)) - match = matchFunctions.getTournamentVars(match) - match = matchFunctions.getOpponents(match) - match = matchFunctions.getExtraData(match) - - return match -end +function CustomMatchGroupInput.processMatch(match, options) + match.finished = Logic.nilIfEmpty(match.finished) or match.status --- called from Module:Match/Subobjects ----@param map table ----@return table -function CustomMatchGroupInput.processMap(map) - map = mapFunctions.getExtraData(map) - map = mapFunctions.getScoresAndWinner(map) + local finishedInput = match.finished --[[@as string?]] + local winnerInput = match.winner --[[@as string?]] - return map -end + Table.mergeInto(match, MatchGroupInputUtil.readDate(match.date)) ----@param record table ----@param timestamp integer -function CustomMatchGroupInput.processOpponent(record, timestamp) - local opponent = Opponent.readOpponentArgs(record) - or Opponent.blank() + local opponents = Array.mapIndexes(function(opponentIndex) + return MatchGroupInputUtil.readOpponent(match, opponentIndex, {}) + end) - -- Convert byes to literals - if Opponent.isBye(opponent) then - opponent = {type = Opponent.literal, name = 'BYE'} - end + local games = MatchFunctions.extractMaps(match, #opponents) + match.bestof = MatchGroupInputUtil.getBestOf(nil, games) + games = MatchFunctions.removeUnsetMaps(games) - ---@type number|string - local teamTemplateDate = timestamp - -- If date is default date, resolve using tournament dates instead - -- default date indicates that the match is missing a date - -- In order to get correct child team template, we will use an approximately date and not the default date - if teamTemplateDate == DateExt.defaultTimestamp then - teamTemplateDate = Variables.varDefaultMulti('tournament_enddate', 'tournament_startdate', NOW) - end + local autoScoreFunction = MatchGroupInputUtil.canUseAutoScore(match, games) + and MatchFunctions.calculateMatchScore(games) + or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.score, opponent.status = MatchGroupInputUtil.computeOpponentScore({ + walkover = match.walkover, + winner = match.winner, + opponentIndex = opponentIndex, + score = opponent.score, + }, autoScoreFunction) + end) - Opponent.resolve(opponent, teamTemplateDate) - MatchGroupInput.mergeRecordWithOpponent(record, opponent) -end + match.finished = MatchGroupInputUtil.matchIsFinished(match, opponents) --- function to check for draws ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckDraw(tbl) - if #tbl < MAX_NUM_OPPONENTS then - return false + if match.finished then + match.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponents) + match.walkover = MatchGroupInputUtil.getWalkover(match.resulttype, opponents) + match.winner = MatchGroupInputUtil.getWinner(match.resulttype, winnerInput, opponents) + MatchGroupInputUtil.setPlacement(opponents, match.winner, 1, 2, match.resulttype) end - return MatchGroupInput.isDraw(tbl) -end + match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode'), DEFAULT_MODE) + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) ----@param data table ----@param indexedScores table[] ----@return table ----@return table[] -function CustomMatchGroupInput.getResultTypeAndWinner(data, indexedScores) - -- Map or Match is marked as finished. - -- Calculate and set winner, resulttype, placements and walkover (if applicable for the outcome) - local winner = tonumber(data.winner) - if Logic.readBool(data.finished) then - if CustomMatchGroupInput.placementCheckDraw(indexedScores) then - data.winner = 0 - data.resulttype = DRAW_RESULT_TYPE - indexedScores = MatchGroupInput.setPlacement(indexedScores, data.winner, 1, 1) - elseif CustomMatchGroupInput.placementCheckSpecialStatus(indexedScores) then - data.winner = MatchGroupInput.getDefaultWinner(indexedScores) - data.resulttype = DEFAULT_RESULT_TYPE - if MatchGroupInput.hasForfeit(indexedScores) then - data.walkover = STATUS_FORFEIT - elseif MatchGroupInput.hasDisqualified(indexedScores) then - data.walkover = STATUS_DISQUALIFIED - elseif MatchGroupInput.hasDefaultWinLoss(indexedScores) then - data.walkover = STATUS_DEFAULT_LOSS - end - indexedScores = MatchGroupInput.setPlacement(indexedScores, data.winner, 1, 2) - elseif CustomMatchGroupInput.placementCheckScoresSet(indexedScores) then - --C-OPS only has exactly 2 opponents, neither more or less - if #indexedScores == MAX_NUM_OPPONENTS then - if tonumber(indexedScores[1].score) > tonumber(indexedScores[2].score) then - data.winner = 1 - else - data.winner = 2 - end - indexedScores = MatchGroupInput.setPlacement(indexedScores, data.winner, 1, 2) - end - end - --If a manual winner is set use it - if winner and data.resulttype ~= DEFAULT_RESULT_TYPE then - if winner == 0 then - data.resulttype = DRAW_RESULT_TYPE - else - data.resulttype = nil - end - data.winner = winner - indexedScores = MatchGroupInput.setPlacement(indexedScores, winner, 1, 2) - end - end - return data, indexedScores -end + match.stream = Streams.processStreams(match) + match.links = MatchFunctions.getLinks(match) + match.games = games + match.opponents = opponents --- Check if any team has a none-standard status ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckSpecialStatus(tbl) - return Table.any(tbl, - function (_, scoreinfo) - return scoreinfo.status ~= STATUS_SCORE and String.isNotEmpty(scoreinfo.status) - end - ) -end + match.extradata = MatchFunctions.getExtraData(match) ----@param tbl table ----@return boolean -function CustomMatchGroupInput.placementCheckScoresSet(tbl) - return Table.all(tbl, function (_, scoreinfo) return scoreinfo.status == STATUS_SCORE end) + return match end -- @@ -185,159 +85,71 @@ end -- ---@param match table ----@return table -function matchFunctions.getBestOf(match) - local mapCount = 0 - for _, _, mapIndex in Table.iter.pairsByPrefix(match, 'map') do - mapCount = mapIndex - end - match.bestof = mapCount - return match -end - --- Template:Map sets a default map name so we can count the number of maps. --- These maps however shouldn't be stored in lpdb, nor displayed --- The discardMap function will check if a map should be removed --- Remove all maps that should be removed. ----@param match table ----@return table -function matchFunctions.removeUnsetMaps(match) - for mapKey, map in Table.iter.pairsByPrefix(match, 'map') do - if map.map == DUMMY_MAP_NAME then - match[mapKey] = nil +---@param opponentCount integer +---@return table[] +function MatchFunctions.extractMaps(match, opponentCount) + local maps = {} + for key, map in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + map.extradata = MapFunctions.getExtraData(map, opponentCount) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + local opponentInfo = Array.map(Array.range(1, opponentCount), function(opponentIndex) + local score, status = MatchGroupInputUtil.computeOpponentScore({ + walkover = map.walkover, + winner = map.winner, + opponentIndex = opponentIndex, + score = map['score' .. opponentIndex], + }) + return {score = score, status = status} + end) + + map.scores = Array.map(opponentInfo, Operator.property('score')) + if map.finished then + map.resulttype = MatchGroupInputUtil.getResultType(winnerInput, finishedInput, opponentInfo) + map.walkover = MatchGroupInputUtil.getWalkover(map.resulttype, opponentInfo) + map.winner = MatchGroupInputUtil.getWinner(map.resulttype, winnerInput, opponentInfo) end - end - return match -end --- Calculate the match scores based on the map results. --- If it's a Best of 1, we'll take the exact score of that map --- If it's not a Best of 1, we should count the map wins --- Only update a teams result if it's --- 1) Not manually added --- 2) At least one map has a winner ----@param match table ----@return table -function matchFunctions.getScoreFromMapWinners(match) - -- For best of 1, display the results of the single map - local opponent1 = match.opponent1 - local opponent2 = match.opponent2 - local newScores = {} - local foundScores = false - if match.bestof == 1 then - if match.map1 then - newScores = match.map1.scores - foundScores = true - end - else -- For best of >1, disply the map wins - for _, map in Table.iter.pairsByPrefix(match, 'map') do - local winner = tonumber(map.winner) - foundScores = true - -- Only two opponents in C-OPS - if winner and winner > 0 and winner <= 2 then - newScores[winner] = (newScores[winner] or 0) + 1 - end - end + table.insert(maps, map) + match[key] = nil end - if not opponent1.score and foundScores then - opponent1.score = newScores[1] or 0 - end - if not opponent2.score and foundScores then - opponent2.score = newScores[2] or 0 - end - match.opponent1 = opponent1 - match.opponent2 = opponent2 - return match -end ----@param match table ----@return table -function matchFunctions.getTournamentVars(match) - match.mode = Logic.emptyOr(match.mode, Variables.varDefault('tournament_mode', 'team')) - return MatchGroupInput.getCommonTournamentVars(match) + return maps end ----@param match table ----@return table -function matchFunctions.getVodStuff(match) - match.stream = Streams.processStreams(match) - match.vod = Logic.emptyOr(match.vod, Variables.varDefault('vod')) - return match +-- Template:Map sets a default map name so we can count the number of maps. +-- These maps however shouldn't be stored +-- The keepMap function will check if a map should be kept +---@param games table[] +---@return table[] +function MatchFunctions.removeUnsetMaps(games) + return Array.filter(games, MapFunctions.keepMap) end ----@param match table ----@return string? -function matchFunctions.getMatchStatus(match) - if match.resulttype == NOT_PLAYED_RESULT_TYPE then - return match.status - else - return nil +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(maps) + return function(opponentIndex) + return MatchGroupInputUtil.computeMatchScoreFromMapWinners(maps, opponentIndex) end end ---@param match table ---@return table -function matchFunctions.getExtraData(match) - match.extradata = { - mapveto = MatchGroupInput.getMapVeto(match), - status = matchFunctions.getMatchStatus(match), - } - return match +function MatchFunctions.getLinks(match) + return {} end ---@param match table ---@return table -function matchFunctions.getOpponents(match) - -- read opponents and ignore empty ones - local opponents = {} - local isScoreSet = false - for opponentIndex = 1, MAX_NUM_OPPONENTS do - -- read opponent - local opponent = match['opponent' .. opponentIndex] - if not Logic.isEmpty(opponent) then - CustomMatchGroupInput.processOpponent(opponent, match.timestamp) - - -- apply status - if TypeUtil.isNumeric(opponent.score) then - opponent.status = STATUS_SCORE - isScoreSet = true - elseif Table.includes(ALLOWED_STATUSES, opponent.score) then - opponent.status = opponent.score - opponent.score = NOT_PLAYED_SCORE - end - opponents[opponentIndex] = opponent - end - end - - -- Handle tournament status for unfinished matches - if (not Logic.readBool(match.finished)) and Logic.isNotEmpty(match.status) then - match.finished = match.status - end - - if Table.includes(NOT_PLAYED_MATCH_STATUSES, match.finished) then - match.resulttype = NOT_PLAYED_MATCH_STATUSES - match.status = match.finished - match.finished = false - match.dateexact = false - else - -- see if match should actually be finished if score is set - if isScoreSet and not Logic.readBool(match.finished) and match.timestamp ~= DateExt.defaultTimestamp then - local threshold = match.dateexact and 30800 or 86400 - if match.timestamp + threshold < NOW then - match.finished = true - end - end - - if Logic.readBool(match.finished) then - match, opponents = CustomMatchGroupInput.getResultTypeAndWinner(match, opponents) - end - end - - -- Update all opponents with new values - for opponentIndex, opponent in pairs(opponents) do - match['opponent' .. opponentIndex] = opponent - end - return match +function MatchFunctions.getExtraData(match) + return { + status = match.resulttype == MatchGroupInputUtil.RESULT_TYPE.NOT_PLAYED and match.status or nil, + mapveto = MatchGroupInputUtil.getMapVeto(match), + } end -- @@ -348,104 +160,15 @@ end -- DUMMY_MAP_NAME needs the match the default value in Template:Map ---@param map table ---@return boolean -function mapFunctions.discardMap(map) - return map.map == DUMMY_MAP_NAME +function MapFunctions.keepMap(map) + return map.map ~= DUMMY_MAP end --- Parse extradata information ---@param map table +---@param opponentCount integer ---@return table -function mapFunctions.getExtraData(map) - map.extradata = { - comment = map.comment, - } - return map -end - ----@param map table ----@return table -function mapFunctions._getHalfScores(map) - map.extradata.t1sides = {} - map.extradata.t2sides = {} - map.extradata.t1halfs = {} - map.extradata.t2halfs = {} - - local key = '' - local overtimes = 0 - - local function getOppositeSide(side) - return side == SIDE_DEF and SIDE_ATK or SIDE_DEF - end - - while true do - local t1Side = map[key .. 't1firstside'] - if Logic.isEmpty(t1Side) or (t1Side ~= SIDE_DEF and t1Side ~= SIDE_ATK) then - break - end - local t2Side = getOppositeSide(t1Side) - - -- Iterate over two Halfs (In regular time a half is 15 rounds, after that sides switch) - for _ = 1, 2, 1 do - if(map[key .. 't1' .. t1Side] and map[key .. 't2' .. t2Side]) then - table.insert(map.extradata.t1sides, t1Side) - table.insert(map.extradata.t2sides, t2Side) - table.insert(map.extradata.t1halfs, tonumber(map[key .. 't1' .. t1Side]) or 0) - table.insert(map.extradata.t2halfs, tonumber(map[key .. 't2' .. t2Side]) or 0) - map[key .. 't1' .. t1Side] = nil - map[key .. 't2' .. t2Side] = nil - -- second half (sides switch) - t1Side, t2Side = t2Side, t1Side - end - end - - overtimes = overtimes + 1 - key = 'o' .. overtimes - end - - return map -end - --- Calculate Score and Winner of the map --- Use the half information if available ----@param map table ----@return table -function mapFunctions.getScoresAndWinner(map) - map.scores = {} - local indexedScores = {} - - map = mapFunctions._getHalfScores(map) - - for scoreIndex = 1, MAX_NUM_OPPONENTS do - -- read scores - local score - if Table.includes(ALLOWED_STATUSES, map['score' .. scoreIndex]) then - score = map['score' .. scoreIndex] - elseif Logic.isNotEmpty(map.extradata['t' .. scoreIndex .. 'halfs']) then - score = MathUtil.sum(map.extradata['t' .. scoreIndex .. 'halfs']) - else - score = tonumber(map['score' .. scoreIndex]) - end - local obj = {} - if not Logic.isEmpty(score) then - if TypeUtil.isNumeric(score) then - obj.status = STATUS_SCORE - obj.score = score - elseif Table.includes(ALLOWED_STATUSES, score) then - obj.status = score - obj.score = NOT_PLAYED_SCORE - end - map.scores[scoreIndex] = score - indexedScores[scoreIndex] = obj - end - end - - if Table.includes(NOT_PLAYED_MATCH_STATUSES, map.finished) then - map.resulttype = NOT_PLAYED_RESULT_TYPE - else - map = CustomMatchGroupInput.getResultTypeAndWinner(map, indexedScores) - end - - return map +function MapFunctions.getExtraData(map, opponentCount) + return {comment = map.comment} end return CustomMatchGroupInput diff --git a/components/match2/wikis/zula/match_summary.lua b/components/match2/wikis/zula/match_summary.lua index fdc1ae6d9ee..18edf49195c 100644 --- a/components/match2/wikis/zula/match_summary.lua +++ b/components/match2/wikis/zula/match_summary.lua @@ -18,11 +18,6 @@ local MatchSummary = Lua.import('Module:MatchSummary/Base') local GREEN_CHECK = Icon.makeIcon{iconName = 'winner', color = 'forest-green-text', size = '110%'} local NO_CHECK = '[[File:NoCheck.png|link=]]' -local ARROW_LEFT = '[[File:Arrow sans left.svg|15x15px|link=|Left team starts]]' -local ARROW_RIGHT = '[[File:Arrow sans right.svg|15x15px|link=|Right team starts]]' - -local TBD = 'TBD' - -- Score Class ---@class ZulaScore ---@operator call(string|number|nil): ZulaScore @@ -103,123 +98,6 @@ function Score:create() return self.root end --- Map Veto Class ----@class ZulaMapVeto: MatchSummaryRowInterface ----@operator call: ZulaMapVeto ----@field root Html ----@field table Html -local MapVeto = Class.new( - function(self) - self.root = mw.html.create('div'):addClass('brkts-popup-mapveto') - self.table = self.root:tag('table') - :addClass('wikitable-striped'):addClass('collapsible'):addClass('collapsed') - self:createHeader() - end -) - ----@return ZulaMapVeto -function MapVeto:createHeader() - self.table:tag('tr') - :tag('th'):css('width','33%'):done() - :tag('th'):css('width','34%'):wikitext('Map Veto'):done() - :tag('th'):css('width','33%'):done() - return self -end - ----@param firstVeto number? ----@return ZulaMapVeto -function MapVeto:vetoStart(firstVeto) - local textLeft - local textCenter - local textRight - if firstVeto == 1 then - textLeft = 'Start Map Veto' - textCenter = ARROW_LEFT - elseif firstVeto == 2 then - textCenter = ARROW_RIGHT - textRight = 'Start Map Veto' - else return self end - self.table:tag('tr'):addClass('brkts-popup-mapveto-vetostart') - :tag('th'):wikitext(textLeft or ''):done() - :tag('th'):wikitext(textCenter):done() - :tag('th'):wikitext(textRight or ''):done() - return self -end - ----@param map string? ----@return ZulaMapVeto -function MapVeto:addDecider(map) - map = Logic.emptyOr(map, TBD) - - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - self:addColumnVetoMap(row, map) - self:addColumnVetoType(row, 'brkts-popup-mapveto-decider', 'DECIDER') - - self.table:node(row) - return self -end - ----@param vetotype string? ----@param map1 string? ----@param map2 string? ----@return ZulaMapVeto -function MapVeto:addRound(vetotype, map1, map2) - map1 = Logic.emptyOr(map1, TBD) - map2 = Logic.emptyOr(map2, TBD) - - local class - local vetoText - if vetotype == 'ban' then - vetoText = 'BAN' - class = 'brkts-popup-mapveto-ban' - elseif vetotype == 'pick' then - vetoText = 'PICK' - class = 'brkts-popup-mapveto-pick' - elseif vetotype == 'defaultban' then - vetoText = 'DEFAULT BAN' - class = 'brkts-popup-mapveto-defaultban' - else - return self - end - - local row = mw.html.create('tr'):addClass('brkts-popup-mapveto-vetoround') - - self:addColumnVetoMap(row, map1) - self:addColumnVetoType(row, class, vetoText) - self:addColumnVetoMap(row, map2) - - self.table:node(row) - return self -end - ----@param row Html ----@param styleClass string ----@param vetoText string ----@return ZulaMapVeto -function MapVeto:addColumnVetoType(row, styleClass, vetoText) - row:tag('td') - :tag('span') - :addClass(styleClass) - :addClass('brkts-popup-mapveto-vetotype') - :wikitext(vetoText) - return self -end - ----@param row Html ----@param map string? ----@return ZulaMapVeto -function MapVeto:addColumnVetoMap(row, map) - row:tag('td'):wikitext(map):done() - return self -end - ----@return Html -function MapVeto:create() - return self.root -end - ---@class ZulaMatchStatus: MatchSummaryRowInterface ---@operator call: ZulaMatchStatus ---@field root Html @@ -277,25 +155,7 @@ function CustomMatchSummary.createBody(match) end -- Add the Map Vetoes - if match.extradata.mapveto then - local vetoData = match.extradata.mapveto - if vetoData then - local mapVeto = MapVeto() - - for _,vetoRound in ipairs(vetoData) do - if vetoRound.vetostart then - mapVeto:vetoStart(tonumber(vetoRound.vetostart)) - end - if vetoRound.type == 'decider' then - mapVeto:addDecider(vetoRound.decider) - else - mapVeto:addRound(vetoRound.type, vetoRound.team1, vetoRound.team2) - end - end - - body:addRow(mapVeto) - end - end + body:addRow(MatchSummary.defaultMapVetoDisplay(match)) -- Match Status (postponed/ cancel(l)ed) if match.extradata.status then @@ -318,8 +178,8 @@ function CustomMatchSummary._createMap(game) local team2Score = Score('rtl'):setRight() -- Teams map score - team1Score:setMapScore(game.scores[1]) - team2Score:setMapScore(game.scores[2]) + team1Score:setMapScore(DisplayHelper.MapScore(game.scores[1], 1, game.resultType, game.walkover, game.winner)) + team2Score:setMapScore(DisplayHelper.MapScore(game.scores[2], 2, game.resultType, game.walkover, game.winner)) local t1sides = extradata['t1sides'] or {} local t2sides = extradata['t2sides'] or {} diff --git a/components/match_ticker/commons/match_ticker.lua b/components/match_ticker/commons/match_ticker.lua index 3200747bf10..afb66bb2246 100644 --- a/components/match_ticker/commons/match_ticker.lua +++ b/components/match_ticker/commons/match_ticker.lua @@ -47,6 +47,7 @@ local DEFAULT_QUERY_COLUMNS = { 'bestof', 'match2id', 'match2bracketdata', + 'match2games', } local NONE = 'none' local INFOBOX_DEFAULT_CLASS = 'fo-nttax-infobox panel' diff --git a/components/match_ticker/commons/match_ticker_display_components.lua b/components/match_ticker/commons/match_ticker_display_components.lua index 4c801512b17..2d632bd0e5c 100644 --- a/components/match_ticker/commons/match_ticker_display_components.lua +++ b/components/match_ticker/commons/match_ticker_display_components.lua @@ -19,6 +19,7 @@ local LeagueIcon = require('Module:LeagueIcon') local Logic = require('Module:Logic') local Lua = require('Module:Lua') local Page = require('Module:Page') +local Operator = require('Module:Operator') local String = require('Module:StringUtils') local Table = require('Module:Table') local Timezone = require('Module:Timezone') @@ -297,8 +298,23 @@ function Details:countdown(matchPageIcon) :addClass('match-countdown') :node(Countdown._create(countdownArgs)) - if String.isNotEmpty(match.vod) then - countdownDisplay:node(VodLink.display{vod = match.vod}) + if Logic.readBool(match.finished) then + local function makeVod(vod, num) + if Logic.isEmpty(vod) then + return nil + end + return VodLink.display{ + vod = vod, + gamenum = num, + } + end + + local gameVods = Array.map(Array.map(match.match2games, Operator.property('vod')), makeVod) + + countdownDisplay:node(makeVod(match.vod)) + Array.forEach(gameVods, function(vod) + countdownDisplay:node(vod) + end) end return mw.html.create('div') diff --git a/components/match_ticker/commons/match_ticker_display_components_new.lua b/components/match_ticker/commons/match_ticker_display_components_new.lua index 9b8625e5790..ebf215f2593 100644 --- a/components/match_ticker/commons/match_ticker_display_components_new.lua +++ b/components/match_ticker/commons/match_ticker_display_components_new.lua @@ -117,12 +117,33 @@ function Details:create() self.root:addClass(HIGHLIGHT_CLASS) end + local matchBottomBar = mw.html.create('div'):addClass('match-bottom-bar') + matchBottomBar:node(self:countdown()) + + if self.match.match2bracketdata.matchpage then + matchBottomBar:node(Page.makeInternalLink(tostring(mw.html.create('div') + :addClass('btn btn-secondary btn-new btn--match-details') + :attr('title', 'View Match Page') + :node(mw.html.create('i') + :addClass('fas fa-external-link') + ) + :wikitext(' Details') + ), self.match.match2bracketdata.matchpage)) + elseif self.match.match2id then + local link = 'Match:ID ' .. self.match.match2id + matchBottomBar:node(Page.makeInternalLink(tostring(mw.html.create('div') + :addClass('btn btn-new btn--add-match-details show-when-logged-in') + :attr('title', 'Add Match Page') + :wikitext('+ Add details') + ), link)) + end + return self.root :node(mw.html.create('div'):addClass('match-links') :node(self:tournament()) :node(self:streams()) ) - :node(self:countdown()) + :node(matchBottomBar) end ---It will display both countdown and date of the match so the user can select which one to show diff --git a/components/opponent/wikis/stormgate/player_ext_custom.lua b/components/opponent/wikis/stormgate/player_ext_custom.lua index 2f3825d4090..e928a33e5c6 100644 --- a/components/opponent/wikis/stormgate/player_ext_custom.lua +++ b/components/opponent/wikis/stormgate/player_ext_custom.lua @@ -52,7 +52,11 @@ end) function CustomPlayerExt.fetchPlayerFaction(resolvedPageName, date) local lpdbPlayer = CustomPlayerExt.fetchPlayer(resolvedPageName) if lpdbPlayer and lpdbPlayer.factionHistory then - date = date or DateExt.getContextualDateOrNow() + local timestamp = DateExt.readTimestamp(date or DateExt.getContextualDateOrNow()) + ---@cast timestamp -nil + -- convert date to iso format to match the dates retrieved from the data points + -- need the time too so the below check remains the same as before + date = DateExt.formatTimestamp('Y-m-d H:i:s', timestamp) local entry = Array.find(lpdbPlayer.factionHistory, function(entry) return date <= entry.endDate end) return entry and Faction.read(entry.faction) else diff --git a/components/opponent/wikis/trackmania/opponent_display_custom.lua b/components/opponent/wikis/trackmania/opponent_display_custom.lua index 650d1dde8b3..df1d57e3fc7 100644 --- a/components/opponent/wikis/trackmania/opponent_display_custom.lua +++ b/components/opponent/wikis/trackmania/opponent_display_custom.lua @@ -32,27 +32,27 @@ function OpponentDisplayCustom.BracketOpponentEntry:addScores(opponent) local extradata = opponent.extradata or {} if not extradata.additionalScores then OpponentDisplay.BracketOpponentEntry.addScores(self, opponent) - else + return + end + self.root:node(OpponentDisplay.BracketScore{ + isWinner = extradata.set1win, + scoreText = OpponentDisplayCustom.InlineScore(opponent, 1), + }) + if opponent.extradata.score2 or opponent.score2 then self.root:node(OpponentDisplay.BracketScore{ - isWinner = extradata.set1win, - scoreText = OpponentDisplayCustom.InlineScore(opponent, ''), + isWinner = extradata.set2win, + scoreText = OpponentDisplayCustom.InlineScore(opponent, 2), }) - if opponent.extradata.score2 or opponent.score2 then - self.root:node(OpponentDisplay.BracketScore{ - isWinner = extradata.set2win, - scoreText = OpponentDisplayCustom.InlineScore(opponent, 2), - }) - end - if opponent.extradata.score3 then - self.root:node(OpponentDisplay.BracketScore{ - isWinner = extradata.set3win, - scoreText = OpponentDisplayCustom.InlineScore(opponent, 3) - }) - end - if (opponent.placement2 or opponent.placement or 0) == 1 - or opponent.advances then - self.content:addClass('brkts-opponent-win') - end + end + if opponent.extradata.score3 then + self.root:node(OpponentDisplay.BracketScore{ + isWinner = extradata.set3win, + scoreText = OpponentDisplayCustom.InlineScore(opponent, 3) + }) + end + if (opponent.placement2 or opponent.placement or 0) == 1 + or opponent.advances then + self.content:addClass('brkts-opponent-win') end end diff --git a/components/opponent/wikis/warcraft/opponent_custom.lua b/components/opponent/wikis/warcraft/opponent_custom.lua index 653c8ff0e82..b671eff6839 100644 --- a/components/opponent/wikis/warcraft/opponent_custom.lua +++ b/components/opponent/wikis/warcraft/opponent_custom.lua @@ -37,7 +37,7 @@ function CustomOpponent.readOpponentArgs(args) end if partySize == 1 then - opponent.players[1].faction = Faction.read(args.faction or args.race) + opponent.players[1].faction = Faction.read(args.faction or args.race or args.p1race) elseif partySize then for playerIx, player in ipairs(opponent.players) do player.faction = Faction.read(args['p' .. playerIx .. 'faction'] or args['p' .. playerIx .. 'race']) diff --git a/components/opponent/wikis/warcraft/player_ext_custom.lua b/components/opponent/wikis/warcraft/player_ext_custom.lua index 904dcc0a4d0..dc8c216d103 100644 --- a/components/opponent/wikis/warcraft/player_ext_custom.lua +++ b/components/opponent/wikis/warcraft/player_ext_custom.lua @@ -52,7 +52,11 @@ end) function CustomPlayerExt.fetchPlayerFaction(resolvedPageName, date) local lpdbPlayer = CustomPlayerExt.fetchPlayer(resolvedPageName) if lpdbPlayer and lpdbPlayer.factionHistory then - date = date or DateExt.getContextualDateOrNow() + local timestamp = DateExt.readTimestamp(date or DateExt.getContextualDateOrNow()) + ---@cast timestamp -nil + -- convert date to iso format to match the dates retrieved from the data points + -- need the time too so the below check remains the same as before + date = DateExt.formatTimestamp('Y-m-d H:i:s', timestamp) local entry = Array.find(lpdbPlayer.factionHistory, function(entry) return date <= entry.endDate end) return entry and Faction.read(entry.faction) else diff --git a/components/prize_pool/commons/prize_pool.lua b/components/prize_pool/commons/prize_pool.lua index c0bbb771311..e6d1bd199f6 100644 --- a/components/prize_pool/commons/prize_pool.lua +++ b/components/prize_pool/commons/prize_pool.lua @@ -19,7 +19,7 @@ local Placement = Lua.import('Module:PrizePool/Placement') local OpponentLibrary = require('Module:OpponentLibraries') local Opponent = OpponentLibrary.Opponent -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local TableRow = Widgets.TableRow local TableCell = Widgets.TableCell @@ -54,7 +54,7 @@ end ---@return WidgetTableCell function PrizePool:placeOrAwardCell(placement) local placeCell = TableCell{ - content = {{placement:getMedal() or '', NON_BREAKING_SPACE, placement:_displayPlace()}}, + children = {placement:getMedal() or '', NON_BREAKING_SPACE, placement:_displayPlace()}, css = {['font-weight'] = 'bolder'}, classes = {'prizepooltable-place'}, } @@ -91,9 +91,9 @@ end ---@return WidgetTableRow function PrizePool:_toggleExpand(placeStart, placeEnd) local text = 'place ' .. placeStart .. ' to ' .. placeEnd - local expandButton = TableCell{content = {'' .. text .. ' '}} + local expandButton = TableCell{children = {'' .. text .. ' '}} :addClass('general-collapsible-expand-button') - local collapseButton = TableCell{content = {'' .. text .. ' '}} + local collapseButton = TableCell{children = {'' .. text .. ' '}} :addClass('general-collapsible-collapse-button') return TableRow{classes = {'ppt-toggle-expand'}}:addCell(expandButton):addCell(collapseButton) diff --git a/components/prize_pool/commons/prize_pool_award.lua b/components/prize_pool/commons/prize_pool_award.lua index 9738a1864c1..3aae6b8141d 100644 --- a/components/prize_pool/commons/prize_pool_award.lua +++ b/components/prize_pool/commons/prize_pool_award.lua @@ -18,7 +18,7 @@ local Placement = Lua.import('Module:PrizePool/Award/Placement') local OpponentLibrary = require('Module:OpponentLibraries') local Opponent = OpponentLibrary.Opponent -local Widgets = require('Module:Infobox/Widget/All') +local Widgets = require('Module:Widget/All') local TableRow = Widgets.TableRow local TableCell = Widgets.TableCell @@ -53,7 +53,7 @@ end ---@return WidgetTableCell function AwardPrizePool:placeOrAwardCell(placement) local awardCell = TableCell{ - content = {placement.award}, + children = {placement.award}, css = {['font-weight'] = 'bolder'}, classes = {'prizepooltable-place'}, } @@ -85,9 +85,9 @@ end ---@return WidgetTableRow function AwardPrizePool:_toggleExpand() - local expandButton = TableCell{content = {'Show more Awards '}} + local expandButton = TableCell{children = {'Show more Awards '}} :addClass('general-collapsible-expand-button') - local collapseButton = TableCell{content = {'Show less Awards '}} + local collapseButton = TableCell{children = {'Show less Awards '}} :addClass('general-collapsible-collapse-button') return TableRow{classes = {'ppt-toggle-expand'}}:addCell(expandButton):addCell(collapseButton) diff --git a/components/prize_pool/commons/prize_pool_base.lua b/components/prize_pool/commons/prize_pool_base.lua index db34f306f74..392c4f4eb3c 100644 --- a/components/prize_pool/commons/prize_pool_base.lua +++ b/components/prize_pool/commons/prize_pool_base.lua @@ -26,11 +26,11 @@ local OpponentLibraries = require('Module:OpponentLibraries') local Opponent = OpponentLibraries.Opponent local OpponentDisplay = OpponentLibraries.OpponentDisplay -local Widgets = require('Module:Infobox/Widget/All') -local WidgetFactory = Lua.import('Module:Widget/Factory') +local Widgets = require('Module:Widget/All') local WidgetTable = Widgets.Table local TableRow = Widgets.TableRow local TableCell = Widgets.TableCell +local Div = Widgets.Div local pageVars = PageVariableNamespace('PrizePool') @@ -149,7 +149,7 @@ BasePrizePool.prizeTypes = { headerDisplay = function (data) local currencyText = Currency.display(BASE_CURRENCY) - return TableCell{content = {{currencyText}}} + return TableCell{children = {currencyText}} end, row = BASE_CURRENCY:lower() .. 'prize', @@ -158,7 +158,7 @@ BasePrizePool.prizeTypes = { end, rowDisplay = function (headerData, data) if data > 0 then - return TableCell{content = { + return TableCell{children = { Currency.display(BASE_CURRENCY, data, {formatValue = true, formatPrecision = headerData.roundPrecision, displayCurrencyCode = false}) }} @@ -188,7 +188,7 @@ BasePrizePool.prizeTypes = { } end, headerDisplay = function (data) - return TableCell{content = {{Currency.display(data.currency)}}} + return TableCell{children = {Currency.display(data.currency)}} end, row = 'localprize', @@ -197,7 +197,7 @@ BasePrizePool.prizeTypes = { end, rowDisplay = function (headerData, data) if data > 0 then - return TableCell{content = { + return TableCell{children = { Currency.display(headerData.currency, data, {formatValue = true, formatPrecision = headerData.roundPrecision, displayCurrencyCode = false}) }} @@ -226,7 +226,7 @@ BasePrizePool.prizeTypes = { return {title = 'Percentage'} end, headerDisplay = function (data) - return TableCell{content = {{data.title}}} + return TableCell{children = {data.title}} end, row = 'percentage', @@ -240,7 +240,7 @@ BasePrizePool.prizeTypes = { end, rowDisplay = function (headerData, data) if String.isNotEmpty(data) then - return TableCell{content = {{data .. '%'}}} + return TableCell{children = {data .. '%'}} end end, }, @@ -266,7 +266,7 @@ BasePrizePool.prizeTypes = { } end, headerDisplay = function (data) - return TableCell{content = {'Qualifies To'}} + return TableCell{children = {'Qualifies To'}} end, row = 'qualified', @@ -294,7 +294,7 @@ BasePrizePool.prizeTypes = { table.insert(content, '[[' .. headerData.link .. ']]') end - return TableCell{content = {content}} + return TableCell{children = {Div{children = content}}} end, mergeDisplayColumns = true, @@ -339,7 +339,7 @@ BasePrizePool.prizeTypes = { table.insert(headerDisplay, text) end - return TableCell{content = {headerDisplay}} + return TableCell{children = {table.concat(headerDisplay)}} end, row = 'points', @@ -348,7 +348,7 @@ BasePrizePool.prizeTypes = { end, rowDisplay = function (headerData, data) if data > 0 then - return TableCell{content = {{LANG:formatNum(data)}}} + return TableCell{children = {LANG:formatNum(data)}} end end, }, @@ -360,7 +360,7 @@ BasePrizePool.prizeTypes = { return {title = input} end, headerDisplay = function (data) - return TableCell{content = {{data.title}}} + return TableCell{children = {data.title}} end, row = 'freetext', @@ -369,7 +369,7 @@ BasePrizePool.prizeTypes = { end, rowDisplay = function (headerData, data) if String.isNotEmpty(data) then - return TableCell{content = {{data}}} + return TableCell{children = {data}} end end, } @@ -608,9 +608,7 @@ function BasePrizePool:_buildTable(isAward) end local tableNode = mw.html.create('div'):css('overflow-x', 'auto') - for _, node in ipairs(WidgetFactory.work(tbl, self._widgetInjector)) do - tableNode:node(node) - end + tableNode:node(tbl:tryMake(self._widgetInjector)) return tableNode end @@ -620,7 +618,7 @@ end function BasePrizePool:_buildHeader(isAward) local headerRow = TableRow{classes = {'prizepooltable-header'}, css = {['font-weight'] = 'bold'}} - headerRow:addCell(TableCell{content = {isAward and 'Award' or 'Place'}, css = {['min-width'] = '80px'}}) + headerRow:addCell(TableCell{children = {isAward and 'Award' or 'Place'}, css = {['min-width'] = '80px'}}) local previousOfType = {} for _, prize in ipairs(self.prizes) do @@ -633,7 +631,7 @@ function BasePrizePool:_buildHeader(isAward) end end - headerRow:addCell(TableCell{content = {'Participant'}, classes = {'prizepooltable-col-team'}}) + headerRow:addCell(TableCell{children = {'Participant'}, classes = {'prizepooltable-col-team'}}) return headerRow end @@ -667,11 +665,11 @@ function BasePrizePool:_buildRows() local lastCellOfType = previousOfPrizeType[prize.type] if lastCellOfType and prizeTypeData.mergeDisplayColumns then - if Table.isNotEmpty(lastCellOfType.content) and Table.isNotEmpty(cell.content) then + if Table.isNotEmpty(lastCellOfType.children) and Table.isNotEmpty(cell.children) then lastCellOfType:addContent(tostring(mw.html.create('hr'):css('width', '100%'))) end - Array.extendWith(lastCellOfType.content, cell.content) + Array.extendWith(lastCellOfType.children, cell.children) lastCellOfType.css['flex-direction'] = 'column' return nil @@ -685,11 +683,11 @@ function BasePrizePool:_buildRows() local lastInColumn = previousOpponent[columnIndex] ---@cast prizeCell -nil - if Table.isEmpty(prizeCell.content) then + if Table.isEmpty(prizeCell.children) then prizeCell = BasePrizePool._emptyCell() end - if lastInColumn and Table.deepEquals(lastInColumn.content, prizeCell.content) then + if lastInColumn and Table.deepEquals(lastInColumn.children, prizeCell.children) then lastInColumn.rowSpan = (lastInColumn.rowSpan or 1) + 1 else previousOpponent[columnIndex] = prizeCell @@ -704,7 +702,7 @@ function BasePrizePool:_buildRows() }) local opponentCss = {['justify-content'] = 'start'} - row:addCell(TableCell{content = {opponentDisplay}, css = opponentCss}) + row:addCell(TableCell{children = {opponentDisplay}, css = opponentCss}) end table.insert(rows, row) @@ -815,7 +813,7 @@ end --- Creates an empty table cell ---@return WidgetTableCell function BasePrizePool._emptyCell() - return TableCell{content = {DASH}} + return TableCell{children = {DASH}} end --- Remove all non-numeric characters from an input and changes it to a number. @@ -921,7 +919,7 @@ end ---@param widgetInjector WidgetInjector An instance of a class that implements the WidgetInjector interface ---@return self function BasePrizePool:setWidgetInjector(widgetInjector) - assert(widgetInjector:is_a(WidgetInjector), 'setWidgetInjector: Not a Widget Injector') + assert(Class.instanceOf(widgetInjector, WidgetInjector), 'setWidgetInjector: Not a Widget Injector') self._widgetInjector = widgetInjector return self end @@ -930,7 +928,7 @@ end ---@param lpdbInjector LpdbInjector An instance of a class that implements the LpdbInjector interface ---@return self function BasePrizePool:setLpdbInjector(lpdbInjector) - assert(lpdbInjector:is_a(LpdbInjector), 'setLpdbInjector: Not an LPDB Injector') + assert(Class.instanceOf(lpdbInjector, LpdbInjector), 'setLpdbInjector: Not an LPDB Injector') self._lpdbInjector = lpdbInjector return self end diff --git a/components/prize_pool/commons/prize_pool_import.lua b/components/prize_pool/commons/prize_pool_import.lua index cc94ce0ab99..908513a7e53 100644 --- a/components/prize_pool/commons/prize_pool_import.lua +++ b/components/prize_pool/commons/prize_pool_import.lua @@ -707,12 +707,12 @@ function Import._makeAdditionalDataFromMatch(opponentName, match) end local score, vsScore, lastVs - for _, opponent in pairs(match.match2opponents) do + for opponentIndex, opponent in pairs(match.match2opponents) do if opponent.name == opponentName then score = Import._getScore(opponent) else vsScore = Import._getScore(opponent) - lastVs = MatchGroupUtil.opponentFromRecord(opponent) + lastVs = MatchGroupUtil.opponentFromRecord(match, opponent, opponentIndex) end end diff --git a/components/prize_pool/wikis/arenaofvalor/prize_pool_custom.lua b/components/prize_pool/wikis/honorofkings/prize_pool_custom.lua similarity index 98% rename from components/prize_pool/wikis/arenaofvalor/prize_pool_custom.lua rename to components/prize_pool/wikis/honorofkings/prize_pool_custom.lua index 42f8766fe69..603fe18eeb3 100644 --- a/components/prize_pool/wikis/arenaofvalor/prize_pool_custom.lua +++ b/components/prize_pool/wikis/honorofkings/prize_pool_custom.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- wiki=arenaofvalor +-- wiki=honorofkings -- page=Module:PrizePool/Custom -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute diff --git a/components/squad/commons/squad.lua b/components/squad/commons/squad.lua deleted file mode 100644 index dc3306595a3..00000000000 --- a/components/squad/commons/squad.lua +++ /dev/null @@ -1,117 +0,0 @@ ---- --- @Liquipedia --- wiki=commons --- page=Module:Squad --- --- Please see https://github.com/Liquipedia/Lua-Modules to contribute --- - -local Arguments = require('Module:Arguments') -local Array = require('Module:Array') -local Class = require('Module:Class') -local FnUtil = require('Module:FnUtil') -local Logic = require('Module:Logic') -local Lua = require('Module:Lua') -local String = require('Module:StringUtils') - -local SquadUtils = Lua.import('Module:Squad/Utils') -local Widget = Lua.import('Module:Infobox/Widget/All') -local WidgetFactory = Lua.import('Module:Widget/Factory') - ----@class Squad ----@operator call:Squad ----@field args table ----@field root Html ----@field private injector WidgetInjector? ----@field rows WidgetTableRowNew[] ----@field type integer -local Squad = Class.new() - - ----@param args table|Frame|nil ----@param injector WidgetInjector? ----@return self -function Squad:init(args, injector) - self.args = Arguments.getArgs(args) - self.rows = {} - - self.injector = injector - self.type = - (SquadUtils.SquadTypeToStorageValue[self.args.type] and self.args.type) or - SquadUtils.statusToSquadType(self.args.status) or - SquadUtils.SquadType.ACTIVE - - return self -end - ----@return self -function Squad:title() - local defaultTitle - if self.type == SquadUtils.SquadType.FORMER or self.type == SquadUtils.SquadType.FORMER_INACTIVE then - defaultTitle = 'Former Squad' - elseif self.type == SquadUtils.SquadType.INACTIVE then - defaultTitle = 'Inactive Players' - end - - local titleText = Logic.emptyOr(self.args.title, defaultTitle) - - if String.isEmpty(titleText) then - return self - end - - table.insert(self.rows, Widget.TableRowNew{ - children = {Widget.TableCellNew{content = {titleText}, colSpan = 10, header = true}} - }) - - return self -end - ----@return self -function Squad:header() - local isInactive = self.type == SquadUtils.SquadType.INACTIVE or self.type == SquadUtils.SquadType.FORMER_INACTIVE - local isFormer = self.type == SquadUtils.SquadType.FORMER or self.type == SquadUtils.SquadType.FORMER_INACTIVE - table.insert(self.rows, Widget.TableRowNew{ - classes = {'HeaderRow'}, - children = Array.append({}, - Widget.TableCellNew{content = {'ID'}, header = true}, - Widget.TableCellNew{header = true}, -- "Team Icon" (most commmonly used for loans) - Widget.Customizable{id = 'header_name', - children = {Widget.TableCellNew{content = {'Name'}, header = true}} - }, - Widget.Customizable{id = 'header_role', - children = {Widget.TableCellNew{header = true}} - }, - Widget.TableCellNew{content = {'Join Date'}, header = true}, - isInactive and Widget.Customizable{id = 'header_inactive', children = { - Widget.TableCellNew{content = {'Inactive Date'}, header = true}, - }} or nil, - isFormer and Widget.Customizable{id = 'header_former', children = { - Widget.TableCellNew{content = {'Leave Date'}, header = true}, - Widget.TableCellNew{content = {'New Team'}, header = true}, - }} or nil - ) - }) - - return self -end - ----@param row WidgetTableRowNew ----@return self -function Squad:row(row) - table.insert(self.rows, row) - return self -end - ----@return Html -function Squad:create() - local dataTable = Widget.TableNew{ - css = {['margin-bottom'] = '10px'}, - classes = {'wikitable-striped', 'roster-card'}, - children = self.rows, - } - local wrapper = mw.html.create() - Array.forEach(WidgetFactory.work(dataTable, self.injector), FnUtil.curry(wrapper.node, wrapper)) - return wrapper -end - -return Squad diff --git a/components/squad/commons/squad_custom.lua b/components/squad/commons/squad_custom.lua index 07821a832c7..d0979482a34 100644 --- a/components/squad/commons/squad_custom.lua +++ b/components/squad/commons/squad_custom.lua @@ -9,14 +9,14 @@ local Info = require('Module:Info') local Lua = require('Module:Lua') -local Squad = Lua.import('Module:Squad') +local Squad = Lua.import('Module:Widget/Squad/Core') local SquadRow = Lua.import('Module:Squad/Row') local SquadUtils = Lua.import('Module:Squad/Utils') local CustomSquad = {} ---@param frame Frame ----@return Html +---@return string function CustomSquad.run(frame) if not Info.config.squads.allowManual then error('This wiki does not use manual squad tables') @@ -28,7 +28,7 @@ end ---@param playerList table[] ---@param squadType integer ---@param customTitle string? ----@return Html? +---@return string function CustomSquad.runAuto(playerList, squadType, customTitle) return SquadUtils.defaultRunAuto(playerList, squadType, Squad, SquadUtils.defaultRow(SquadRow), customTitle) end diff --git a/components/squad/commons/squad_row.lua b/components/squad/commons/squad_row.lua index 1799caa0639..0c51ad54c3b 100644 --- a/components/squad/commons/squad_row.lua +++ b/components/squad/commons/squad_row.lua @@ -15,14 +15,14 @@ local OpponentDisplay = OpponentLib.OpponentDisplay local String = require('Module:StringUtils') local Template = require('Module:Template') -local Widget = Lua.import('Module:Infobox/Widget/All') +local Widget = Lua.import('Module:Widget/All') local RoleIcons = { captain = Icon.makeIcon{iconName = 'captain', hover = 'Captain'}, sub = Icon.makeIcon{iconName = 'substitute', hover = 'Substitute'}, } ----@class SquadRow +---@class SquadRow: BaseClass ---@operator call(ModelRow): SquadRow ---@field children Widget[] ---@field model ModelRow diff --git a/components/squad/commons/squad_utils.lua b/components/squad/commons/squad_utils.lua index 1dbdac4d394..9d81ba4937b 100644 --- a/components/squad/commons/squad_utils.lua +++ b/components/squad/commons/squad_utils.lua @@ -23,7 +23,7 @@ local Lpdb = Lua.import('Module:Lpdb') local Faction = Lua.import('Module:Faction') local SquadAutoRefs = Lua.import('Module:SquadAuto/References') local Injector = Lua.import('Module:Widget/Injector') -local Widget = Lua.import('Module:Infobox/Widget/All') +local Widget = Lua.import('Module:Widget/All') local SquadUtils = {} @@ -178,52 +178,55 @@ function SquadUtils.storeSquadPerson(squadPerson) end ---@param frame table ----@param squadClass Squad ----@param personFunction fun(player: table, squadType: integer):WidgetTableRowNew +---@param squadWidget SquadWidget +---@param rowCreator fun(player: table, squadType: integer):WidgetTableRowNew ---@param injector WidgetInjector? ----@return Html -function SquadUtils.defaultRunManual(frame, squadClass, personFunction, injector) +---@return string +function SquadUtils.defaultRunManual(frame, squadWidget, rowCreator, injector) local args = Arguments.getArgs(frame) - local injectorInstance = (injector and injector()) or - (Info.config.squads.hasPosition and SquadUtils.positionHeaderInjector()()) or - nil - local squad = squadClass(args, injectorInstance):title() - local players = SquadUtils.parsePlayers(squad.args) - - if squad.type == SquadUtils.SquadType.FORMER and SquadUtils.anyInactive(players) then - squad.type = SquadUtils.SquadType.FORMER_INACTIVE - end + local props = { + type = SquadUtils.statusToSquadType(args.status) or SquadUtils.SquadType.ACTIVE, + title = args.title, + injector = (injector and injector()) or + (Info.config.squads.hasPosition and SquadUtils.positionHeaderInjector()()) or + nil, + } + local players = SquadUtils.parsePlayers(args) - squad:header() + if props.type == SquadUtils.SquadType.FORMER and SquadUtils.anyInactive(players) then + props.type = SquadUtils.SquadType.FORMER_INACTIVE + end - Array.forEach(players, function(player) - squad:row(personFunction(player, squad.type)) + props.children = Array.map(players, function(player) + return rowCreator(player, props.type) end) - return squad:create() + return tostring(squadWidget(props)) end ---@param players table[] ----@param squadType integer ----@param squadClass Squad +---@param squadType SquadType +---@param squadWidget SquadWidget ---@param rowCreator fun(person: table, squadType: integer):WidgetTableRowNew ---@param customTitle string? ---@param injector? WidgetInjector ---@param personMapper? fun(person: table): table ----@return Html? -function SquadUtils.defaultRunAuto(players, squadType, squadClass, rowCreator, customTitle, injector, personMapper) - local args = {type = squadType, title = customTitle} - local injectorInstance = (injector and injector()) or - (Info.config.squads.hasPosition and SquadUtils.positionHeaderInjector()()) or - nil - local squad = squadClass(args, injectorInstance):title():header() +---@return string +function SquadUtils.defaultRunAuto(players, squadType, squadWidget, rowCreator, customTitle, injector, personMapper) + local props = { + type = squadType, + title = customTitle, + injector = (injector and injector()) or + (Info.config.squads.hasPosition and SquadUtils.positionHeaderInjector()()) or + nil, + } local mappedPlayers = Array.map(players, personMapper or SquadUtils.convertAutoParameters) - Array.forEach(mappedPlayers, function(player) - squad:row(rowCreator(player, squad.type)) + props.children = Array.map(mappedPlayers, function(player) + return rowCreator(player, props.type) end) - return squad:create() + return tostring(squadWidget(props)) end ---@param squadRowClass SquadRow diff --git a/components/squad/wikis/dota2/squad_custom.lua b/components/squad/wikis/dota2/squad_custom.lua index 1131e8bf475..374f787332d 100644 --- a/components/squad/wikis/dota2/squad_custom.lua +++ b/components/squad/wikis/dota2/squad_custom.lua @@ -9,9 +9,9 @@ local Class = require('Module:Class') local Lua = require('Module:Lua') local Table = require('Module:Table') -local Widget = require('Module:Infobox/Widget/All') +local Widget = require('Module:Widget/All') -local Squad = Lua.import('Module:Squad') +local Squad = Lua.import('Module:Widget/Squad/Core') local SquadRow = Lua.import('Module:Squad/Row') local SquadUtils = Lua.import('Module:Squad/Utils') @@ -35,6 +35,7 @@ function ExtendedSquadRow:activeteam() local date = self.model.inactivedate if not activeTeam then + table.insert(self.children,Widget.TableCellNew{classes = {'NewTeam'}, content = {}}) return self end @@ -55,7 +56,7 @@ function ExtendedSquadRow:activeteam() end ---@param frame Frame ----@return Html +---@return string function CustomSquad.run(frame) return SquadUtils.defaultRunManual(frame, Squad, CustomSquad._playerRow, CustomInjector) end diff --git a/components/squad/wikis/overwatch/squad_custom.lua b/components/squad/wikis/overwatch/squad_custom.lua index 6a63cf30a77..8a4246d0db3 100644 --- a/components/squad/wikis/overwatch/squad_custom.lua +++ b/components/squad/wikis/overwatch/squad_custom.lua @@ -13,9 +13,9 @@ local Lua = require('Module:Lua') local Operator = require('Module:Operator') local String = require('Module:StringUtils') local Table = require('Module:Table') -local Widget = require('Module:Infobox/Widget/All') +local Widget = require('Module:Widget/All') -local Squad = Lua.import('Module:Squad') +local Squad = Lua.import('Module:Widget/Squad/Core') local SquadRow = Lua.import('Module:Squad/Row') local SquadUtils = Lua.import('Module:Squad/Utils') @@ -48,28 +48,29 @@ function ExtendedSquadRow:number() end ---@param frame Frame ----@return Html +---@return string function CustomSquad.run(frame) local args = Arguments.getArgs(frame) - local squad = Squad(args, CustomInjector()):title() - - local players = SquadUtils.parsePlayers(squad.args) + local props = { + injector = CustomInjector(), + type = SquadUtils.statusToSquadType(args.status) or SquadUtils.SquadType.ACTIVE, + title = args.title, + } + local players = SquadUtils.parsePlayers(args) HAS_NUMBER = Array.any(players, Operator.property('number')) - squad:header() - - Array.forEach(players, function(player) - squad:row(CustomSquad._playerRow(player, squad.type)) + props.children = Array.map(players, function(player) + return CustomSquad._playerRow(player, props.type) end) - return squad:create() + return tostring(Squad(props)) end ---@param playerList table[] ---@param squadType integer ---@param customTitle string? ----@return Html? +---@return string? function CustomSquad.runAuto(playerList, squadType, customTitle) return SquadUtils.defaultRunAuto(playerList, squadType, Squad, SquadUtils.defaultRow(SquadRow), customTitle) end diff --git a/components/squad/wikis/smash/squad_custom.lua b/components/squad/wikis/smash/squad_custom.lua index 2009d2524f7..a51d2acf4f3 100644 --- a/components/squad/wikis/smash/squad_custom.lua +++ b/components/squad/wikis/smash/squad_custom.lua @@ -14,9 +14,9 @@ local Lua = require('Module:Lua') local SquadPlayerData = require('Module:SquadPlayer/data') local Table = require('Module:Table') local Variables = require('Module:Variables') -local Widget = require('Module:Infobox/Widget/All') +local Widget = require('Module:Widget/All') -local Squad = Lua.import('Module:Squad') +local Squad = Lua.import('Module:Widget/Squad/Core') local SquadRow = Lua.import('Module:Squad/Row') local SquadUtils = Lua.import('Module:Squad/Utils') @@ -54,22 +54,26 @@ function ExtendedSquadRow:mains() end ---@param frame Frame ----@return Html +---@return string function CustomSquad.run(frame) local args = Arguments.getArgs(frame) - local squad = Squad(args, CustomInjector()):title():header() + local props = { + injector = CustomInjector(), + type = SquadUtils.statusToSquadType(args.status) or SquadUtils.SquadType.ACTIVE, + title = args.title, + } - local tableGame = squad.args.game + local tableGame = args.game - local players = SquadUtils.parsePlayers(squad.args) + local players = SquadUtils.parsePlayers(args) - Array.forEach(players, function(person) + props.children = Array.map(players, function(person) local game = person.game and mw.text.split(person.game:lower(), ',')[1] or tableGame local mains = SquadPlayerData.get{link = person.link, player = person.id, game = game} or person.mains person.flag = Variables.varDefault('nationality') or person.flag person.name = Variables.varDefault('name') or person.name - local squadPerson = SquadUtils.readSquadPersonArgs(Table.merge(person, {type = squad.type})) + local squadPerson = SquadUtils.readSquadPersonArgs(Table.merge(person, {type = props.type})) squadPerson.extradata.game = game squadPerson.extradata.mains = mains SquadUtils.storeSquadPerson(squadPerson) @@ -79,11 +83,11 @@ function CustomSquad.run(frame) row:id():name() row:mains():date('joindate', 'Join Date: ') - if squad.type == SquadUtils.SquadType.INACTIVE or squad.type == SquadUtils.SquadType.FORMER_INACTIVE then + if props.type == SquadUtils.SquadType.INACTIVE or props.type == SquadUtils.SquadType.FORMER_INACTIVE then row:date('inactivedate', 'Inactive Date: ') end - if squad.type == SquadUtils.SquadType.FORMER or squad.type == SquadUtils.SquadType.FORMER_INACTIVE then + if props.type == SquadUtils.SquadType.FORMER or props.type == SquadUtils.SquadType.FORMER_INACTIVE then row:date('leavedate', 'Leave Date: ') row:newteam() end @@ -91,11 +95,11 @@ function CustomSquad.run(frame) Variables.varDefine('nationality', '') Variables.varDefine('name', '') - squad:row(row:create()) + return row:create() end) - return squad:create() + return tostring(Squad(props)) end return CustomSquad diff --git a/components/squad/wikis/starcraft/squad_custom.lua b/components/squad/wikis/starcraft/squad_custom.lua index 5cfd68b0a1d..b5ee2b3682c 100644 --- a/components/squad/wikis/starcraft/squad_custom.lua +++ b/components/squad/wikis/starcraft/squad_custom.lua @@ -12,35 +12,14 @@ local Logic = require('Module:Logic') local Lua = require('Module:Lua') local String = require('Module:StringUtils') local Table = require('Module:Table') -local Widget = require('Module:Infobox/Widget/All') +local Widget = require('Module:Widget/All') -local Squad = Lua.import('Module:Squad') +local Squad = Lua.import('Module:Widget/Squad/Core') +local SquadTldb = Lua.import('Module:Widget/Squad/Core/Tldb') local SquadRow = Lua.import('Module:Squad/Row') local SquadUtils = Lua.import('Module:Squad/Utils') local CustomSquad = {} -local TlpdSquad = Class.new(Squad) - ----@return self -function TlpdSquad:header() - table.insert(self.rows, Widget.TableRowNew{ - classes = {'HeaderRow'}, - cells = { - Widget.TableCellNew{content = {'ID'}, header = true}, - Widget.TableCellNew{header = true}, -- "Team Icon" (most commmonly used for loans) - Widget.TableCellNew{content = {'Name'}, header = true}, - Widget.TableCellNew{content = {'ELO'}, header = true}, - Widget.TableCellNew{content = {'ELO Peak'}, header = true}, - } - }) - - return self -end - ----@return self -function TlpdSquad:title() - return self -end ---@class StarcraftSquadRow: SquadRow local ExtendedSquadRow = Class.new(SquadRow) @@ -59,11 +38,11 @@ function ExtendedSquadRow:elo() end ---@param frame Frame ----@return Html +---@return string function CustomSquad.run(frame) local args = Arguments.getArgs(frame) local tlpd = Logic.readBool(args.tlpd) - local SquadClass = tlpd and TlpdSquad or Squad + local SquadClass = tlpd and SquadTldb or Squad return SquadUtils.defaultRunManual(frame, SquadClass, function(person, squadType) local inputId = person.id --[[@as number]] diff --git a/components/squad/wikis/starcraft2/squad_custom.lua b/components/squad/wikis/starcraft2/squad_custom.lua index 7659410be8e..ab5551c02c5 100644 --- a/components/squad/wikis/starcraft2/squad_custom.lua +++ b/components/squad/wikis/starcraft2/squad_custom.lua @@ -11,14 +11,14 @@ local Logic = require('Module:Logic') local Lua = require('Module:Lua') local Table = require('Module:Table') -local Squad = Lua.import('Module:Squad') +local Squad = Lua.import('Module:Widget/Squad/Core') local SquadRow = Lua.import('Module:Squad/Row') local SquadUtils = Lua.import('Module:Squad/Utils') local CustomSquad = {} ---@param frame Frame ----@return Html +---@return string function CustomSquad.run(frame) return SquadUtils.defaultRunManual(frame, Squad, CustomSquad._playerRow) end @@ -26,7 +26,7 @@ end ---@param playerList table[] ---@param squadType integer ---@param customTitle string? ----@return Html? +---@return string? function CustomSquad.runAuto(playerList, squadType, customTitle) return SquadUtils.defaultRunAuto( playerList, diff --git a/components/squad/wikis/stormgate/squad_custom.lua b/components/squad/wikis/stormgate/squad_custom.lua index e0de2461311..713a5e512db 100644 --- a/components/squad/wikis/stormgate/squad_custom.lua +++ b/components/squad/wikis/stormgate/squad_custom.lua @@ -10,14 +10,14 @@ local Logic = require('Module:Logic') local Lua = require('Module:Lua') local Table = require('Module:Table') -local Squad = Lua.import('Module:Squad') +local Squad = Lua.import('Module:Widget/Squad/Core') local SquadRow = Lua.import('Module:Squad/Row') local SquadUtils = Lua.import('Module:Squad/Utils') local CustomSquad = {} ---@param frame Frame ----@return Html +---@return string function CustomSquad.run(frame) return SquadUtils.defaultRunManual(frame, Squad, CustomSquad._playerRow) end @@ -25,7 +25,7 @@ end ---@param playerList table[] ---@param squadType integer ---@param customTitle string? ----@return Html? +---@return string? function CustomSquad.runAuto(playerList, squadType, customTitle) return SquadUtils.defaultRunAuto( playerList, diff --git a/components/statistics/count.lua b/components/statistics/count.lua index 0ef745d8f02..95fd76fae0a 100644 --- a/components/statistics/count.lua +++ b/components/statistics/count.lua @@ -180,24 +180,22 @@ function Count.placements(args) elseif String.isNotEmpty(args.team) then local opponentConditions = ConditionTree(BooleanOperator.any) - Array.map(Count._getOpponentNames(args.team), - function(templateValue) - return opponentConditions:add{ + Array.forEach(Count._getOpponentNames(args.team), function(templateValue) + opponentConditions:add{ ConditionNode(ColumnName('opponentname'), Comparator.eq, templateValue), ConditionNode(ColumnName('opponentname'), Comparator.eq, templateValue:gsub(' ', '_')) } - end - ) + end) lpdbConditions:add{opponentConditions} end if String.isNotEmpty(args.placement) then local placementConditions = ConditionTree(BooleanOperator.any) - Array.map(Array.map(mw.text.split(args.placement, ',', true), String.trim), - function(placementValue) - return placementConditions:add{ - ConditionNode(ColumnName('placement'), Comparator.eq, placementValue)} - end) + Array.forEach(Array.map(mw.text.split(args.placement, ',', true), String.trim), + function(placementValue) + placementConditions:add{ConditionNode(ColumnName('placement'), Comparator.eq, placementValue)} + end + ) lpdbConditions:add{placementConditions} end diff --git a/components/statistics/portal_statistics.lua b/components/statistics/portal_statistics.lua index 344178168e0..70e5a786fa3 100644 --- a/components/statistics/portal_statistics.lua +++ b/components/statistics/portal_statistics.lua @@ -1022,6 +1022,7 @@ function StatisticsPortal._cacheOpponentPlacementData(args) .. 'opponentplayers, opponentname, opponenttype', conditions = conditions:toString(), limit = 1000, + order = 'date asc', } local function makeOpponentTable(item) diff --git a/components/widget/widget_breakdown.lua b/components/widget/infobox/widget_infobox_breakdown.lua similarity index 75% rename from components/widget/widget_breakdown.lua rename to components/widget/infobox/widget_infobox_breakdown.lua index 15623203c0e..cbaf0cce219 100644 --- a/components/widget/widget_breakdown.lua +++ b/components/widget/infobox/widget_infobox_breakdown.lua @@ -1,7 +1,7 @@ --- -- @Liquipedia -- wiki=commons --- page=Module:Widget/Breakdown +-- page=Module:Widget/Infobox/Breakdown -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- @@ -12,29 +12,27 @@ local Lua = require('Module:Lua') local Widget = Lua.import('Module:Widget') ---@class BreakdownWidget: Widget ----@operator call({content:(string|number)[],classes:string[],contentClasses:table}):BreakdownWidget ----@field contents (string|number)[] +---@operator call({children:(string|number)[],classes:string[],contentClasses:table}):BreakdownWidget ---@field classes string[] ---@field contentClasses table --can have gaps in the outer table local Breakdown = Class.new( Widget, function(self, input) - self.contents = input.content self.classes = input.classes self.contentClasses = input.contentClasses or {} end ) ----@param injector WidgetInjector? ----@return {[1]: Html?} -function Breakdown:make(injector) - return {Breakdown:_breakdown(self.contents, self.classes, self.contentClasses)} +---@param children string[] +---@return string? +function Breakdown:make(children) + return Breakdown:_breakdown(children, self.classes, self.contentClasses) end ---@param contents (string|number)[] ---@param classes string[] ---@param contentClasses table --can have gaps in the outer table ----@return Html? +---@return string? function Breakdown:_breakdown(contents, classes, contentClasses) if type(contents) ~= 'table' or contents == {} then return nil @@ -54,7 +52,7 @@ function Breakdown:_breakdown(contents, classes, contentClasses) div:node(infoboxCustomCell) end - return div + return tostring(div) end return Breakdown diff --git a/components/widget/widget_cell.lua b/components/widget/infobox/widget_infobox_cell.lua similarity index 87% rename from components/widget/widget_cell.lua rename to components/widget/infobox/widget_infobox_cell.lua index 36e7e6d651c..a11c5f84041 100644 --- a/components/widget/widget_cell.lua +++ b/components/widget/infobox/widget_infobox_cell.lua @@ -1,7 +1,7 @@ --- -- @Liquipedia -- wiki=commons --- page=Module:Widget/Cell +-- page=Module:Widget/Infobox/Cell -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- @@ -19,13 +19,12 @@ local Widget = Lua.import('Module:Widget') ---@class CellWidget: Widget ---@operator call({name:string|number,content:(string|number)[],classes:string[]?,options:CellWidgetOptions}):CellWidget ---@field name string|number ----@field content (string|number)[] ---@field options CellWidgetOptions ---@field classes string[]? local Cell = Class.new(Widget, function(self, input) self.name = self:assertExistsAndCopy(input.name) - self.content = input.content + self.children = input.children or input.content or {} self.options = input.options or {} self.classes = input.classes @@ -60,7 +59,7 @@ function Cell:_class(...) return self end ----@param ... string|number +---@param ... string ---@return CellWidget function Cell:_content(...) local firstItem = select(1, ...) @@ -75,7 +74,7 @@ function Cell:_content(...) if i > 1 then self.contentDiv:wikitext('') end - local item = select(i, ...) + local item = select(i, ...) ---@type string? if item == nil then break end @@ -89,22 +88,20 @@ function Cell:_content(...) return self end ----@param injector WidgetInjector? ----@return {[1]: Html?} -function Cell:make(injector) +---@param children string[] +---@return string? +function Cell:make(children) self:_new(self.name) self:_class(unpack(self.classes or {})) - self:_content(unpack(self.content)) + self:_content(unpack(children)) if self.contentDiv == nil then - return {} + return end self.root :node(self.description) :node(self.contentDiv) - return { - self.root - } + return tostring(self.root) end return Cell diff --git a/components/widget/widget_center.lua b/components/widget/infobox/widget_infobox_center.lua similarity index 67% rename from components/widget/widget_center.lua rename to components/widget/infobox/widget_infobox_center.lua index 3122a9a9f12..6fd73ddfbc3 100644 --- a/components/widget/widget_center.lua +++ b/components/widget/infobox/widget_infobox_center.lua @@ -1,7 +1,7 @@ --- -- @Liquipedia -- wiki=commons --- page=Module:Widget/Center +-- page=Module:Widget/Infobox/Center -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- @@ -13,26 +13,24 @@ local Table = require('Module:Table') local Widget = Lua.import('Module:Widget') ---@class CentereWidget: Widget ----@operator call({content: (string|number)[], classes: string[]}): CentereWidget ----@field content (string|number)[] +---@operator call(table): CentereWidget ---@field classes string[] local Center = Class.new( Widget, function(self, input) - self.content = input.content self.classes = input.classes end ) ----@param injector WidgetInjector? ----@return {[1]: Html?} -function Center:make(injector) - return {Center:_create(self.content, self.classes)} +---@param children string[] +---@return string? +function Center:make(children) + return Center:_create(children, self.classes) end ---@param content (string|number)[] ---@param classes string[] ----@return Html? +---@return string? function Center:_create(content, classes) if Table.isEmpty(content) then return nil @@ -47,7 +45,7 @@ function Center:_create(content, classes) centered:wikitext(item) end - return mw.html.create('div'):node(centered) + return tostring(mw.html.create('div'):node(centered)) end return Center diff --git a/components/widget/widget_chronology.lua b/components/widget/infobox/widget_infobox_chronology.lua similarity index 81% rename from components/widget/widget_chronology.lua rename to components/widget/infobox/widget_infobox_chronology.lua index e3d50e0cf0e..32f2149af99 100644 --- a/components/widget/widget_chronology.lua +++ b/components/widget/infobox/widget_infobox_chronology.lua @@ -1,7 +1,7 @@ --- -- @Liquipedia -- wiki=commons --- page=Module:Widget/Chronology +-- page=Module:Widget/Infobox/Chronology -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- @@ -13,43 +13,43 @@ local Table = require('Module:Table') local Widget = Lua.import('Module:Widget') ---@class ChronologyWidget: Widget ----@operator call({content: table}): ChronologyWidget +---@operator call({links: table}): ChronologyWidget ---@field links table local Chronology = Class.new( Widget, function(self, input) - self.links = input.content + self.links = input.links end ) ----@param injector WidgetInjector? ----@return Html[] -function Chronology:make(injector) +---@param children string[] +---@return string? +function Chronology:make(children) return Chronology:_chronology(self.links) end ---@param links table ----@return Html[] +---@return string? function Chronology:_chronology(links) if links == nil or Table.size(links) == 0 then - return self + return end - local chronologyContent = {} - chronologyContent[1] = self:_createChronologyRow(links['previous'], links['next']) + local chronologyContent = mw.html.create() + chronologyContent:node(self:_createChronologyRow(links['previous'], links['next'])) local index = 2 local previous = links['previous' .. index] local next = links['next' .. index] while (previous ~= nil or next ~= nil) do - chronologyContent[index] = self:_createChronologyRow(previous, next) + chronologyContent:node(self:_createChronologyRow(previous, next)) index = index + 1 previous = links['previous' .. index] next = links['next' .. index] end - return chronologyContent + return tostring(chronologyContent) end ---@param previous string|number|nil diff --git a/components/widget/infobox/widget_infobox_core.lua b/components/widget/infobox/widget_infobox_core.lua new file mode 100644 index 00000000000..d628bc455b5 --- /dev/null +++ b/components/widget/infobox/widget_infobox_core.lua @@ -0,0 +1,52 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:Widget/Infobox/Core +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Class = require('Module:Class') +local Lua = require('Module:Lua') +local Variables = require('Module:Variables') +local WarningBox = require('Module:WarningBox') + +local Widget = Lua.import('Module:Widget') +local Div = Lua.import('Module:Widget/Div') +local Fragment = Lua.import('Module:Widget/Fragment') + +---@class Infobox: Widget +---@operator call(table): Infobox +---@field props table +local Infobox = Class.new(Widget, function(self, props) + self.props = props +end) + +---@param children string[] +---@return string +function Infobox:make(children) + local firstInfobox = not Variables.varDefault('has_infobox') + Variables.varDefine('has_infobox', 'true') + + local adbox = Div{classes = {'fo-nttax-infobox-adbox'}, children = {mw.getCurrentFrame():preprocess('')}} + local content = Div{classes = {'fo-nttax-infobox'}, children = children} + local bottomContent = Div{children = self.props.bottomContent} + + return tostring(Fragment{children = { + Div{ + classes = { + 'fo-nttax-infobox-wrapper', + 'infobox-' .. self.props.gameName:lower(), + self.props.forceDarkMode and 'infobox-darkmodeforced' or nil, + }, + children = { + content, + firstInfobox and adbox or nil, + bottomContent, + } + }, + WarningBox.displayAll(self.props.warnings), + }}) +end + +return Infobox diff --git a/components/widget/widget_header.lua b/components/widget/infobox/widget_infobox_header.lua similarity index 95% rename from components/widget/widget_header.lua rename to components/widget/infobox/widget_infobox_header.lua index a232ea4f105..b293096bac4 100644 --- a/components/widget/widget_header.lua +++ b/components/widget/infobox/widget_infobox_header.lua @@ -1,7 +1,7 @@ --- -- @Liquipedia -- wiki=commons --- page=Module:Widget/Header +-- page=Module:Widget/Infobox/Header -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- @@ -35,9 +35,9 @@ local Header = Class.new( end ) ----@param injector WidgetInjector? ----@return Html[] -function Header:make(injector) +---@param children string[] +---@return string +function Header:make(children) local header = { Header:_name(self.name), Header:_image( @@ -59,7 +59,11 @@ function Header:make(injector) table.insert(header, 2, subHeader) end - return header + local wrapper = mw.html.create() + for _, element in ipairs(header) do + wrapper:node(element) + end + return tostring(wrapper) end ---@param name string? diff --git a/components/widget/widget_highlights.lua b/components/widget/infobox/widget_infobox_highlights.lua similarity index 62% rename from components/widget/widget_highlights.lua rename to components/widget/infobox/widget_infobox_highlights.lua index 3d431c56862..2dba456abda 100644 --- a/components/widget/widget_highlights.lua +++ b/components/widget/infobox/widget_infobox_highlights.lua @@ -1,7 +1,7 @@ --- -- @Liquipedia -- wiki=commons --- page=Module:Widget/Highlights +-- page=Module:Widget/Infobox/Highlights -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- @@ -13,23 +13,17 @@ local Table = require('Module:Table') local Widget = Lua.import('Module:Widget') ---@class HighlightsWidget: Widget ----@operator call({content: (string|number)[]?}):HighlightsWidget ----@field list (string|number)[]? -local Highlights = Class.new( - Widget, - function(self, input) - self.list = input.content - end -) +---@operator call(table):HighlightsWidget +local Highlights = Class.new(Widget) ----@param injector WidgetInjector? ----@return {[1]: Html}? -function Highlights:make(injector) - return Highlights:_highlights(self.list) +---@param children string[] +---@return string? +function Highlights:make(children) + return Highlights:_highlights(children) end ---@param list (string|number)[]? ----@return Html? +---@return string? function Highlights:_highlights(list) if list == nil or Table.size(list) == 0 then return nil @@ -44,7 +38,7 @@ function Highlights:_highlights(list) div:node(highlights) - return {div} + return tostring(div) end return Highlights diff --git a/components/widget/widget_links.lua b/components/widget/infobox/widget_infobox_links.lua similarity index 84% rename from components/widget/widget_links.lua rename to components/widget/infobox/widget_infobox_links.lua index ba2a1157c9f..8d863b8be8a 100644 --- a/components/widget/widget_links.lua +++ b/components/widget/infobox/widget_infobox_links.lua @@ -1,7 +1,7 @@ --- -- @Liquipedia -- wiki=commons --- page=Module:Widget/Links +-- page=Module:Widget/Infobox/Links -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- @@ -14,22 +14,22 @@ local UtilLinks = Lua.import('Module:Links') local Widget = Lua.import('Module:Widget') ---@class LinksWidget: Widget ----@operator call({content: table, variant: string?}): LinksWidget +---@operator call({links: table, variant: string?}): LinksWidget ---@field links table ---@field variant string? local Links = Class.new( Widget, function(self, input) - self.links = Table.copy(input.content) + self.links = Table.copy(input.links) self.variant = input.variant end ) local PRIORITY_GROUPS = Lua.import('Module:Links/PriorityGroups', {loadData = true}) ----@param injector WidgetInjector? ----@return {[1]: Html} -function Links:make(injector) +---@param children string[] +---@return string? +function Links:make(children) local infoboxLinks = mw.html.create('div') infoboxLinks :addClass('infobox-center') :addClass('infobox-icons') @@ -56,9 +56,7 @@ function Links:make(injector) infoboxLinks:wikitext(' ' .. self:_makeLink(key, value)) end - return { - mw.html.create('div'):node(infoboxLinks) - } + return tostring(mw.html.create('div'):node(infoboxLinks)) end ---@param key string diff --git a/components/widget/widget_title.lua b/components/widget/infobox/widget_infobox_title.lua similarity index 54% rename from components/widget/widget_title.lua rename to components/widget/infobox/widget_infobox_title.lua index 06ca2ef6287..4d59d262dc8 100644 --- a/components/widget/widget_title.lua +++ b/components/widget/infobox/widget_infobox_title.lua @@ -1,7 +1,7 @@ --- -- @Liquipedia -- wiki=commons --- page=Module:Widget/Title +-- page=Module:Widget/Infobox/Title -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- @@ -12,31 +12,33 @@ local Lua = require('Module:Lua') local Widget = Lua.import('Module:Widget') ---@class TitleWidget: Widget ----@operator call({name: string|number|nil}): TitleWidget ----@field content string|number|nil +---@operator call(table): TitleWidget local Title = Class.new( Widget, - function(self, input) - self.content = self:assertExistsAndCopy(input.name) + function(self) + -- Legacy support for single string children, convert to array + -- Widget v2.1 will have this support added to the base class + if type(self.children) == 'string' then + self.children = {self.children} + end end ) ----@param injector WidgetInjector? ----@return {[1]: Html} -function Title:make(injector) - return {Title:_create(self.content)} +---@param children string[] +---@return string? +function Title:make(children) + return Title:_create(table.concat(children)) end ---@param infoDescription string|number|nil ----@return Html +---@return string function Title:_create(infoDescription) local header = mw.html.create('div') header :addClass('infobox-header') :addClass('wiki-backgroundcolor-light') :addClass('infobox-header-2') :wikitext(infoDescription) - return mw.html.create('div'):node(header) + return tostring(mw.html.create('div'):node(header)) end return Title - diff --git a/components/widget/squad/widget_squad_core.lua b/components/widget/squad/widget_squad_core.lua new file mode 100644 index 00000000000..d5cbf132601 --- /dev/null +++ b/components/widget/squad/widget_squad_core.lua @@ -0,0 +1,96 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:Widget/Squad/Core +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local Class = require('Module:Class') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') +local String = require('Module:StringUtils') + +local SquadUtils = Lua.import('Module:Squad/Utils') +local Widgets = Lua.import('Module:Widget/All') +local Widget = Lua.import('Module:Widget') +local WidgetUtil = Lua.import('Module:Widget/Util') + +---@class SquadWidget: Widget +---@operator call:SquadWidget +---@field private injector WidgetInjector? +---@field type SquadType +---@field title string? +local Squad = Class.new(Widget, function(self, props) + self.injector = props.injector + self.type = props.type + self.title = props.title +end) + +---@param children string[] +---@return string +function Squad:make(children) + local title = self._title(self.type, self.title) + local header = self._header(self.type) + + local allChildren = WidgetUtil.collect(title, header, unpack(children)) + + return Widgets.TableNew{ + css = {['margin-bottom'] = '10px'}, + classes = {'wikitable-striped', 'roster-card'}, + children = allChildren, + }:tryMake(self.injector) or '' +end + +---@param type SquadType +---@param title string? +---@return WidgetTableRowNew? +function Squad._title(type, title) + local defaultTitle + if type == SquadUtils.SquadType.FORMER or type == SquadUtils.SquadType.FORMER_INACTIVE then + defaultTitle = 'Former Squad' + elseif type == SquadUtils.SquadType.INACTIVE then + defaultTitle = 'Inactive Players' + end + + local titleText = Logic.emptyOr(title, defaultTitle) + + if String.isEmpty(titleText) then + return + end + + return Widgets.TableRowNew{ + children = {Widgets.TableCellNew{content = {titleText}, colSpan = 10, header = true}} + } +end + +---@param type SquadType +---@return WidgetTableRowNew +function Squad._header(type) + local isInactive = type == SquadUtils.SquadType.INACTIVE or type == SquadUtils.SquadType.FORMER_INACTIVE + local isFormer = type == SquadUtils.SquadType.FORMER or type == SquadUtils.SquadType.FORMER_INACTIVE + return Widgets.TableRowNew{ + classes = {'HeaderRow'}, + children = Array.append({}, + Widgets.TableCellNew{content = {'ID'}, header = true}, + Widgets.TableCellNew{header = true}, -- "Team Icon" (most commmonly used for loans) + Widgets.Customizable{id = 'header_name', + children = {Widgets.TableCellNew{content = {'Name'}, header = true}} + }, + Widgets.Customizable{id = 'header_role', + children = {Widgets.TableCellNew{header = true}} + }, + Widgets.TableCellNew{content = {'Join Date'}, header = true}, + isInactive and Widgets.Customizable{id = 'header_inactive', children = { + Widgets.TableCellNew{content = {'Inactive Date'}, header = true}, + }} or nil, + isFormer and Widgets.Customizable{id = 'header_former', children = { + Widgets.TableCellNew{content = {'Leave Date'}, header = true}, + Widgets.TableCellNew{content = {'New Team'}, header = true}, + }} or nil + ) + } +end + +return Squad diff --git a/components/widget/squad/widget_squad_core_tldb.lua b/components/widget/squad/widget_squad_core_tldb.lua new file mode 100644 index 00000000000..63bc086c4d8 --- /dev/null +++ b/components/widget/squad/widget_squad_core_tldb.lua @@ -0,0 +1,41 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:Widget/Squad/Core/Tldb +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Class = require('Module:Class') +local Lua = require('Module:Lua') + +local Widgets = Lua.import('Module:Widget/All') +local SquadWidget = Lua.import('Module:Widget/Squad/Core') + +---@class SquadTldbWidget: SquadWidget +---@operator call:SquadTldbWidget +local SquadTldb = Class.new(SquadWidget) + +---@param type SquadType +---@return WidgetTableRowNew +function SquadTldb._header(type) + return Widgets.TableRowNew{ + classes = {'HeaderRow'}, + cells = { + Widgets.TableCellNew{content = {'ID'}, header = true}, + Widgets.TableCellNew{header = true}, -- "Team Icon" (most commmonly used for loans) + Widgets.TableCellNew{content = {'Name'}, header = true}, + Widgets.TableCellNew{content = {'ELO'}, header = true}, + Widgets.TableCellNew{content = {'ELO Peak'}, header = true}, + } + } +end + +---@param type SquadType +---@param title string? +---@return WidgetTableRowNew? +function SquadTldb._title(type, title) + return nil +end + +return SquadTldb diff --git a/components/widget/widget.lua b/components/widget/widget.lua index e6f35872d54..b2fce0220c6 100644 --- a/components/widget/widget.lua +++ b/components/widget/widget.lua @@ -5,14 +5,22 @@ -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- +local Array = require('Module:Array') local Class = require('Module:Class') local ErrorDisplay = require('Module:Error/Display') local Logic = require('Module:Logic') local String = require('Module:StringUtils') +---@class WidgetParameters +---@field children (Widget|Html|string|number)[]? + ---@class Widget: BaseClass ----@operator call(): Widget -local Widget = Class.new() +---@operator call(WidgetParameters): Widget +---@field children (Widget|Html|string|number)[] +---@field makeChildren? fun(self:Widget, injector: WidgetInjector?): Widget[]? +local Widget = Class.new(function(self, input) + self.children = input and input.children or {} +end) ---Asserts the existence of a value and copies it ---@param value string @@ -21,18 +29,19 @@ function Widget:assertExistsAndCopy(value) return assert(String.nilIfEmpty(value), 'Tried to set a nil value to a mandatory property') end ----@param injector WidgetInjector? ----@return Widget[]|Html[]|nil -function Widget:make(injector) +---@param children string[] +---@return string|nil +function Widget:make(children) error('A Widget must override the make() function!') end ---@param injector WidgetInjector? ----@return Widget[]|Html[]|nil +---@return string|nil function Widget:tryMake(injector) + local processedChildren = self:tryChildren(injector) return Logic.tryOrElseLog( - function() return self:make(injector) end, - function(error) return {ErrorDisplay.InlineError(error)} end, + function() return self:make(processedChildren) end, + function(error) return tostring(ErrorDisplay.InlineError(error)) end, function(error) error.header = 'Error occured in widget: (caught by Widget:tryMake)' return error @@ -40,4 +49,39 @@ function Widget:tryMake(injector) ) end +---@param injector WidgetInjector? +---@return string[] +function Widget:tryChildren(injector) + local children = self.children + if self.makeChildren then + children = self:makeChildren(injector) or {} + end + return Array.map(children, function(child) + if Class.instanceOf(child, Widget) then + ---@cast child Widget + return Logic.tryOrElseLog( + function() return child:tryMake(injector) end, + function(error) return tostring(ErrorDisplay.InlineError(error)) end, + function(error) + error.header = 'Error occured in widget: (caught by Widget:tryChildren)' + return error + end + ) + end + ---@cast child -Widget + return tostring(child) + end) +end + +---@return string +function Widget:__tostring() + return self:tryMake() or '' +end + +--- Here to allow for Widget to be used as a node in the third part html library (mw.html). +---@param ret string[] +function Widget:_build(ret) + table.insert(ret, self:__tostring()) +end + return Widget diff --git a/components/infobox/commons/infobox_widget_all.lua b/components/widget/widget_all.lua similarity index 56% rename from components/infobox/commons/infobox_widget_all.lua rename to components/widget/widget_all.lua index eb4b21b26a5..cda23810aef 100644 --- a/components/infobox/commons/infobox_widget_all.lua +++ b/components/widget/widget_all.lua @@ -1,7 +1,7 @@ --- -- @Liquipedia -- wiki=commons --- page=Module:Infobox/Widget/All +-- page=Module:Widget/All -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- @@ -15,14 +15,17 @@ Widgets.Builder = Lua.import('Module:Widget/Builder') Widgets.Customizable = Lua.import('Module:Widget/Customizable') --- Infobox Widgets -Widgets.Breakdown = Lua.import('Module:Widget/Breakdown') -Widgets.Cell = Lua.import('Module:Widget/Cell') -Widgets.Center = Lua.import('Module:Widget/Center') -Widgets.Chronology = Lua.import('Module:Widget/Chronology') -Widgets.Header = Lua.import('Module:Widget/Header') -Widgets.Highlights = Lua.import('Module:Widget/Highlights') -Widgets.Links = Lua.import('Module:Widget/Links') -Widgets.Title = Lua.import('Module:Widget/Title') +Widgets.Breakdown = Lua.import('Module:Widget/Infobox/Breakdown') +Widgets.Cell = Lua.import('Module:Widget/Infobox/Cell') +Widgets.Center = Lua.import('Module:Widget/Infobox/Center') +Widgets.Chronology = Lua.import('Module:Widget/Infobox/Chronology') +Widgets.Header = Lua.import('Module:Widget/Infobox/Header') +Widgets.Highlights = Lua.import('Module:Widget/Infobox/Highlights') +Widgets.Links = Lua.import('Module:Widget/Infobox/Links') +Widgets.Title = Lua.import('Module:Widget/Infobox/Title') + +--- Generic Widgets +Widgets.Div = Lua.import('Module:Widget/Div') --- Table Widgets (div-table) (might be removed) Widgets.Table = Lua.import('Module:Widget/Table') diff --git a/components/widget/widget_builder.lua b/components/widget/widget_builder.lua index 767f3ff6212..09637f7715a 100644 --- a/components/widget/widget_builder.lua +++ b/components/widget/widget_builder.lua @@ -6,12 +6,10 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- -local Array = require('Module:Array') local Class = require('Module:Class') local Lua = require('Module:Lua') local Widget = Lua.import('Module:Widget') -local WidgetFactory = Lua.import('Module:Widget/Factory') ---@class BuilderWidget: Widget ---@operator call({builder: function}): BuilderWidget @@ -23,15 +21,16 @@ local Builder = Class.new( end ) +---@param children string[] +---@return string +function Builder:make(children) + return table.concat(children) +end + ---@param injector WidgetInjector? ----@return Widget[] -function Builder:make(injector) - local children = self.builder() - local widgets = {} - for _, child in ipairs(children or {}) do - Array.extendWith(widgets, WidgetFactory.work(child, injector)) - end - return widgets +---@return Widget[]? +function Builder:makeChildren(injector) + return self.builder() end return Builder diff --git a/components/widget/widget_button.lua b/components/widget/widget_button.lua new file mode 100644 index 00000000000..29f4e1c33d3 --- /dev/null +++ b/components/widget/widget_button.lua @@ -0,0 +1,77 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:Widget/Button +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Class = require('Module:Class') +local Lua = require('Module:Lua') + +local Widget = Lua.import('Module:Widget') +local Link = Lua.import('Module:Widget/Link') +local Div = Lua.import('Module:Widget/Div') + +---@class ButtonWidgetParameters: WidgetParameters +---@field title string? +---@field link string? +---@field linktype 'internal'|'external'|nil +---@field variant 'primary'|'secondary'|'ghost'|nil +---@field size 'sm'|'md'|'lg'|nil + +---@class ButtonWidget: Widget +---@operator call(ButtonWidgetParameters): ButtonWidget +---@field title string? +---@field link string? +---@field linktype 'internal'|'external' +---@field variant 'primary'|'secondary'|'ghost' +---@field size 'sm'|'md'|'lg' + +local Button = Class.new( + Widget, + function(self, input) + self.title = input.title + self.link = input.link + self.linktype = input.linktype or 'internal' + self.variant = input.variant or 'primary' + self.size = input.size or 'md' + end +) + +---@param children string[] +---@return string +function Button:make(children) + --- MW Parser does not allowed the tag, so we use a div for now instead + local button = mw.html.create('div'):addClass('btn'):addClass('btn-new') + button:attr('title', self.title) + button:attr('aria-label', self.title) + button:attr('role', 'button') + button:attr('tabindex', '0') + + button:wikitext(table.concat(children)) + + if self.variant == 'primary' then + button:addClass('btn-primary') + elseif self.variant == 'secondary' then + button:addClass('btn-secondary') + end + + if self.size == 'sm' then + button:addClass('btn-small') + elseif self.size == 'lg' then + button:addClass('btn-large') + end + + if not self.link then + return tostring(button) + end + -- Have to wrap it in an extra div to prevent the mediawiki parser from messing it up + return tostring(Div{children = {Link{ + link = self.link, + linktype = self.linktype, + children = {button} + }}}) +end + +return Button diff --git a/components/widget/widget_customizable.lua b/components/widget/widget_customizable.lua index f7cb1fc2903..817a6559b30 100644 --- a/components/widget/widget_customizable.lua +++ b/components/widget/widget_customizable.lua @@ -14,18 +14,22 @@ local Widget = Lua.import('Module:Widget') ---@class CustomizableWidget: Widget ---@operator call({id: string, children: Widget[]}): CustomizableWidget ---@field id string ----@field children Widget[] local Customizable = Class.new( Widget, function(self, input) self.id = self:assertExistsAndCopy(input.id) - self.children = input.children end ) +---@param children string[] +---@return string +function Customizable:make(children) + return table.concat(children) +end + ---@param injector WidgetInjector? ---@return Widget[]? -function Customizable:make(injector) +function Customizable:makeChildren(injector) if injector == nil then return self.children end diff --git a/components/widget/widget_div.lua b/components/widget/widget_div.lua new file mode 100644 index 00000000000..8cd0dfa8095 --- /dev/null +++ b/components/widget/widget_div.lua @@ -0,0 +1,35 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:Widget/Div +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local Class = require('Module:Class') +local FnUtil = require('Module:FnUtil') +local Lua = require('Module:Lua') + +local Widget = Lua.import('Module:Widget') + +---@class GenericDivWidget: Widget +---@operator call({children: (Widget|string)[]?}?): GenericDivWidget +---@field classes string[] +local Div = Class.new( + Widget, + function(self, input) + self.classes = input.classes or {} + end +) + +---@param children string[] +---@return string? +function Div:make(children) + local div = mw.html.create('div') + Array.forEach(self.classes, FnUtil.curry(div.addClass, div)) + Array.forEach(children, FnUtil.curry(div.node, div)) + return tostring(div) +end + +return Div diff --git a/components/widget/widget_factory.lua b/components/widget/widget_factory.lua index 18d4312debc..3a5cab71010 100644 --- a/components/widget/widget_factory.lua +++ b/components/widget/widget_factory.lua @@ -6,36 +6,19 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- -local Array = require('Module:Array') local Class = require('Module:Class') local Lua = require('Module:Lua') -local Widget = Lua.import('Module:Widget') +local WidgetFactory = {} ----@class WidgetFactory -local WidgetFactory = Class.new() - ----@param widget Widget ----@param injector WidgetInjector? ----@return Html[] -function WidgetFactory.work(widget, injector) - local convertedWidgets = {} ---@type Html[] - - if widget == nil then - return {} - end - - for _, child in ipairs(widget:tryMake(injector) or {}) do - if type(child) == 'table' and type(child['is_a']) == 'function' and child:is_a(Widget) then - ---@cast child Widget - Array.extendWith(convertedWidgets, WidgetFactory.work(child, injector)) - else - ---@cast child Html - table.insert(convertedWidgets, child) - end - end - - return convertedWidgets +---@param args {widget: string, children: ((Widget|Html|string|number)[])|Widget|Html|string|number, [any]:any} +---@return Widget +function WidgetFactory.fromTemplate(args) + local widgetClass = args.widget + args.widget = nil + args.children = type(args.children) == 'table' and args.children or {args.children} + local WidgetClass = Lua.import('Module:Widget/' .. widgetClass) + return WidgetClass(args) end -return WidgetFactory +return Class.export(WidgetFactory) diff --git a/components/widget/widget_fragment.lua b/components/widget/widget_fragment.lua new file mode 100644 index 00000000000..a354ab812ce --- /dev/null +++ b/components/widget/widget_fragment.lua @@ -0,0 +1,28 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:Widget/Fragment +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local Class = require('Module:Class') +local FnUtil = require('Module:FnUtil') +local Lua = require('Module:Lua') + +local Widget = Lua.import('Module:Widget') + +---@class FragmentWidget: Widget +---@operator call(WidgetParameters): FragmentWidget +local Fragment = Class.new(Widget) + +---@param children string[] +---@return string? +function Fragment:make(children) + local div = mw.html.create() + Array.forEach(children, FnUtil.curry(div.node, div)) + return tostring(div) +end + +return Fragment diff --git a/components/widget/widget_icon.lua b/components/widget/widget_icon.lua new file mode 100644 index 00000000000..93a20c0b518 --- /dev/null +++ b/components/widget/widget_icon.lua @@ -0,0 +1,28 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:Widget/Icon +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Class = require('Module:Class') +local Lua = require('Module:Lua') + +local Widget = Lua.import('Module:Widget') + +---@class IconWidget: Widget +---@operator call(WidgetParameters): IconWidget +local Icon = Class.new( + Widget, + function(self, input) + end +) + +---@param children string[] +---@return string? +function Icon:make(children) + error('Widget/Icon is an interface and should not be instantiated directly') +end + +return Icon diff --git a/components/widget/widget_icon_fontawesome.lua b/components/widget/widget_icon_fontawesome.lua new file mode 100644 index 00000000000..4f93c795901 --- /dev/null +++ b/components/widget/widget_icon_fontawesome.lua @@ -0,0 +1,31 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:Widget/Icon/Fontawesome +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Class = require('Module:Class') +local Icon = require('Module:Icon') +local Lua = require('Module:Lua') + +local WidgetIcon = Lua.import('Module:Widget/Icon') + +---@class IconFontawesomeWidget: IconWidget +---@operator call(IconProps): IconFontawesomeWidget +---@field props IconProps +local FontawesomeIcon = Class.new( + WidgetIcon, + function(self, input) + self.props = input + end +) + +---@param children string[] +---@return string? +function FontawesomeIcon:make(children) + return Icon.makeIcon(self.props) +end + +return FontawesomeIcon diff --git a/components/widget/widget_icon_image.lua b/components/widget/widget_icon_image.lua new file mode 100644 index 00000000000..5b67be0443c --- /dev/null +++ b/components/widget/widget_icon_image.lua @@ -0,0 +1,43 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:Widget/Icon/Image +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Class = require('Module:Class') +local Image = require('Module:Image') +local Lua = require('Module:Lua') + +local WidgetIcon = Lua.import('Module:Widget/Icon') + +---@class IconImageWidgetParameters: WidgetParameters +---@field imageLight string? +---@field imageDark string? +---@field link string? + +---@class IconImageWidget: IconWidget +---@operator call(IconImageWidgetParameters): IconImageWidget +---@field props IconImageWidgetParameters +local Icon = Class.new( + WidgetIcon, + function(self, input) + self.props = input + end +) + +---@param children string[] +---@return string? +function Icon:make(children) + return Image.display( + self.props.imageLight, + self.props.imageDark, + { + link = self.props.link or '', + size = 'x20px', + } + ) +end + +return Icon diff --git a/components/widget/widget_inline_icon_and_text.lua b/components/widget/widget_inline_icon_and_text.lua new file mode 100644 index 00000000000..d4ea609f392 --- /dev/null +++ b/components/widget/widget_inline_icon_and_text.lua @@ -0,0 +1,52 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:Widget/InlineIconAndText +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Class = require('Module:Class') +local Lua = require('Module:Lua') + +local Widget = Lua.import('Module:Widget') +local Link = Lua.import('Module:Widget/Link') +local Span = Lua.import('Module:Widget/Span') + +---@class InlineIconAndTextWidgetParameters: WidgetParameters +---@field icon IconWidget +---@field text string? +---@field link string? + +---@class InlineIconAndTextWidget: Widget +---@operator call(InlineIconAndTextWidgetParameters): InlineIconAndTextWidget + +local InlineIconAndText = Class.new( + Widget, + function(self, input) + local text = self:assertExistsAndCopy(input.text) + local link = self:assertExistsAndCopy(input.link) + + local span = Span{ + classes = {'image-link'}, + children = { + input.icon, + ' ', + Link{ + link = link, + linktype = 'internal', + children = {text} + } + }, + } + self.children = {span} + end +) + +---@param children string[] +---@return string +function InlineIconAndText:make(children) + return table.concat(children) +end + +return InlineIconAndText diff --git a/components/widget/widget_link.lua b/components/widget/widget_link.lua new file mode 100644 index 00000000000..9668b24feb7 --- /dev/null +++ b/components/widget/widget_link.lua @@ -0,0 +1,39 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:Widget/Link +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Class = require('Module:Class') +local Lua = require('Module:Lua') + +local Widget = Lua.import('Module:Widget') + +---@class LinkWidgetParameters: WidgetParameters +---@field link string +---@field linktype 'internal'|'external'|nil + +---@class LinkWidget: Widget +---@operator call(LinkWidgetParameters): LinkWidget +---@field link string +---@field linktype 'internal'|'external' +local Link = Class.new( + Widget, + function(self, input) + self.link = self:assertExistsAndCopy(input.link) + self.linktype = input.linktype or 'internal' + end +) + +---@param children string[] +---@return string? +function Link:make(children) + if self.linktype == 'external' then + return '[' .. self.link .. ' '.. table.concat(children) .. ']' + end + return '[[' .. self.link .. '|'.. table.concat(children) .. ']]' +end + +return Link diff --git a/components/widget/widget_span.lua b/components/widget/widget_span.lua new file mode 100644 index 00000000000..fb6a75567ce --- /dev/null +++ b/components/widget/widget_span.lua @@ -0,0 +1,38 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:Widget/Span +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local Class = require('Module:Class') +local FnUtil = require('Module:FnUtil') +local Lua = require('Module:Lua') + +local Widget = Lua.import('Module:Widget') + +---@class GenericSpanWidgetParameters: WidgetParameters +---@field classes string[]? + +---@class GenericSpanWidget: Widget +---@operator call(GenericSpanWidgetParameters): GenericSpanWidget +---@field classes string[] +local Span = Class.new( + Widget, + function(self, input) + self.classes = input.classes or {} + end +) + +---@param children string[] +---@return string? +function Span:make(children) + local span = mw.html.create('span') + Array.forEach(self.classes, FnUtil.curry(span.addClass, span)) + Array.forEach(children, FnUtil.curry(span.node, span)) + return tostring(span) +end + +return Span diff --git a/components/widget/widget_table.lua b/components/widget/widget_table.lua index d5c6dc3783c..f2a6a3ee2b3 100644 --- a/components/widget/widget_table.lua +++ b/components/widget/widget_table.lua @@ -11,24 +11,21 @@ local Class = require('Module:Class') local Lua = require('Module:Lua') local Widget = Lua.import('Module:Widget') -local WidgetFactory = Lua.import('Module:Widget/Factory') ---@class WidgetTableInput ----@field rows WidgetTableRow[]? +---@field children WidgetTableRow[]? ---@field classes string[]? ---@field css {[string]: string|number|nil}? ---@field columns integer? ---@class WidgetTable:Widget ---@operator call(WidgetTableInput):WidgetTable ----@field rows WidgetTableRow[] ---@field classes string[] ---@field css {[string]: string|number|nil} ---@field columns integer? local Table = Class.new( Widget, function(self, input) - self.rows = input.rows or {} self.classes = input.classes or {} self.css = input.css or {} self.columns = input.columns @@ -38,7 +35,7 @@ local Table = Class.new( ---@param row WidgetTableRow? ---@return self function Table:addRow(row) - table.insert(self.rows, row) + table.insert(self.children, row) return self end @@ -49,9 +46,9 @@ function Table:addClass(class) return self end ----@param injector WidgetInjector? ----@return {[1]: Html} -function Table:make(injector) +---@param children string[] +---@return string? +function Table:make(children) local displayTable = mw.html.create('div'):addClass('csstable-widget') displayTable:css{ ['grid-template-columns'] = 'repeat(' .. (self.columns or self:_getMaxCells()) .. ', auto)', @@ -63,13 +60,11 @@ function Table:make(injector) displayTable:css(self.css) - for _, row in ipairs(self.rows) do - for _, node in ipairs(WidgetFactory.work(row, injector)) do - displayTable:node(node) - end + for _, row in ipairs(children) do + displayTable:node(row) end - return {displayTable} + return tostring(displayTable) end ---@return integer? @@ -77,7 +72,7 @@ function Table:_getMaxCells() local getNumberCells = function(row) return row:getCellCount() end - return Array.reduce(Array.map(self.rows, getNumberCells), math.max) + return Array.reduce(Array.map(self.children, getNumberCells), math.max) end return Table diff --git a/components/widget/widget_table_cell.lua b/components/widget/widget_table_cell.lua index 1b81ffe0c60..8684a41dca6 100644 --- a/components/widget/widget_table_cell.lua +++ b/components/widget/widget_table_cell.lua @@ -14,13 +14,12 @@ local Lua = require('Module:Lua') local Widget = Lua.import('Module:Widget') ---@class WidgetCellInput ----@field content (string|number|table|Html)[]? +---@field children (string|number|table|Html)[]? ---@field classes string[]? ---@field css {[string]: string|number}? ---@class WidgetTableCell:Widget ---@operator call(WidgetCellInput): WidgetTableCell ----@field content (string|number|table|Html)[] ---@field classes string[] ---@field css {[string]: string|number} ---@field rowSpan integer? @@ -28,7 +27,6 @@ local Widget = Lua.import('Module:Widget') local TableCell = Class.new( Widget, function(self, input) - self.content = input.content or {} self.classes = input.classes or {} self.css = input.css or {} end @@ -37,7 +35,7 @@ local TableCell = Class.new( ---@param text string|number|table|nil ---@return self function TableCell:addContent(text) - table.insert(self.content, text) + table.insert(self.children, text) return self end @@ -56,9 +54,9 @@ function TableCell:addCss(key, value) return self end ----@param injector WidgetInjector? ----@return {[1]: Html} -function TableCell:make(injector) +---@param children string[] +---@return string? +function TableCell:make(children) local cell = mw.html.create('div'):addClass('csstable-widget-cell') cell:css{ ['grid-row'] = self.rowSpan and 'span ' .. self.rowSpan or nil, @@ -71,26 +69,9 @@ function TableCell:make(injector) cell:css(self.css) - cell:node(self:_concatContent()) + Array.forEach(children, FnUtil.curry(cell.node, cell)) - return {cell} -end - ----@return string -function TableCell:_concatContent() - return table.concat(Array.map(self.content, function (content) - if type(content) ~= 'table' then - return content - end - - if not Array.isArray(content) then - return tostring(content) - end - - local wrapper = mw.html.create('div') - Array.forEach(content, FnUtil.curry(wrapper.node, wrapper)) - return tostring(wrapper) - end)) + return tostring(cell) end return TableCell diff --git a/components/widget/widget_table_cell_new.lua b/components/widget/widget_table_cell_new.lua index 8ac97cb1ea5..da8c61a6508 100644 --- a/components/widget/widget_table_cell_new.lua +++ b/components/widget/widget_table_cell_new.lua @@ -15,7 +15,7 @@ local Lua = require('Module:Lua') local Widget = Lua.import('Module:Widget') ---@class WidgetCellInputNew ----@field content (string|number|table|Html)[]? +---@field content (Widget|Html|string|number)[] ---@field classes string[]? ---@field css {[string]: string|number}? ---@field rowSpan integer? @@ -24,7 +24,6 @@ local Widget = Lua.import('Module:Widget') ---@class WidgetTableCellNew:Widget ---@operator call(WidgetCellInputNew): WidgetTableCellNew ----@field content (string|number|table|Html)[] ---@field classes string[] ---@field css {[string]: string|number} ---@field rowSpan integer? @@ -33,7 +32,7 @@ local Widget = Lua.import('Module:Widget') local TableCell = Class.new( Widget, function(self, input) - self.content = input.content or {} + self.children = input.children or input.content or {} -- TODO remove input.content self.classes = input.classes or {} self.css = input.css or {} self.rowSpan = input.rowSpan @@ -42,9 +41,9 @@ local TableCell = Class.new( end ) ----@param injector WidgetInjector? ----@return {[1]: Html} -function TableCell:make(injector) +---@param children string[] +---@return string? +function TableCell:make(children) local cell = mw.html.create(self.isHeader and 'th' or 'td') cell:attr('colspan', self.colSpan) cell:attr('rowspan', self.rowSpan) @@ -53,26 +52,9 @@ function TableCell:make(injector) cell:css(self.css) - cell:node(self:_content()) + Array.forEach(children, FnUtil.curry(cell.node, cell)) - return {cell} -end - ----@return string -function TableCell:_content() - return table.concat(Array.map(self.content, function (content) - if type(content) ~= 'table' then - return content - end - - if not Array.isArray(content) then - return tostring(content) - end - - local wrapper = mw.html.create('div') - Array.forEach(content, FnUtil.curry(wrapper.node, wrapper)) - return tostring(wrapper) - end)) + return tostring(cell) end return TableCell diff --git a/components/widget/widget_table_new.lua b/components/widget/widget_table_new.lua index 631643d0f4a..1a6e95db92d 100644 --- a/components/widget/widget_table_new.lua +++ b/components/widget/widget_table_new.lua @@ -12,7 +12,6 @@ local FnUtil = require('Module:FnUtil') local Lua = require('Module:Lua') local Widget = Lua.import('Module:Widget') -local WidgetFactory = Lua.import('Module:Widget/Factory') ---@class WidgetTableNewInput ---@field children Widget[]? @@ -21,21 +20,19 @@ local WidgetFactory = Lua.import('Module:Widget/Factory') ---@class WidgetTableNew:Widget ---@operator call(WidgetTableNewInput):WidgetTableNew ----@field children Widget[] ---@field classes string[] ---@field css {[string]: string|number|nil} local Table = Class.new( Widget, function(self, input) - self.children = input.children or {} self.classes = input.classes or {} self.css = input.css or {} end ) ----@param injector WidgetInjector? ----@return {[1]: Html} -function Table:make(injector) +---@param children string[] +---@return string? +function Table:make(children) local wrapper = mw.html.create('div'):addClass('table-responsive') local output = mw.html.create('table'):addClass('wikitable') @@ -43,12 +40,10 @@ function Table:make(injector) output:css(self.css) - Array.forEach(self.children, function(child) - Array.forEach(WidgetFactory.work(child, injector), FnUtil.curry(output.node, output)) - end) + Array.forEach(children, FnUtil.curry(output.node, output)) wrapper:node(output) - return {wrapper} + return tostring(wrapper) end return Table diff --git a/components/widget/widget_table_row.lua b/components/widget/widget_table_row.lua index 24486f59bf1..9478cd95358 100644 --- a/components/widget/widget_table_row.lua +++ b/components/widget/widget_table_row.lua @@ -10,22 +10,19 @@ local Class = require('Module:Class') local Lua = require('Module:Lua') local Widget = Lua.import('Module:Widget') -local WidgetFactory = Lua.import('Module:Widget/Factory') ---@class WidgetTableRowInput ----@field cells Widget[]? +---@field children Widget[]? ---@field classes string[]? ---@field css {[string]: string|number|nil}? ---@class WidgetTableRow:Widget ---@operator call(WidgetTableRowInput): WidgetTableRow ----@field cells Widget[] ---@field classes string[] ---@field css {[string]: string|number|nil} local TableRow = Class.new( Widget, function(self, input) - self.cells = input.cells or {} self.classes = input.classes or {} self.css = input.css or {} end @@ -34,7 +31,7 @@ local TableRow = Class.new( ---@param cell Widget? ---@return self function TableRow:addCell(cell) - table.insert(self.cells, cell) + table.insert(self.children, cell) return self end @@ -55,12 +52,12 @@ end ---@return integer function TableRow:getCellCount() - return #self.cells + return #self.children end ----@param injector WidgetInjector? ----@return {[1]: Html} -function TableRow:make(injector) +---@param children string[] +---@return string? +function TableRow:make(children) local row = mw.html.create('div'):addClass('csstable-widget-row') for _, class in ipairs(self.classes) do @@ -69,13 +66,11 @@ function TableRow:make(injector) row:css(self.css) - for _, cell in ipairs(self.cells) do - for _, node in ipairs(WidgetFactory.work(cell, injector)) do - row:node(node) - end + for _, cell in ipairs(children) do + row:node(cell) end - return {row} + return tostring(row) end return TableRow diff --git a/components/widget/widget_table_row_new.lua b/components/widget/widget_table_row_new.lua index ca022a80aa9..e284e14559b 100644 --- a/components/widget/widget_table_row_new.lua +++ b/components/widget/widget_table_row_new.lua @@ -12,7 +12,6 @@ local FnUtil = require('Module:FnUtil') local Lua = require('Module:Lua') local Widget = Lua.import('Module:Widget') -local WidgetFactory = Lua.import('Module:Widget/Factory') ---@class WidgetTableRowNewInput ---@field children Widget[]? @@ -21,32 +20,28 @@ local WidgetFactory = Lua.import('Module:Widget/Factory') ---@class WidgetTableRowNew:Widget ---@operator call(WidgetTableRowNewInput): WidgetTableRowNew ----@field children Widget[] ---@field classes string[] ---@field css {[string]: string|number|nil} local TableRow = Class.new( Widget, function(self, input) - self.children = input.children or {} self.classes = input.classes or {} self.css = input.css or {} end ) ----@param injector WidgetInjector? ----@return {[1]: Html} -function TableRow:make(injector) +---@param children string[] +---@return string? +function TableRow:make(children) local row = mw.html.create('tr') Array.forEach(self.classes, FnUtil.curry(row.addClass, row)) row:css(self.css) - Array.forEach(self.children, function(child) - Array.forEach(WidgetFactory.work(child, injector), FnUtil.curry(row.node, row)) - end) + Array.forEach(children, FnUtil.curry(row.node, row)) - return {row} + return tostring(row) end return TableRow diff --git a/components/widget/widget_util.lua b/components/widget/widget_util.lua new file mode 100644 index 00000000000..1eb08728f65 --- /dev/null +++ b/components/widget/widget_util.lua @@ -0,0 +1,30 @@ +--- +-- @Liquipedia +-- wiki=commons +-- page=Module:Widget/Util +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local Table = require('Module:Table') + +local Util = {} + +function Util.collect(...) + local elements = Table.pack(...) + local array = {} + for index = 1, elements.n do + local x = elements[index] + if Array.isArray(x) then + for _, y in ipairs(x) do + table.insert(array, y) + end + elseif x ~= nil then + table.insert(array, x) + end + end + return array +end + +return Util diff --git a/definitions/mw.lua b/definitions/mw.lua index 6d0d1a754bc..5f3a1deb8c3 100644 --- a/definitions/mw.lua +++ b/definitions/mw.lua @@ -76,7 +76,9 @@ function mw.frame:callParserFunction(name, args) end ---This is transclusion. As in transclusion, if the passed title does not contain a namespace prefix it will be assumed to be in the Template: namespace. ---@param params {title: string, args: table?} ---@return string -function mw.frame:expandTemplate(params) end +function mw.frame:expandTemplate(params) + error('Cannot expand template in fake') +end ---This is equivalent to a call to frame:callParserFunction() with function name '#tag:' .. name and with content prepended to args. ---@param name string @@ -1008,7 +1010,7 @@ mw.ext.TeamTemplate = {} ---@param teamteplate string ---@param date string|number? ----@return {templatename: string, historicaltemplate: string?, shortname: string, name: string, bracket: string, page: string, icon: string, image: string, legacyimage: string, legacyimagedark: string} +---@return {templatename: string, historicaltemplate: string?, shortname: string, name: string, bracketname: string, page: string, icon: string, image: string, legacyimage: string, legacyimagedark: string}|nil function mw.ext.TeamTemplate.raw(teamteplate, date) end ---@param teamteplate string diff --git a/javascript/commons/Countdown.js b/javascript/commons/Countdown.js index a9c11d3ade8..07ac14e0cce 100644 --- a/javascript/commons/Countdown.js +++ b/javascript/commons/Countdown.js @@ -18,18 +18,21 @@ liquipedia.countdown = { mw.loader.using( 'user.options', () => { liquipedia.countdown.timerObjectNodes.forEach( ( timerObjectNode ) => { const dateObject = liquipedia.countdown.parseTimerObjectNodeToDateObj( timerObjectNode ); - const dateChild = document.createElement( 'span' ); - if ( typeof dateObject === 'object' ) { - const disableTimeZoneAdjust = mw.user.options.get( 'teamliquidintegration-disable-countdown-timezone-adjust' ) === '1' || mw.user.options.get( 'teamliquidintegration-disable-countdown-timezone-adjust' ) === 1; - if ( disableTimeZoneAdjust ) { + let dateChild; + if ( timerObjectNode.firstChild instanceof HTMLSpanElement ) { + dateChild = timerObjectNode.firstChild; + } else { + dateChild = document.createElement( 'span' ); + if ( + mw.user.options.get( 'teamliquidintegration-disable-countdown-timezone-adjust' ) || + typeof dateObject !== 'object' + ) { dateChild.innerHTML = timerObjectNode.innerHTML; } else { dateChild.innerHTML = liquipedia.countdown.getCorrectTimeZoneString( dateObject ); } - } else { - dateChild.innerHTML = timerObjectNode.innerHTML; + dateChild.classList.add( 'timer-object-date' ); } - dateChild.classList.add( 'timer-object-date' ); timerObjectNode.innerHTML = ''; timerObjectNode.appendChild( dateChild ); let separatorChild; diff --git a/javascript/commons/Mainpage.js b/javascript/commons/Mainpage.js index c26b5ba0d2c..b1c154bc918 100644 --- a/javascript/commons/Mainpage.js +++ b/javascript/commons/Mainpage.js @@ -37,7 +37,12 @@ liquipedia.mainpage = { if ( typeof data.parse !== 'undefined' ) { document.getElementById( 'this-day-date' ).innerHTML = '(' + liquipedia.mainpage.getMonthName( month ) + ' ' + liquipedia.mainpage.getOrdinal( day ) + ')'; document.getElementById( 'this-day-trivialink' ).innerHTML = 'Add trivia about this day here.'; - document.getElementById( 'this-day-facts' ).innerHTML = data.parse.text[ '*' ].replace( '\n', '' ); + const thisDayFacts = document.getElementById( 'this-day-facts' ); + thisDayFacts.innerHTML = data.parse.text[ '*' ].replace( '\n', '' ); + // Remove ads from the API data to avoid duplicates + thisDayFacts.querySelectorAll( '.content-ad' ).forEach( ( n ) => { + n.parentNode.removeChild( n ); + } ); document.querySelectorAll( '.age' ).forEach( ( age ) => { age.innerHTML = ( new Date() ).getFullYear() - age.dataset.year; } ); diff --git a/javascript/commons/SwitchButtons.js b/javascript/commons/SwitchButtons.js index 8b3876e8e83..40dc8ae8c3a 100644 --- a/javascript/commons/SwitchButtons.js +++ b/javascript/commons/SwitchButtons.js @@ -42,7 +42,7 @@ * Properties: * The switch button can be easily accessed and manipulated by other components using the getSwitchGroup method. * - * SwithGroup object contains the following properties: + * SwitchGroup object contains the following properties: * - type: The type of the switch group (toggle or pill). * - name: The name of the switch group. * - activeClassName: The class name that is added to the active switch button. @@ -120,10 +120,13 @@ liquipedia.switchButtons = { targetValue ) ); } else { - switchGroup.nodes.forEach( ( node ) => node.classList.toggle( - switchGroup.activeClassName, - targetValue === node.dataset.switchValue - ) ); + switchGroup.nodes.forEach( ( node ) => { + const isActive = targetValue === node.dataset.switchValue; + node.classList.toggle( switchGroup.activeClassName, isActive ); + if ( isActive ) { + node.dispatchEvent( new Event( 'click' ) ); + } + } ); } }, diff --git a/spec/golden_masters/infobox_league_apexlegends.txt b/spec/golden_masters/infobox_league_apexlegends.txt new file mode 100644 index 00000000000..59a84811c39 --- /dev/null +++ b/spec/golden_masters/infobox_league_apexlegends.txt @@ -0,0 +1 @@ +[[https://liquipedia.net/wiki/ e]][[[lpcommons:|h]]]FakePageLeague InformationPlatform:[[PC Competitions|PC]][[Category:PC Competitions]]Location:[[Template:Abbr/TBD]] \ No newline at end of file diff --git a/spec/golden_masters/infobox_league_counterstrike.txt b/spec/golden_masters/infobox_league_counterstrike.txt new file mode 100644 index 00000000000..cbe6f10c31c --- /dev/null +++ b/spec/golden_masters/infobox_league_counterstrike.txt @@ -0,0 +1 @@ +[[https://liquipedia.net/wiki/ e]][[[lpcommons:|h]]]FakePageLeague InformationLocation:[[Template:Abbr/TBD]] \ No newline at end of file diff --git a/spec/golden_masters/infobox_league_dota2.txt b/spec/golden_masters/infobox_league_dota2.txt new file mode 100644 index 00000000000..1d0570d079f --- /dev/null +++ b/spec/golden_masters/infobox_league_dota2.txt @@ -0,0 +1 @@ +[[https://liquipedia.net/wiki/ e]][[[lpcommons:|h]]]FakePageLeague InformationLocation:[[Template:Abbr/TBD]]Game:[[Dota 2|Dota 2]] \ No newline at end of file diff --git a/spec/golden_masters/infobox_league_leagueoflegends.txt b/spec/golden_masters/infobox_league_leagueoflegends.txt new file mode 100644 index 00000000000..22a739336bc --- /dev/null +++ b/spec/golden_masters/infobox_league_leagueoflegends.txt @@ -0,0 +1 @@ +[[https://liquipedia.net/wiki/ e]][[[lpcommons:|h]]]FakePageLeague InformationLocation:[[Template:Abbr/TBD]] \ No newline at end of file diff --git a/spec/golden_masters/infobox_league_mobilelegends.txt b/spec/golden_masters/infobox_league_mobilelegends.txt new file mode 100644 index 00000000000..b03697fa254 --- /dev/null +++ b/spec/golden_masters/infobox_league_mobilelegends.txt @@ -0,0 +1 @@ +[[https://liquipedia.net/wiki/ e]][[[lpcommons:|h]]]FakePageLeague InformationLocation:[[Template:Abbr/TBD]] \ No newline at end of file diff --git a/spec/golden_masters/infobox_league_overwatch.txt b/spec/golden_masters/infobox_league_overwatch.txt new file mode 100644 index 00000000000..5e5d7b199c2 --- /dev/null +++ b/spec/golden_masters/infobox_league_overwatch.txt @@ -0,0 +1 @@ +[[https://liquipedia.net/wiki/ e]][[[lpcommons:|h]]]FakePageLeague InformationLocation:[[Template:Abbr/TBD]]Game:[[Overwatch 2|Overwatch 2]] \ No newline at end of file diff --git a/spec/golden_masters/infobox_league_rainbowsix.txt b/spec/golden_masters/infobox_league_rainbowsix.txt new file mode 100644 index 00000000000..881495bf46b --- /dev/null +++ b/spec/golden_masters/infobox_league_rainbowsix.txt @@ -0,0 +1 @@ +[[https://liquipedia.net/wiki/ e]][[[lpcommons:|h]]]FakePageLeague InformationLocation:[[Template:Abbr/TBD]]Game:Tom Clancy's Rainbow Six SiegePlatform:[[:Category:PC|PC]] \ No newline at end of file diff --git a/spec/golden_masters/infobox_league_rocketleague.txt b/spec/golden_masters/infobox_league_rocketleague.txt new file mode 100644 index 00000000000..a3211434d80 --- /dev/null +++ b/spec/golden_masters/infobox_league_rocketleague.txt @@ -0,0 +1 @@ +[[https://liquipedia.net/wiki/ e]][[[lpcommons:|h]]]FakePageLeague InformationLocation:[[Template:Abbr/TBD]] \ No newline at end of file diff --git a/spec/golden_masters/infobox_league_starcraft2.txt b/spec/golden_masters/infobox_league_starcraft2.txt new file mode 100644 index 00000000000..12e93d9c545 --- /dev/null +++ b/spec/golden_masters/infobox_league_starcraft2.txt @@ -0,0 +1 @@ +[[https://liquipedia.net/wiki/ e]][[[lpcommons:|h]]]FakePageLeague InformationGame Version:[[Wings of Liberty|Wings of Liberty]]Location:[[Template:Abbr/TBD]] \ No newline at end of file diff --git a/spec/golden_masters/infobox_league_valorant.txt b/spec/golden_masters/infobox_league_valorant.txt new file mode 100644 index 00000000000..a119bf1c4ae --- /dev/null +++ b/spec/golden_masters/infobox_league_valorant.txt @@ -0,0 +1 @@ +[[https://liquipedia.net/wiki/ e]][[[lpcommons:|h]]]FakePageLeague InformationLocation:[[Template:Abbr/TBD]] \ No newline at end of file diff --git a/spec/golden_masters/match2_matchlist_smoke_apexlegends.txt b/spec/golden_masters/match2_matchlist_smoke_apexlegends.txt index 82adb93e078..932d6157766 100644 --- a/spec/golden_masters/match2_matchlist_smoke_apexlegends.txt +++ b/spec/golden_masters/match2_matchlist_smoke_apexlegends.txt @@ -1 +1 @@ -MatchOverall standingsSchedulePoints Distribution1 kill1 kill point1st - 2nd0 placement pointsRankTeamTotal PointsPts.1st[[abc|A]]32nd[[B|B]]2 \ No newline at end of file +MatchOverall standingsSchedulePoints Distribution1 kill1 kill point1st - 2nd0 placement pointsRankTeamTotal PointsPts.1st[[abc|A]]32nd[[B|B]]2 \ No newline at end of file diff --git a/spec/golden_masters/match2_matchlist_smoke_leagueoflegends.txt b/spec/golden_masters/match2_matchlist_smoke_leagueoflegends.txt index 79ec22c3708..f0806e71879 100644 --- a/spec/golden_masters/match2_matchlist_smoke_leagueoflegends.txt +++ b/spec/golden_masters/match2_matchlist_smoke_leagueoflegends.txt @@ -1 +1 @@ -Match ListA32B[[abc|A]]32[[B|B]]A Match Comment \ No newline at end of file +Match ListA32B[[abc|A]]32[[B|B]][[Match:ID_leagueofle_0001|Match Page]]A Match Comment \ No newline at end of file diff --git a/spec/golden_masters/match2_matchlist_smoke_starcraft2.txt b/spec/golden_masters/match2_matchlist_smoke_starcraft2.txt index 98334119e5d..13fd772c9c1 100644 --- a/spec/golden_masters/match2_matchlist_smoke_starcraft2.txt +++ b/spec/golden_masters/match2_matchlist_smoke_starcraft2.txt @@ -1 +1 @@ -Match ListA32B[[abc|A]]32[[B|B]]A Match Comment \ No newline at end of file +Match ListA32B[[abc|A]]32[[B|B]]A Match Comment \ No newline at end of file diff --git a/spec/golden_masters/prize_pool.txt b/spec/golden_masters/prize_pool.txt index 11efc41385f..129a62a579a 100644 --- a/spec/golden_masters/prize_pool.txt +++ b/spec/golden_masters/prize_pool.txt @@ -1 +1 @@ -$TBA USD are spread among the participants as seen below:Place$ USD€ EUR kr SEKQualifies To[[A Page|Points]]A titleParticipant[[File:Gold.png|alt=]] 1st$970.973€1,000----[[File:se_hd.png|36x24px|Sweden|link=]][[Rathoz|Rathoz]] \ No newline at end of file +$TBA USD are spread among the participants as seen below:Place$ USD€ EUR kr SEKQualifies To[[A Page|Points]]A titleParticipant[[File:Gold.png|alt=]] 1st$970.973€1,000----[[File:se_hd.png|36x24px|Sweden|link=]][[Rathoz|Rathoz]] \ No newline at end of file diff --git a/spec/golden_masters/squad_row_dota2.txt b/spec/golden_masters/squad_row_dota2.txt index 2a106045d96..725e001f4ae 100644 --- a/spec/golden_masters/squad_row_dota2.txt +++ b/spec/golden_masters/squad_row_dota2.txt @@ -1 +1 @@ -Former SquadIDNamePositionJoin DateInactive DateActive TeamLeave DateNew Team[[File:se_hd.png|36x24px|Sweden|link=]] [[Baz|Baz]](Foo Bar)Foo BarJoin Date: 2022-01-01Inactive Date: 2022-03-03Leave Date: 2022-05-01 \ No newline at end of file +Former SquadIDNamePositionJoin DateInactive DateActive TeamLeave DateNew Team[[File:se_hd.png|36x24px|Sweden|link=]] [[Baz|Baz]](Foo Bar)Foo BarJoin Date: 2022-01-01Inactive Date: 2022-03-03Leave Date: 2022-05-01 \ No newline at end of file diff --git a/spec/infobox_spec.lua b/spec/infobox_spec.lua new file mode 100644 index 00000000000..81626a6317e --- /dev/null +++ b/spec/infobox_spec.lua @@ -0,0 +1,19 @@ +--- Triple Comment to Enable our LLS Plugin +insulate('Infobox', function() + insulate('League', function () + allwikis('smoke', function (args, wikiName) + local LpdbSquadStub = stub(mw.ext.LiquipediaDB, 'lpdb_tournament') + local LpdbQueryStub = stub(mw.ext.LiquipediaDB, 'lpdb', {}) + local InfoboxLeagueCustom = require('Module:Infobox/League/Custom') + + GoldenTest('infobox_league_' .. wikiName, tostring(InfoboxLeagueCustom.run(args.input))) + + LpdbSquadStub:revert() + LpdbQueryStub:revert() + end, {default = { + input = { + require('test_assets.tournaments').dummy + }, + }}) + end) +end) diff --git a/spec/match2_spec.lua b/spec/match2_spec.lua index 5b4c58a4bd9..b2da7a37253 100644 --- a/spec/match2_spec.lua +++ b/spec/match2_spec.lua @@ -1,29 +1,31 @@ --- Triple Comment to Enable our LLS Plugin describe('match2', function() + local tournamentData = require('test_assets.tournaments').dummy insulate('matchlist', function() + local InfoboxLeague = require('Module:Infobox/League/Custom') local Json = require('Module:Json') before_each(function () local dataSaved, dataSavedOpponent, dataSavedPlayer, dataSavedGame = {}, {}, {}, {} -- Mock the lpdb functions - stub(mw.ext.LiquipediaDB, "lpdb_match2", function (objName, data) + stub(mw.ext.LiquipediaDB, 'lpdb_match2', function (objName, data) dataSaved = data end) - stub(mw.ext.LiquipediaDB, "lpdb_match2opponent", function(objName, data) + stub(mw.ext.LiquipediaDB, 'lpdb_match2opponent', function(objName, data) data.match2players = dataSavedPlayer dataSavedPlayer = {} table.insert(dataSavedOpponent, data) return objName end) - stub(mw.ext.LiquipediaDB, "lpdb_match2player", function(objName, data) + stub(mw.ext.LiquipediaDB, 'lpdb_match2player', function(objName, data) table.insert(dataSavedPlayer, data) return objName end) - stub(mw.ext.LiquipediaDB, "lpdb_match2game", function(objName, data) + stub(mw.ext.LiquipediaDB, 'lpdb_match2game', function(objName, data) table.insert(dataSavedGame, data) return objName end) - stub(mw.ext.LiquipediaDB, "lpdb", function(tbl) + stub(mw.ext.LiquipediaDB, 'lpdb', function(tbl) if tbl == 'match2' then dataSaved.extradata = Json.parse(dataSaved.extradata) dataSaved.match2bracketdata = Json.parse(dataSaved.match2bracketdata) @@ -36,6 +38,9 @@ describe('match2', function() end return {} end) + + stub(mw.ext.LiquipediaDB, 'lpdb_tournament') + InfoboxLeague.run(tournamentData) -- Set variable for tournament context end) after_each(function () @@ -48,6 +53,7 @@ describe('match2', function() mw.ext.LiquipediaDB.lpdb_match2opponent:revert() mw.ext.LiquipediaDB.lpdb_match2player:revert() mw.ext.LiquipediaDB.lpdb_match2game:revert() + mw.ext.LiquipediaDB.lpdb_tournament:revert() end) allwikis('smoketest', function(args, wikiName) local Info = require('Module:Info') diff --git a/spec/test_helper.lua b/spec/test_helper.lua index e401e27c1bb..9130053fec0 100644 --- a/spec/test_helper.lua +++ b/spec/test_helper.lua @@ -80,6 +80,8 @@ return function(busted, helper, options) table.insert(paths, 'standard/links/commons/?.lua') table.insert(paths, 'standard/tier/commons/?.lua') table.insert(paths, 'components/widget/?.lua') + table.insert(paths, 'components/widget/squad/?.lua') + table.insert(paths, 'components/widget/infobox/?.lua') package.path = table.concat(paths, ';') end diff --git a/standard/array.lua b/standard/array.lua index 40c62bc4bec..62ab4854595 100644 --- a/standard/array.lua +++ b/standard/array.lua @@ -35,6 +35,7 @@ end ---Return true if the input is a table in array format ---@param tbl any ---@return boolean +---@nodiscard function Array.isArray(tbl) return type(tbl) == 'table' and Table.size(tbl) == #tbl end @@ -43,6 +44,7 @@ end ---@generic T ---@param tbl T[] ---@return T[] +---@nodiscard function Array.copy(tbl) local copy = {} for _, element in ipairs(tbl) do @@ -64,6 +66,7 @@ Array.sub({3, 5, 7, 11}, -2, -1) = {7, 11} ---@param startIndex integer ---@param endIndex integer? ---@return T[] +---@nodiscard function Array.sub(tbl, startIndex, endIndex) if startIndex < 0 then startIndex = #tbl + 1 + startIndex end if not endIndex then endIndex = #tbl end @@ -81,6 +84,7 @@ end ---@param elements V[] ---@param funct fun(element: V, index?: integer): T|nil ---@return T[] +---@nodiscard function Array.map(elements, funct) local mappedArray = {} for index, element in ipairs(elements) do @@ -100,6 +104,7 @@ Array.filter({1, 2, 3}, function(x) return x % 2 == 1 end) ---@param tbl T[] ---@param predicate fun(element?: T, index?: integer): boolean ---@return T[] +---@nodiscard function Array.filter(tbl, predicate) local filteredArray = {} for index, element in ipairs(tbl) do @@ -114,6 +119,7 @@ end ---@generic T ---@param tbl T[] ---@return T[] +---@nodiscard function Array.flatten(tbl) local flattenedArray = {} for _, x in ipairs(tbl) do @@ -132,6 +138,7 @@ end ---@param tbl T[] ---@param funct any ---@return T[] +---@nodiscard function Array.flatMap(tbl, funct) return Array.flatten(Array.map(tbl, funct)) end @@ -141,6 +148,7 @@ end ---@param tbl T[] ---@param predicate fun(element: T): boolean ---@return boolean +---@nodiscard function Array.all(tbl, predicate) for _, element in ipairs(tbl) do if not predicate(element) then @@ -155,6 +163,7 @@ end ---@param tbl T[] ---@param predicate fun(element: T): boolean ---@return boolean +---@nodiscard function Array.any(tbl, predicate) for _, element in ipairs(tbl) do if predicate(element) then @@ -169,6 +178,7 @@ end ---@param tbl T[] ---@param predicate fun(element?: T, index?: integer): boolean ---@return T? +---@nodiscard function Array.find(tbl, predicate) for index, element in ipairs(tbl) do if predicate(element, index) then @@ -195,6 +205,7 @@ Array.groupBy({2, 3, 5, 7, 11, 13}, function(x) return x % 4 end) ---@param funct fun(xValue: T): K? ---@return T[][] ---@return table +---@nodiscard function Array.groupBy(tbl, funct) local groupsByKey = {} local groups = {} @@ -230,6 +241,7 @@ Array.groupAdjacentBy({2, 3, 5, 7, 14, 16}, function(x) return x % 2 end) ---@param f fun(elem: V): T ---@param equals? fun(key: T, currentKey: T): boolean ---@return V[][] +---@nodiscard function Array.groupAdjacentBy(array, f, equals) equals = equals or Logic.deepEquals @@ -252,6 +264,7 @@ end ---@param tblX T[] ---@param tblY T[] ---@return boolean +---@nodiscard function Array.lexicalCompare(tblX, tblY) for index = 1, math.min(#tblX, #tblY) do if tblX[index] < tblY[index] then @@ -270,6 +283,7 @@ end ---@param y1 T[]|T ---@param y2 T[]|T ---@return boolean +---@nodiscard function Array.lexicalCompareIfTable(y1, y2) if type(y1) == 'table' and type(y2) == 'table' then return Array.lexicalCompare(y1, y2) @@ -309,6 +323,7 @@ Array.sortBy({ ---@param funct fun(element: T): V ---@param compare? fun(a: V, b: V): boolean ---@return T[] +---@nodiscard function Array.sortBy(tbl, funct, compare) local copy = Table.copy(tbl) Array.sortInPlaceBy(copy, funct, compare) @@ -330,6 +345,7 @@ end ---@generic T ---@param tbl T[] ---@return T[] +---@nodiscard function Array.reverse(tbl) local reversedArray = {} for index = #tbl, 1, -1 do @@ -349,6 +365,7 @@ Array.append({2, 3}, 5, 7, 11) ---@param tbl T[] ---@param ... any ---@return any[] +---@nodiscard function Array.append(tbl, ...) return Array.appendWith(Array.copy(tbl), ...) end @@ -383,6 +400,7 @@ Array.extend({2, 3}, 5, 7, nil, {11, 13}) ---@param tbl T[]|T ---@param ... T[]|T ---@return T[] +---@nodiscard function Array.extend(tbl, ...) return Array.extendWith({}, tbl, ...) end @@ -436,6 +454,7 @@ end ---@param from integer ---@param to integer ---@return integer[] +---@nodiscard function Array.range(from, to) local elements = {} for element = from, to do @@ -450,6 +469,7 @@ end ---@param iterator? fun(tbl: table, ...):fun(table: table, index?: K):K, V, ... ---@param ... any ---@return K[] +---@nodiscard function Array.extractKeys(tbl, iterator, ...) iterator = iterator or pairs local array = {} @@ -465,6 +485,7 @@ end ---@param iterator? fun(tbl: table, ...):fun(table: table, index?: K):K, V, ... ---@param ... any ---@return V[] +---@nodiscard function Array.extractValues(tbl, iterator, ...) iterator = iterator or pairs local array = {} @@ -509,6 +530,7 @@ Array.reduce({2, 3, 5}, pow) ---@param operator fun(aggregate: V, arrayValue: T): V ---@param initialValue V? ---@return V? +---@nodiscard function Array.reduce(array, operator, initialValue) local aggregate if initialValue ~= nil then @@ -529,6 +551,7 @@ end ---@param funct fun(item: T): V ---@param compare? fun(maxScore: V, score: V): boolean ---@return V +---@nodiscard function Array.maxBy(array, funct, compare) compare = compare or Array.lexicalCompareIfTable @@ -548,6 +571,7 @@ end ---@param array T[] ---@param compare? fun(maxScore: T, score: T): boolean ---@return T +---@nodiscard function Array.max(array, compare) return Array.maxBy(array, function(x) return x end, compare) end @@ -558,6 +582,7 @@ end ---@param funct fun(item: T): V ---@param compare? fun(score: V, minScore: V): boolean ---@return V? +---@nodiscard function Array.minBy(array, funct, compare) compare = compare or Array.lexicalCompareIfTable @@ -577,6 +602,7 @@ end ---@param array T[] ---@param compare? fun(score: T, minScore: T): boolean ---@return T +---@nodiscard function Array.min(array, compare) return Array.minBy(array, function(x) return x end, compare) end @@ -594,6 +620,7 @@ Array.indexOf({3, 5, 4, 6, 7}, function(x) return x % 2 == 0 end) ---@param array V[] ---@param pred fun(elem: V, ix: integer?): boolean ---@return integer +---@nodiscard function Array.indexOf(array, pred) for ix, elem in ipairs(array) do if pred(elem, ix) then @@ -614,6 +641,7 @@ Array.unique({4, 5, 4, 3}) ---@generic V ---@param elements V[] ---@return V[] +---@nodiscard function Array.unique(elements) local elementCache = {} local uniqueElements = {} @@ -629,6 +657,7 @@ end ---@param inputString string? ---@param sep string? ---@return string[] +---@nodiscard function Array.parseCommaSeparatedString(inputString, sep) if Logic.isEmpty(inputString) then return {} end ---@cast inputString -nil diff --git a/standard/class.lua b/standard/class.lua index bb7fc56d20d..0065995560e 100644 --- a/standard/class.lua +++ b/standard/class.lua @@ -6,10 +6,6 @@ -- Please see https://github.com/Liquipedia/Lua-Modules to contribute -- ---- --- @author Vogan for Liquipedia --- - local Arguments = require('Module:Arguments') local Class = {} @@ -18,7 +14,8 @@ Class.PRIVATE_FUNCTION_SPECIFIER = '_' ---@class BaseClass ---@operator call:self ----@field is_a fun(self, BaseClass):boolean +---@field is_a fun(self, class: BaseClass):boolean #deprecated +---@field init fun(self, ...) function Class.new(base, init) local instance = {} @@ -59,16 +56,9 @@ function Class.new(base, init) return Class.export(instance, options) end - instance.is_a = function(self, class) - local m = getmetatable(self) - while m do - if m == class then - return true - end - m = m._base - end - return false - end + ---@deprecated + instance.is_a = Class.instanceOf + setmetatable(instance, metatable) return instance end @@ -159,4 +149,18 @@ function Class._frameToArgs(frame, options) return (Table.isNotEmpty(namedArgs) and namedArgs or nil), indexedArgs end +---@param instance any +---@param class BaseClass +---@return boolean +function Class.instanceOf(instance, class) + local metatable = getmetatable(instance) + while metatable do + if metatable == class then + return true + end + metatable = metatable._base + end + return false +end + return Class diff --git a/standard/condition.lua b/standard/condition.lua index 3ff8f44371b..0ee410c05b4 100644 --- a/standard/condition.lua +++ b/standard/condition.lua @@ -34,7 +34,7 @@ local ConditionTree = Class.new(_ConditionNode, function ConditionTree:add(node) if not node then return self - elseif node.is_a ~= nil and node:is_a(_ConditionNode) then + elseif Class.instanceOf(node, _ConditionNode) then table.insert(self._nodes, node) else -- List of nodes @@ -50,7 +50,7 @@ function ConditionTree:toString() assert(self.booleanOperator ~= nil) return table.concat(Array.map(self._nodes, function(node) - if node:is_a(ConditionTree) then + if Class.instanceOf(node, ConditionTree) then return String.interpolate('(${node})', {node = node:toString()}) end @@ -66,7 +66,6 @@ end ---@field name ColumnName ---@field comparator lpdbComparator ---@field value string|number ----@field is_a function local ConditionNode = Class.new(_ConditionNode, function(self, name, comparator, value) self.name = name diff --git a/standard/error.lua b/standard/error.lua index da927d75e6d..18fe7125890 100644 --- a/standard/error.lua +++ b/standard/error.lua @@ -66,10 +66,7 @@ end) ---@param error Error ---@return boolean function Error.isError(error) - return type(error) == 'table' - and type(error.is_a) == 'function' - and error:is_a(Error) - and type(error.message) == 'string' + return Class.instanceOf(error, Error) and type(error.message) == 'string' end function Error:__tostring() diff --git a/standard/info/wikis/arenafps/info.lua b/standard/info/wikis/arenafps/info.lua index a52b3e3e6e6..affe5908bc8 100644 --- a/standard/info/wikis/arenafps/info.lua +++ b/standard/info/wikis/arenafps/info.lua @@ -410,7 +410,7 @@ return { allowManual = true, }, match2 = { - status = 1, + status = 2, matchWidth = 180, }, }, diff --git a/standard/info/wikis/counterstrike/info.lua b/standard/info/wikis/counterstrike/info.lua index e417ca3b517..2fef59d3100 100644 --- a/standard/info/wikis/counterstrike/info.lua +++ b/standard/info/wikis/counterstrike/info.lua @@ -123,6 +123,7 @@ local infoData = { opponentHeight = 26, scoreWidth = 26, matchWidth = 200, + gameScoresIfBo1 = true, }, }, } diff --git a/standard/info/wikis/criticalops/info.lua b/standard/info/wikis/criticalops/info.lua index f0b932a4d8a..f3b0c641dda 100644 --- a/standard/info/wikis/criticalops/info.lua +++ b/standard/info/wikis/criticalops/info.lua @@ -35,6 +35,7 @@ return { match2 = { status = 2, matchWidth = 180, + gameScoresIfBo1 = true, }, }, } diff --git a/standard/info/wikis/crossfire/info.lua b/standard/info/wikis/crossfire/info.lua index 43436ce7412..091b1459824 100644 --- a/standard/info/wikis/crossfire/info.lua +++ b/standard/info/wikis/crossfire/info.lua @@ -62,6 +62,7 @@ return { status = 1, matchWidthMobile = 110, matchWidth = 190, + gameScoresIfBo1 = true, }, }, defaultRoundPrecision = 0, diff --git a/standard/info/wikis/arenaofvalor/info.lua b/standard/info/wikis/honorofkings/info.lua similarity index 97% rename from standard/info/wikis/arenaofvalor/info.lua rename to standard/info/wikis/honorofkings/info.lua index c7875bcb44e..ef5100919f5 100644 --- a/standard/info/wikis/arenaofvalor/info.lua +++ b/standard/info/wikis/honorofkings/info.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- wiki=arenaofvalor +-- wiki=honorofkings -- page=Module:Info -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute @@ -8,7 +8,7 @@ return { startYear = 2015, - wikiName = 'arenaofvalor', + wikiName = 'honorofkings', name = 'Arena of Valor', defaultGame = 'aov', diff --git a/standard/info/wikis/rainbowsix/info.lua b/standard/info/wikis/rainbowsix/info.lua index 0d7731ddaba..3a237355410 100644 --- a/standard/info/wikis/rainbowsix/info.lua +++ b/standard/info/wikis/rainbowsix/info.lua @@ -48,6 +48,7 @@ return { match2 = { status = 2, matchWidth = 180, + gameScoresIfBo1 = true, }, }, defaultRoundPrecision = 0, diff --git a/standard/info/wikis/simracing/info.lua b/standard/info/wikis/simracing/info.lua index e06bd2170ec..564fbc9322d 100644 --- a/standard/info/wikis/simracing/info.lua +++ b/standard/info/wikis/simracing/info.lua @@ -194,6 +194,19 @@ return { lightMode = 'F1 2018 allmode.png', }, }, + ['f1 2024'] = { + abbreviation = 'F1 24', + name = 'F1 2024', + link = 'F1 2024', + logo = { + darkMode = 'F1 2018 allmode.png', + lightMode = 'F1 2018 allmode.png', + }, + defaultTeamLogo = { + darkMode = 'F1 2018 allmode.png', + lightMode = 'F1 2018 allmode.png', + }, + }, heat3 = { abbreviation = 'Heat 3', name = 'NASCAR Heat 3', @@ -220,6 +233,19 @@ return { lightMode = 'Nascar Heat default allmode.png', }, }, + lmu = { + abbreviation = 'LMU', + name = 'Le Mans Ultimate', + link = 'Le Mans Ultimate', + logo = { + darkMode = 'Le Mans Ultimate default allmode.png', + lightMode = 'Le Mans Ultimate default allmode.png', + }, + defaultTeamLogo = { + darkMode = 'Le Mans Ultimate default allmode.png', + lightMode = 'Le Mans Ultimate default allmode.png', + }, + }, rf2 = { abbreviation = 'rF2', name = 'rFactor 2', @@ -519,6 +545,19 @@ return { lightMode = 'Motogp23 default allmode.png', }, }, + motogp24 = { + abbreviation = 'MotoGP 24', + name = 'MotoGP 24', + link = 'MotoGP 24', + logo = { + darkMode = 'Motogp24 default allmode.png', + lightMode = 'Motogp24 default allmode.png', + }, + defaultTeamLogo = { + darkMode = 'Motogp24 default allmode.png', + lightMode = 'Motogp24 default allmode.png', + }, + }, ['forza 2'] = { abbreviation = 'FM2', name = 'Forza Motorsport 2', @@ -688,6 +727,32 @@ return { lightMode = 'WRC 10 default lightmode.png', }, }, + ['wrc generations'] = { + abbreviation = 'WRC Gen.', + name = 'WRC Generations', + link = 'WRC Generations', + logo = { + darkMode = 'WRC Generations default allmode.png', + lightMode = 'WRC Generations default allmode.png', + }, + defaultTeamLogo = { + darkMode = 'WRC Generations default allmode.png', + lightMode = 'WRC Generations default allmode.png', + }, + }, + ['ea wrc'] = { + abbreviation = 'EA WRC', + name = 'EA SPORTS WRC', + link = 'EA SPORTS WRC', + logo = { + darkMode = 'EA SPORTS WRC default darkmode.png', + lightMode = 'EA SPORTS WRC default lightmode.png', + }, + defaultTeamLogo = { + darkMode = 'EA SPORTS WRC default darkmode.png', + lightMode = 'EA SPORTS WRC default lightmode.png', + }, + }, pgr2 = { abbreviation = 'PGR 2', name = 'Project Gotham Racing 2', diff --git a/standard/info/wikis/tft/info.lua b/standard/info/wikis/tft/info.lua index 6fd0e8cc49d..59cfd065074 100644 --- a/standard/info/wikis/tft/info.lua +++ b/standard/info/wikis/tft/info.lua @@ -46,7 +46,7 @@ return { allowManual = true, }, match2 = { - status = 0, + status = 1, }, }, defaultRoundPrecision = 0, diff --git a/standard/info/wikis/valorant/info.lua b/standard/info/wikis/valorant/info.lua index 6b96f42a436..65c07049042 100644 --- a/standard/info/wikis/valorant/info.lua +++ b/standard/info/wikis/valorant/info.lua @@ -35,6 +35,7 @@ return { match2 = { status = 2, matchWidth = 180, + gameScoresIfBo1 = true, }, }, } diff --git a/standard/info/wikis/zula/info.lua b/standard/info/wikis/zula/info.lua index 2e567e78bc5..f4d4637a4bd 100644 --- a/standard/info/wikis/zula/info.lua +++ b/standard/info/wikis/zula/info.lua @@ -75,6 +75,7 @@ return { status = 2, matchWidthMobile = 110, matchWidth = 200, + gameScoresIfBo1 = true, }, }, } diff --git a/standard/links_stream.lua b/standard/links_stream.lua index 33f97d41fa7..6da24c7a930 100644 --- a/standard/links_stream.lua +++ b/standard/links_stream.lua @@ -222,9 +222,7 @@ local StreamKey = Class.new( ) StreamLinks.StreamKey = StreamKey ----@param tbl string ----@param languageCode string ----@param index integer +---@overload fun(self, tbl: string, languageCode: string, index: integer): StreamKey ---@overload fun(self, tbl: StreamKey): StreamKey ---@overload fun(self, tbl: string): StreamKey function StreamKey:new(tbl, languageCode, index) @@ -236,6 +234,7 @@ function StreamKey:new(tbl, languageCode, index) index = tbl.index -- All three parameters are supplied elseif languageCode and index then + ---@cast tbl -StreamKey platform = tbl elseif type(tbl) == 'string' then local components = mw.text.split(tbl, '_', true) @@ -251,8 +250,8 @@ function StreamKey:new(tbl, languageCode, index) end end - self.platform = platform --[[@as string]] - self.languageCode = languageCode --[[@as string]] + self.platform = platform + self.languageCode = languageCode self.index = tonumber(index) --[[@as integer]] self:_isValid() self.languageCode = self.languageCode:lower() @@ -298,14 +297,10 @@ function StreamKey:_isValid() return true end ----@param value StreamKey ----@return true +---@overload fun(value: StreamKey): true ---@overload fun(value: any): false function StreamKey._isStreamKey(value) - if type(value) == 'table' and type(value.is_a) == 'function' and value:is_a(StreamKey) then - return true - end - return false + return Class.instanceOf(value, StreamKey) end StreamKey.__tostring = StreamKey.toString diff --git a/standard/page.lua b/standard/page.lua index a3a369f0007..64926238e40 100644 --- a/standard/page.lua +++ b/standard/page.lua @@ -64,8 +64,8 @@ function Page.makeExternalLink(display, link) end --- Converts a link to a proper pagename format ----@param link string? ----@return string? +---@overload fun(link: string): string +---@overload fun(link?: nil): nil function Page.pageifyLink(link) if String.isEmpty(link) then return nil diff --git a/standard/result_or_error.lua b/standard/result_or_error.lua index cac5c5cfaf5..0043a35d3e4 100644 --- a/standard/result_or_error.lua +++ b/standard/result_or_error.lua @@ -66,12 +66,12 @@ end ---@return boolean function ResultOrError:isResult() - return self:is_a(ResultOrError.Result) + return Class.instanceOf(self, ResultOrError.Result) end ---@return boolean function ResultOrError:isError() - return self:is_a(ResultOrError.Error) + return Class.instanceOf(self, ResultOrError.Error) end --[[ @@ -136,9 +136,7 @@ function ResultOrError.try(f, originalError) xpcall( function() local result = f() - local isResultOrError = type(result) == 'table' - and type(result.is_a) == 'function' - and result:is_a(ResultOrError) + local isResultOrError = Class.instanceOf(result, ResultOrError) resultOrError = isResultOrError and result or ResultOrError.Result(result) diff --git a/standard/table.lua b/standard/table.lua index a77bfde4f8b..15db509cd0a 100644 --- a/standard/table.lua +++ b/standard/table.lua @@ -295,12 +295,13 @@ f(2, 3) f('player4', 4, 'player') ]] ----@generic K, V, T, I ----@param args {[K] : V} +---@generic K, T +---@param args {[K] : any} ---@param prefixes string[] ----@param f function ----@return {[I] : T} -function Table.mapArgumentsByPrefix(args, prefixes, f) +---@param f fun(key?: K, index?: integer, prefix: string?): T +---@param noInterleave boolean? +---@return {[integer?] : T} +function Table.mapArgumentsByPrefix(args, prefixes, f, noInterleave) local function indexFromKey(key) local prefix, index = key:match('^([%a_]+)(%d+)$') if Table.includes(prefixes, prefix) then @@ -310,7 +311,7 @@ function Table.mapArgumentsByPrefix(args, prefixes, f) end end - return Table.mapArguments(args, indexFromKey, f) + return Table.mapArguments(args, indexFromKey, f, noInterleave) end --- Extracts keys based on a passed `indexFromKey` function interleaved with numeric indexes @@ -319,9 +320,9 @@ end -- Most common use-case will be `Table.mapArgumentsByPrefix` where -- the `indexFromKey` function retrieves keys based on a prefix. -- ----@generic K, V, T, I ----@param args {[K] : V} ----@param indexFromKey fun(key?: K): integer? +---@generic K, T, I +---@param args {[K] : any} +---@param indexFromKey fun(key?: K): I?, ... ---@param f fun(key?: K, index?: integer, ...?: any): T ---@param noInterleave boolean? ---@return {[I] : T} diff --git a/standard/tier/wikis/arenaofvalor/tier_data.lua b/standard/tier/wikis/honorofkings/tier_data.lua similarity index 98% rename from standard/tier/wikis/arenaofvalor/tier_data.lua rename to standard/tier/wikis/honorofkings/tier_data.lua index 28a8bb876ab..9b290a76cb6 100644 --- a/standard/tier/wikis/arenaofvalor/tier_data.lua +++ b/standard/tier/wikis/honorofkings/tier_data.lua @@ -1,6 +1,6 @@ --- -- @Liquipedia --- wiki=arenaofvalor +-- wiki=honorofkings -- page=Module:Tier/Data -- -- Please see https://github.com/Liquipedia/Lua-Modules to contribute diff --git a/standard/tier/wikis/worldoftanks/tier_data.lua b/standard/tier/wikis/worldoftanks/tier_data.lua index 339502dd1c8..c22c4ed7c9c 100644 --- a/standard/tier/wikis/worldoftanks/tier_data.lua +++ b/standard/tier/wikis/worldoftanks/tier_data.lua @@ -97,5 +97,13 @@ return { link = 'Showmatches', category = 'Showmatch Tournaments', }, + points = { + value = 'Points', + sort = 'B1', + name = 'Points', + short = 'Points', + link = 'Point Rankings', + category = 'Point Rankings', + }, }, } diff --git a/stylesheets/commons/Bracket.less b/stylesheets/commons/Bracket.less index 6ca072143d0..fec74f7f577 100644 --- a/stylesheets/commons/Bracket.less +++ b/stylesheets/commons/Bracket.less @@ -592,7 +592,7 @@ Author(s): FO-nTTaX, salle } .wiki-artifact .bracket-popup-wrapper .bracket-popup .bracket-popup-body .bracket-popup-body-match, -.wiki-arenaofvalor .bracket-popup-wrapper .bracket-popup .bracket-popup-body .bracket-popup-body-match, +.wiki-honorofkings .bracket-popup-wrapper .bracket-popup .bracket-popup-body .bracket-popup-body-match, .wiki-leagueoflegends .bracket-popup-wrapper .bracket-popup .bracket-popup-body .bracket-popup-body-match, .wiki-runeterra .bracket-popup-wrapper .bracket-popup .bracket-popup-body .bracket-popup-body-match, .wiki-wildrift .bracket-popup-wrapper .bracket-popup .bracket-popup-body .bracket-popup-body-match, { @@ -715,7 +715,7 @@ Author(s): FO-nTTaX, salle width: 330px; } -.wiki-arenaofvalor .bracket-popup-wrapper.bracket-popup-team { +.wiki-honorofkings .bracket-popup-wrapper.bracket-popup-team { width: 390px; } @@ -1087,7 +1087,7 @@ Author(s): FO-nTTaX, salle display: inline-block; } -.wiki-arenaofvalor .bracket-popup div.draft img, +.wiki-honorofkings .bracket-popup div.draft img, .wiki-artifact .bracket-popup div.draft img, .wiki-paladins .bracket-popup div.draft img, .wiki-leagueoflegends .bracket-popup div.draft img, @@ -1154,14 +1154,14 @@ Author(s): FO-nTTaX, salle display: block; } -.wiki-arenaofvalor .bracket-popup-body-match .match-row .draft.blue, +.wiki-honorofkings .bracket-popup-body-match .match-row .draft.blue, .wiki-battlerite .bracket-popup-body-match .match-row .draft.blue, .wiki-leagueoflegends .bracket-popup-body-match .match-row .draft.blue, .wiki-wildrift .bracket-popup-body-match .match-row .draft.blue { background-color: #426ccf; } -.wiki-arenaofvalor .bracket-popup-body-match .match-row .draft.red, +.wiki-honorofkings .bracket-popup-body-match .match-row .draft.red, .wiki-battlerite .bracket-popup-body-match .match-row .draft.red, .wiki-leagueoflegends .bracket-popup-body-match .match-row .draft.red, .wiki-wildrift .bracket-popup-body-match .match-row .draft.red { @@ -1169,7 +1169,7 @@ Author(s): FO-nTTaX, salle } .wiki-artifact .bracket-popup div.draft img, -.wiki-arenaofvalor .bracket-popup div.draft img, +.wiki-honorofkings .bracket-popup div.draft img, .wiki-paladins .bracket-popup div.draft img, .wiki-leagueoflegends .bracket-popup div.draft img, .wiki-runeterra .bracket-popup div.draft img, @@ -1180,7 +1180,7 @@ Author(s): FO-nTTaX, salle } .wiki-artifact .bracket-popup.bracket-popup-showmatch div.draft img, -.wiki-arenaofvalor .bracket-popup.bracket-popup-showmatch div.draft img, +.wiki-honorofkings .bracket-popup.bracket-popup-showmatch div.draft img, .wiki-paladins .bracket-popup.bracket-popup-showmatch div.draft img, .wiki-leagueoflegends .bracket-popup.bracket-popup-showmatch div.draft img, .wiki-runeterra .bracket-popup.bracket-popup-showmatch div.draft img, diff --git a/stylesheets/commons/Colours.less b/stylesheets/commons/Colours.less index 190f3b52858..3ccec5c2f96 100644 --- a/stylesheets/commons/Colours.less +++ b/stylesheets/commons/Colours.less @@ -155,7 +155,7 @@ div.NavPic, .cinnabar-theme-dark-bg, .wiki-leagueoflegends .bracket-popup-body-match .match-row .draft.red, .wiki-battlerite .bracket-popup-body-match .match-row .draft.red, -.wiki-arenaofvalor .bracket-popup-body-match .match-row .draft.red, +.wiki-honorofkings .bracket-popup-body-match .match-row .draft.red, .wiki-wildrift .bracket-popup-body-match .match-row .draft.red { background-color: var( --clr-cinnabar-40, #b12a2a ); } @@ -313,7 +313,7 @@ div.NavFrame div.NavHead, .sapphire-theme-dark-bg, .wiki-leagueoflegends .bracket-popup-body-match .match-row .draft.blue, .wiki-battlerite .bracket-popup-body-match .match-row .draft.blue, -.wiki-arenaofvalor .bracket-popup-body-match .match-row .draft.blue, +.wiki-honorofkings .bracket-popup-body-match .match-row .draft.blue, .wiki-wildrift .bracket-popup-body-match .match-row .draft.blue { background-color: var( --clr-sapphire-40, #31519c ); } diff --git a/stylesheets/commons/MatchTicker.less b/stylesheets/commons/MatchTicker.less index a35482961ce..9b50b30e03d 100644 --- a/stylesheets/commons/MatchTicker.less +++ b/stylesheets/commons/MatchTicker.less @@ -99,6 +99,49 @@ Author(s): Nadox } } + .match-bottom-bar { + display: flex; + align-items: center; + justify-content: space-between; + + > a { + flex-shrink: 0; + } + + > a:hover { + .btn--add-match-details { + .theme--light & { + background-color: rgba( 0, 0, 0, 0.4 ); + color: #0d61a4; + } + + .theme--dark & { + background-color: rgba( 255, 255, 255, 0.4 ); + color: var( --clr-primary-80 ); + } + } + } + + .btn--match-details, + .btn--add-match-details { + padding: 0.3125rem 0.75rem; + } + + .btn--match-details { + background-color: transparent; + } + + .btn--add-match-details { + .theme--light & { + color: #0d61a4; + } + + .theme--dark & { + color: var( --clr-primary-80 ); + } + } + } + .match-countdown { display: flex; justify-content: center; diff --git a/stylesheets/commons/Miscellaneous.less b/stylesheets/commons/Miscellaneous.less index 31c673ecc10..cb7beb65375 100644 --- a/stylesheets/commons/Miscellaneous.less +++ b/stylesheets/commons/Miscellaneous.less @@ -2758,3 +2758,27 @@ Template: Sfrac height: 1px; overflow: hidden; } + +/******************************************************************************* +Author(s): salle +Template: DeadlockSpiritScalingBox +*******************************************************************************/ + +div.dl-spirit-x-box { + position: relative; + width: 100px; + height: 24px; +} + +div.dl-spirit-x-box > div.dl-spirit-x-box-ratio { + font-weight: bold; + padding: 0 8px 0 12px; + position: absolute; + left: 28px; + top: 3px; +} + +div.dl-spirit-x-box > div.dl-spirit-x-box-image { + position: absolute; + top: 0; +}
\n