diff --git a/lua/wikis/commons/GameTable.lua b/lua/wikis/commons/GameTable.lua index e7965d09614..ad76250de0c 100644 --- a/lua/wikis/commons/GameTable.lua +++ b/lua/wikis/commons/GameTable.lua @@ -9,6 +9,7 @@ local Lua = require('Module:Lua') local Array = Lua.import('Module:Array') local Class = Lua.import('Module:Class') +local DateExt = Lua.import('Module:Date/Ext') local Game = Lua.import('Module:Game') local Logic = Lua.import('Module:Logic') local Operator = Lua.import('Module:Operator') @@ -16,94 +17,96 @@ local VodLink = Lua.import('Module:VodLink') local MatchTable = Lua.import('Module:MatchTable') -local NOT_PLAYED = 'notplayed' -local SCORE_CONCAT = ' : ' +local HtmlWidgets = Lua.import('Module:Widget/Html/All') +local TableWidgets = Lua.import('Module:Widget/Table2/All') +local WidgetUtil = Lua.import('Module:Widget/Util') ----@class GameTableMatch: MatchTableMatch ----@field games match2game[] +local NOT_PLAYED = 'notplayed' +local SCORE_CONCAT = ' : ' ---@class GameTable: MatchTable ----@field countGames number +---@operator call(table): GameTable +---@field countGames integer local GameTable = Class.new(MatchTable, function (self) self.countGames = 0 end) ----@param game match2game ----@return match2game? -function GameTable:gameFromRecord(game) - if self.countGames == self.config.limit then return nil end - if game.status == NOT_PLAYED or Logic.isEmpty(game.winner) then - return nil - end - - return game -end - ----@param record table ----@return GameTableMatch? +---@param record match2 +---@return MatchTableMatch? function GameTable:matchFromRecord(record) - if self.countGames == self.config.limit then return nil end + if self.countGames >= self.config.limit then return nil end local matchRecord = MatchTable.matchFromRecord(self, record) - ---@cast matchRecord GameTableMatch - if Logic.isEmpty(record.match2games) then + if not matchRecord then + return + elseif Logic.isEmpty(record.match2games) then return nil end - matchRecord.games = {} - --order games from last played to first - Array.forEach(Array.reverse(record.match2games), function (game) - local gameRecord = self:gameFromRecord(game) - if gameRecord then self.countGames = self.countGames + 1 end - table.insert(matchRecord.games, gameRecord) + matchRecord.games = Array.filter(matchRecord.games, function (game) + return self:filterGame(game) end) + self.countGames = self.countGames + #matchRecord.games + return matchRecord end +---@param game MatchGroupUtilGame +---@return boolean +function GameTable:filterGame(game) + return game.status ~= NOT_PLAYED and Logic.isNotEmpty(game.winner) +end + ---@param vod string? ----@return Html? +---@return Widget? function GameTable:_displayGameVod(vod) - if not self.config.showVod then return end - - local vodNode = mw.html.create('td') - if Logic.isEmpty(vod) then - return vodNode:wikitext('') + if not self.config.showVod then + return + elseif Logic.isEmpty(vod) then + return TableWidgets.Cell{} end ---@cast vod -nil - return vodNode:node(VodLink.display{vod = vod}) + return TableWidgets.Cell{children = VodLink.display{vod = vod}} end ---@param result MatchTableMatchResult ----@param game match2game +---@param game MatchGroupUtilGame ---@return Html? function GameTable:_displayGameScore(result, game) local scores = Array.map(game.opponents, Operator.property('score')) - local toScore = function(opponentRecord) - local isWinner = opponentRecord.id == tonumber(game.winner) - local score = scores[opponentRecord.id] or (isWinner and 1) or 0 - return mw.html.create(isWinner and 'b' or nil) - :wikitext(score) + local indexes = result.flipped and {2, 1} or {1, 2} + + ---@param opponentIndex integer + ---@return Widget + local toScore = function(opponentIndex) + local isWinner = opponentIndex == tonumber(game.winner) + local score = scores[opponentIndex] or (isWinner and 1) or 0 + return HtmlWidgets.Span{ + css = {['font-weight'] = isWinner and 'bold' or nil}, + children = score + } end - return mw.html.create('td') - :addClass('match-table-score') - :node(toScore(result.opponent)) - :node(SCORE_CONCAT) - :node(toScore(result.vs)) + return TableWidgets.Cell{children = { + toScore(indexes[1]), + SCORE_CONCAT, + toScore(indexes[2]), + }} end ----@param game match2game +---@param game MatchGroupUtilGame ---@return Html? function GameTable:_displayGameIconForGame(game) if not self.config.displayGameIcons then return end - return mw.html.create('td') - :node(Game.icon{game = game.game}) + return TableWidgets.Cell{ + children = Game.icon{game = game.game} + } end ----@param match GameTableMatch ----@param game match2game ----@return Html? +---@param match MatchTableMatch +---@param game MatchGroupUtilGame +---@return Widget|Widget[]? function GameTable:displayGame(match, game) if not self.config.showResult then return @@ -111,41 +114,56 @@ function GameTable:displayGame(match, game) return self:nonStandardMatch(match) end - return mw.html.create() - :node(self.config.showOpponent and self:_displayOpponent(match.result.opponent, true) or nil) - :node(self:_displayGameScore(match.result, game)) - :node(self:_displayOpponent(match.result.vs):css('text-align', 'left')) + return WidgetUtil.collect( + self.config.showOpponent and self:_displayOpponent(match.result.opponent, true) or nil, + self:_displayGameScore(match.result, game), + self:_displayOpponent(match.result.vs) + ) end ----@param match GameTableMatch ----@param game match2game ----@return Html? +---@param match MatchTableMatch +---@param game MatchGroupUtilGame +---@return Widget function GameTable:gameRow(match, game) - local winner = match.result.opponent.id == tonumber(game.winner) and 1 or 2 - - return mw.html.create('tr') - :addClass(self:_getBackgroundClass(winner)) - :node(self:_displayDate(match)) - :node(self:_displayTier(match)) - :node(self:_displayType(match)) - :node(self:_displayGameIconForGame(game)) - :node(self:_displayIcon(match)) - :node(self:_displayTournament(match)) - :node(self:displayGame(match, game)) - :node(self:_displayGameVod(game.vod)) - :node(self:_displayMatchPage(match)) + local indexes = match.result.flipped and {2, 1} or {1, 2} + local winner = indexes[game.winner] + + return TableWidgets.Row{ + classes = {self:getBackgroundClass(winner)}, + children = WidgetUtil.collect( + self:_displayDate(match), + self:displayTier(match), + self:_displayType(match), + self:_displayGameIconForGame(game), + self:_displayIcon(match), + self:_displayTournament(match), + self:displayGame(match, game), + self:_displayGameVod(game.vod), + self:_displayMatchPage(match) + ) + } end ----@param match GameTableMatch ----@return Html? -function GameTable:matchRow(match) - local display = mw.html.create() - - Array.forEach(match.games, function(game) - display:node(self:gameRow(match, game)) +---@return Widget[] +function GameTable:buildRows() + ---@type Widget[] + local rows = {} + + local currentYear = math.huge + Array.forEach(self.matches, function(match) + local year = DateExt.getYearOf(match.date) + if self.config.showYearHeaders and year ~= currentYear then + currentYear = year + table.insert(rows, self:_yearRow(year)) + end + Array.extendWith(rows, Array.reverse( + Array.map(match.games, function (game) + return self:gameRow(match, game) + end) + )) end) - return display + return rows end return GameTable diff --git a/lua/wikis/commons/GameTable/Character.lua b/lua/wikis/commons/GameTable/Character.lua index f09268f9f78..de1b77d5a99 100644 --- a/lua/wikis/commons/GameTable/Character.lua +++ b/lua/wikis/commons/GameTable/Character.lua @@ -9,9 +9,9 @@ local Lua = require('Module:Lua') local Arguments = Lua.import('Module:Arguments') local Array = Lua.import('Module:Array') -local CharacterIcon = Lua.import('Module:CharacterIcon') local Class = Lua.import('Module:Class') local Logic = Lua.import('Module:Logic') +local Namespace = Lua.import('Module:Namespace') local Operator = Lua.import('Module:Operator') local Table = Lua.import('Module:Table') @@ -24,40 +24,47 @@ local Comparator = Condition.Comparator local BooleanOperator = Condition.BooleanOperator local ColumnName = Condition.ColumnName +local HtmlWidgets = Lua.import('Module:Widget/Html/All') +local Link = Lua.import('Module:Widget/Basic/Link') +local MatchSummaryCharacters = Lua.import('Module:Widget/Match/Summary/Characters') +local TableWidgets = Lua.import('Module:Widget/Table2/All') +local WidgetUtil = Lua.import('Module:Widget/Util') + local DRAW_WINNER = 0 local CHARACTER_MODE = 'character' -local SCORE_CONCAT = ' : ' +local NOT_PLAYED = 'notplayed' +local SCORE_CONCAT = ' : ' ---@class CharacterGameTableConfig: MatchTableConfig ---@field showGameWithoutCharacters boolean ---@field showSideClass boolean ---@field showBans boolean ---@field showLength boolean +---@field showPatch boolean ---@field numPicks number ---@field numBans number ----@field iconSize string ----@field iconSeparator string ----@class CharacterGameTableGame: match2game +---@class CharacterGameTableGame: MatchGroupUtilGame ---@field picks string[][] ---@field bans string[][]? ---@field pickedBy number? ---@field pickedByplayer number? +---@class CharacterGameTableMatch: MatchTableMatch +---@field games CharacterGameTableGame[] + ---@class CharacterGameTable: GameTable +---@operator call(table): CharacterGameTable ---@field character string ---@field isCharacterTable boolean ---@field isPickedByRequired boolean ---@field config CharacterGameTableConfig +---@field matches CharacterGameTableMatch[] local CharacterGameTable = Class.new(GameTable, function (self) + self.args.dateFormat = Logic.emptyOr(self.args.dateFormat, 'compact') + self.args.matchPageButtonText = Logic.emptyOr(self.args.matchPageButtonText, 'short') self.isCharacterTable = self.args.tableMode == CHARACTER_MODE self.isPickedByRequired = self.isCharacterTable - - if not self.isCharacterTable then - self.resultFromRecord = GameTable.resultFromRecord - self.buildConditions = GameTable.buildConditions - self.statsFromMatches = GameTable.statsFromMatches - end end) ---@return integer @@ -75,7 +82,7 @@ function CharacterGameTable:readCharacter() if Logic.isNotEmpty(self.args.character) then self.character = self.args.character else - assert(self.title.namespace == 0, 'Lua.importd character= argument') + assert(Namespace.isMain(self.title), 'Lua.importd character= argument') self.character = self.title.rootText end @@ -98,10 +105,9 @@ function CharacterGameTable:readConfig() showSideClass = Logic.nilOr(Logic.readBoolOrNil(args.showSideClass), true), showBans = Logic.nilOr(Logic.readBoolOrNil(args.showBans), true), showLength = Logic.readBool(args.length), + showPatch = Logic.nilOr(Logic.readBoolOrNil(args.showPatch), true), numPicks = self:getNumberOfPicks(), numBans = self:getNumberOfBans(), - iconSize = Logic.nilIfEmpty(self.args.iconSize) or '27px', - iconSeparator = Logic.nilIfEmpty(args.iconSeparator) or '' }) return self @@ -161,8 +167,11 @@ function CharacterGameTable:_buildCharacterConditions() return characterConditions end ----@return string +---@return ConditionTree function CharacterGameTable:buildConditions() + if not self.isCharacterTable then + return GameTable.buildConditions(self) + end local lpdbData = mw.ext.LiquipediaDB.lpdb('match2game', { conditions = self:_buildMatchConditions(), query = 'match2id', @@ -176,7 +185,7 @@ function CharacterGameTable:buildConditions() conditions:add(ConditionNode(ColumnName('match2id'), Comparator.eq, game.match2id)) end) - return conditions:toString() + return conditions end ---@param game CharacterGameTableGame @@ -219,45 +228,48 @@ function CharacterGameTable:getCharacterPick(game) return findCharacter(1) or findCharacter(2) end ----@param game match2game ----@return match2game? -function CharacterGameTable:gameFromRecord(game) - local gameRecord = GameTable.gameFromRecord(self, game) - if not gameRecord then - return nil +---@param game MatchGroupUtilGame +---@return boolean +function CharacterGameTable:filterGame(game) + if game.status == NOT_PLAYED or Logic.isEmpty(game.winner) then + return false end - - ---@cast gameRecord CharacterGameTableGame - 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.isPickedByRequired and self:getCharacterPick(gameRecord) or nil + ---@cast game CharacterGameTableGame + game.picks = self:getCharacters(game, self.config.numPicks, self.getCharacterKey) + game.bans = self.config.showBans and + self:getCharacters(game, self.config.numBans,self.getCharacterBanKey) or nil + game.pickedBy = self.isPickedByRequired and self:getCharacterPick(game) or nil if self.isPickedByRequired then - return Logic.isNotEmpty(gameRecord.pickedBy) and gameRecord or nil + return Logic.isNotEmpty(game.pickedBy) end - local foundPicks = Table.isNotEmpty(gameRecord.picks[1]) or Table.isNotEmpty(gameRecord.picks[2]) - return (foundPicks or self.config.showGameWithoutCharacters) and gameRecord or nil + local foundPicks = Table.isNotEmpty(game.picks[1]) or Table.isNotEmpty(game.picks[2]) + return foundPicks or self.config.showGameWithoutCharacters end ----@param record table +---@param record MatchGroupUtilMatch ---@return MatchTableMatchResult? function CharacterGameTable:resultFromRecord(record) - return { - opponent = record.match2opponents[1], - vs = record.match2opponents[2], - winner = tonumber(record.winner), - countGames = true, - } + if self.isCharacterTable then + return { + opponent = record.opponents[1], + vs = record.opponents[2], + winner = tonumber(record.winner), + countGames = true, + } + end + return GameTable.resultFromRecord(self, record) end ---@return {games: {w: number, d: number, l: number}} function CharacterGameTable:statsFromMatches() + if not self.isCharacterTable then + return GameTable.statsFromMatches(self) + end local totalGames = {w = 0, d = 0, l = 0} Array.forEach(self.matches, function(match) - ---@cast match GameTableMatch Array.forEach(match.games, function (game, index) local winner = tonumber(game.winner) @@ -276,71 +288,131 @@ function CharacterGameTable:statsFromMatches() } end ----@return Html -function CharacterGameTable:headerRow() - local makeHeaderCell = function(text, width) - return mw.html.create('th'):css('max-width', width):node(text) - end - +---@protected +---@return table[] +function CharacterGameTable:buildColumnDefinitions() 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.displayGameIcons and makeHeaderCell(nil, '25px') or nil, - config.showIcon and makeHeaderCell(nil, '25px'):addClass('unsortable') or nil, - makeHeaderCell('Tournament') + local isCharTable = self.isCharacterTable + return WidgetUtil.collect( + { + -- Date column + align = 'left', + sortType = 'number', + }, + config.showTier and {align = 'left'} or nil, + config.showType and {align = 'center'} or nil, + config.displayGameIcons and {align = 'center'} or nil, + config.showIcon and { + align = 'center', + unsortable = true, + } or nil, + { + -- Tournament column + align = 'left', + }, + config.showResult and WidgetUtil.collect( + not isCharTable and {align = 'center'} or nil, + { + align = 'center', + unsortable = true, + }, + config.showBans and { + align = 'center', + unsortable = true + } or nil, + isCharTable and { + {align = 'center'}, + {align = 'center'}, + {align = 'center'}, + } or nil, + { + align = 'center', + unsortable = true, + }, + config.showBans and { + align = 'center', + unsortable = true + } or nil + ) or nil, + config.showLength and { + align = 'left', + } or nil, + config.showPatch and { + align = 'left', + } or nil, + config.showVod and { + align = 'left', + unsortable = true, + } or nil, + config.showMatchPage and { + align = 'center', + unsortable = true, + } or nil ) +end - if config.showResult then - local isCharTable = self.isCharacterTable - nodes = Array.appendWith(nodes, - not isCharTable and makeHeaderCell('vs.', '80px') or nil, - makeHeaderCell('Picks'):addClass('unsortable'), - config.showBans and makeHeaderCell('Bans'):addClass('unsortable') or nil, - isCharTable and makeHeaderCell(nil, '80px') or nil, - isCharTable and makeHeaderCell('Score') or nil, - isCharTable and makeHeaderCell(nil, '80px') or nil, - makeHeaderCell('vs. Picks'):addClass('unsortable'), - config.showBans and makeHeaderCell('vs. Bans'):addClass('unsortable') or nil - ) +---@return Widget +function CharacterGameTable:headerRow() + ---@param text string? + ---@return Widget + local makeHeaderCell = function(text) + return TableWidgets.CellHeader{children = text} end - nodes = Array.append(nodes, - config.showLength and makeHeaderCell('Length') or nil, - config.showVod and makeHeaderCell('VOD', '60px') or nil, - config.showMatchPage and makeHeaderCell('') or nil - ) - - local header = mw.html.create('tr') - Array.forEach(nodes, function (node) - header:node(node) - end) + local config = self.config + local isCharTable = self.isCharacterTable - return header + return TableWidgets.TableHeader{children = { + TableWidgets.Row{children = WidgetUtil.collect( + makeHeaderCell('Date'), + config.showTier and makeHeaderCell('Tier') or nil, + config.showType and makeHeaderCell('Type') or nil, + config.displayGameIcons and makeHeaderCell() or nil, + config.showIcon and makeHeaderCell() or nil, + makeHeaderCell('Tournament'), + config.showResult and WidgetUtil.collect( + not isCharTable and makeHeaderCell('vs.') or nil, + makeHeaderCell('Picks'), + config.showBans and makeHeaderCell('Bans') or nil, + isCharTable and { + makeHeaderCell(), + makeHeaderCell('Score'), + makeHeaderCell(), + } or nil, + makeHeaderCell('vs. Picks'), + config.showBans and makeHeaderCell('vs. Bans') or nil + ) or nil, + config.showLength and makeHeaderCell('Length') or nil, + config.showPatch and makeHeaderCell('Patch') or nil, + config.showVod and TableWidgets.CellHeader{ + align = 'center', + children = 'VOD' + } or nil, + config.showMatchPage and makeHeaderCell() or nil + )} + }} end ---@param game CharacterGameTableGame ---@param opponentIndex number ---@param key string ----@return Html? +---@return Widget function CharacterGameTable:_displayCharacters(game, opponentIndex, key) local config = self.config - local makeIcon = function(character) - return CharacterIcon.Icon{character = character, size = config.iconSize, date = game.date} - end - local icons = Array.map(game[key][opponentIndex] or {}, makeIcon) - - return mw.html.create('td') - :addClass(config.showSideClass and self:getSideClass(game.extradata, opponentIndex) or nil) - :node(#icons > 0 and table.concat(icons, config.iconSeparator) or nil) + return TableWidgets.Cell{ + classes = key == 'bans' and {'lor-graycard'} or nil, + children = MatchSummaryCharacters{ + bg = config.showSideClass and self:getSideClass(game.extradata, opponentIndex) or nil, + characters = game[key][opponentIndex] or {}, + date = game.date, + } + } end ----@param match GameTableMatch +---@param match CharacterGameTableMatch ---@param game CharacterGameTableGame ----@return Html? +---@return Widget[]? function CharacterGameTable:displayGame(match, game) if not self.config.showResult then return @@ -351,90 +423,116 @@ function CharacterGameTable:displayGame(match, game) ---@cast pickedBy -nil local pickedVs = pickedBy == 1 and 2 or 1 local opponentRecords = {match.result.opponent, match.result.vs} - return mw.html.create() - :node(self:_displayDraft(game, opponentRecords[pickedBy], false)) - :node(self:_displayScore(game, pickedBy, pickedVs)) - :node(self:_displayDraft(game, opponentRecords[pickedVs], true)) + return WidgetUtil.collect( + self:_displayDraft(game, opponentRecords[pickedBy], pickedBy, false), + self:_displayScore(game, pickedBy, pickedVs), + self:_displayDraft(game, opponentRecords[pickedVs], pickedVs, true) + ) else - return mw.html.create() - :node(self:_displayOpponent(match.result.vs):css('text-align', 'left')) - :node(self:_displayDraft(game, match.result.opponent)) - :node(self:_displayDraft(game, match.result.vs)) + local indexes = match.result.flipped and {2, 1} or {1, 2} + return WidgetUtil.collect( + self:_displayOpponent(match.result.vs), + self:_displayDraft(game, match.result.opponent, indexes[1]), + self:_displayDraft(game, match.result.vs, indexes[2]) + ) end end ---@param game CharacterGameTableGame ----@param opponentRecord match2opponent +---@param opponentRecord standardOpponent +---@param opponentIndex integer ---@param flipped boolean? ----@return Html? -function CharacterGameTable:_displayDraft(game, opponentRecord, flipped) - local opponentIndex = opponentRecord.id - +---@return Widget[]? +function CharacterGameTable:_displayDraft(game, opponentRecord, opponentIndex, flipped) local isCharTable = self.isCharacterTable local opponent = self:_displayOpponent(opponentRecord, flipped) - return mw.html.create() - :node((flipped and isCharTable) and opponent or nil) - :node(self:_displayCharacters(game, opponentIndex, 'picks')) - :node(self.config.showBans and - self:_displayCharacters(game, opponentIndex, 'bans'):addClass('lor-graycard') or nil - ) - :node((not flipped and isCharTable) and opponent or nil) + return WidgetUtil.collect( + (flipped and isCharTable) and opponent or nil, + self:_displayCharacters(game, opponentIndex, 'picks'), + self.config.showBans and self:_displayCharacters(game, opponentIndex, 'bans') or nil, + (not flipped and isCharTable) and opponent or nil + ) end ---@param game CharacterGameTableGame ---@param pickedBy number ---@param pickedVs number ----@return Html +---@return Widget function CharacterGameTable:_displayScore(game, pickedBy, pickedVs) local winner = tonumber(game.winner) local scores = Array.map(game.opponents, Operator.property('score')) local toScore = function(opponentId) local isWinner = winner == opponentId - return mw.html.create(isWinner and 'b' or nil) - :wikitext(scores[opponentId] or (isWinner and 'W' or 'L')) + return HtmlWidgets.Span{ + css = {['font-weight'] = isWinner and 'bold' or nil}, + children = scores[opponentId] or (isWinner and 'W' or 'L') + } end - return mw.html.create('td') - :addClass('match-table-score') - :node(toScore(pickedBy)) - :node(SCORE_CONCAT) - :node(toScore(pickedVs)) + return TableWidgets.Cell{children = { + toScore(pickedBy), + SCORE_CONCAT, + toScore(pickedVs), + }} end ---@param game CharacterGameTableGame ----@return Html? +---@return Widget? function CharacterGameTable:_displayLength(game) if not self.config.showLength then return end - return mw.html.create('td') - :node(game.length) + return TableWidgets.Cell{children = game.length} +end + +---@private +---@param game CharacterGameTableGame +---@return Widget? +function CharacterGameTable:_displayPatch(game) + if not self.config.showPatch then return end + + if Logic.isEmpty(game.patch) then + return TableWidgets.Cell{} + end + + return TableWidgets.Cell{children = self:getPatchLink(game)} +end + +---@protected +---@param game CharacterGameTableGame +---@return Widget? +function CharacterGameTable:getPatchLink(game) + return Link{link = 'Patch ' .. game.patch, children = game.patch} end ----@param match GameTableMatch +---@param match CharacterGameTableMatch ---@param game CharacterGameTableGame ----@return Html? +---@return Widget function CharacterGameTable:gameRow(match, game) - local winner = (self.isCharacterTable and game.pickedBy or - match.result.opponent.id) == tonumber(game.winner) and 1 or 2 - - return mw.html.create('tr') - :addClass(self:_getBackgroundClass(winner)) - :node(self:_displayDate(match)) - :node(self:_displayTier(match)) - :node(self:_displayType(match)) - :node(self:_displayGameIconForGame(game)) - :node(self:_displayIcon(match)) - :node(self:_displayTournament(match)) - :node(self:displayGame(match, game)) - :node(self:_displayLength(game)) - :node(self:_displayGameVod(game.vod)) - :node(self:_displayMatchPage(match)) + local indexes = ((self.isCharacterTable and game.pickedBy == game.winner) or match.result.flipped) and {2, 1} or {1, 2} + local winner = indexes[game.winner] + + return TableWidgets.Row{ + classes = {self:getBackgroundClass(winner)}, + children = WidgetUtil.collect( + self:_displayDate(match), + self:displayTier(match), + self:_displayType(match), + self:_displayGameIconForGame(game), + self:_displayIcon(match), + self:_displayTournament(match), + self:displayGame(match, game), + self:_displayLength(game), + self:_displayPatch(game), + self:_displayGameVod(game.vod), + self:_displayMatchPage(match) + ) + } end ---@param frame Frame ----@return Html +---@return Widget function CharacterGameTable.results(frame) local args = Arguments.getArgs(frame) diff --git a/lua/wikis/commons/GameTable/Character/Custom.lua b/lua/wikis/commons/GameTable/Character/Custom.lua new file mode 100644 index 00000000000..d10d0bb72c4 --- /dev/null +++ b/lua/wikis/commons/GameTable/Character/Custom.lua @@ -0,0 +1,12 @@ +--- +-- @Liquipedia +-- page=Module:GameTable/Character/Custom +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Lua = require('Module:Lua') + +local CharacterGameTable = Lua.import('Module:GameTable/Character') + +return CharacterGameTable diff --git a/lua/wikis/commons/GameTable/Custom.lua b/lua/wikis/commons/GameTable/Custom.lua index 4a27cd360b0..6a02e0b214e 100644 --- a/lua/wikis/commons/GameTable/Custom.lua +++ b/lua/wikis/commons/GameTable/Custom.lua @@ -14,7 +14,7 @@ local GameTable = Lua.import('Module:GameTable') local CustomGameTable = {} ---@param args table ----@return Html +---@return Widget function CustomGameTable.results(args) return GameTable(args):readConfig():query():build() end diff --git a/lua/wikis/commons/MatchPage/Base.lua b/lua/wikis/commons/MatchPage/Base.lua index a298d248890..4cfec59f4ac 100644 --- a/lua/wikis/commons/MatchPage/Base.lua +++ b/lua/wikis/commons/MatchPage/Base.lua @@ -512,14 +512,12 @@ function BaseMatchPage:previousMatches() headToHead and AdditionalSection{ css = {flex = '2 0 100%'}, header = 'Head to Head', - bodyClasses = {'match-table-wrapper'}, children = headToHead, } or nil, Array.map(self.opponents, function (opponent) local matchTable = self:_buildMatchTable(opponent) return AdditionalSection{ header = OpponentDisplay.InlineOpponent{opponent = opponent, teamStyle = 'hybrid'}, - bodyClasses = matchTable and {'match-table-wrapper'} or nil, children = matchTable or self:getTournamentIcon() } end) @@ -537,7 +535,7 @@ end ---@private ---@param props table ----@return Html +---@return Widget function BaseMatchPage:_createMatchTable(props) return MatchTable(Table.mergeInto({ addCategory = false, @@ -554,7 +552,7 @@ end ---@private ---@param opponent standardOpponent ----@return Html? +---@return Widget? function BaseMatchPage:_buildMatchTable(opponent) if not BaseMatchPage._isTeamOpponent(opponent) then return @@ -570,7 +568,7 @@ function BaseMatchPage:_buildMatchTable(opponent) end ---@private ----@return Html? +---@return Widget? function BaseMatchPage:_buildHeadToHeadMatchTable() if not Array.all(self.opponents, BaseMatchPage._isTeamOpponent) then return diff --git a/lua/wikis/commons/MatchTable.lua b/lua/wikis/commons/MatchTable.lua index 41bb8983ec9..eb2abf6dec4 100644 --- a/lua/wikis/commons/MatchTable.lua +++ b/lua/wikis/commons/MatchTable.lua @@ -35,6 +35,8 @@ local OpponentDisplay = Lua.import('Module:OpponentDisplay/Custom') local Link = Lua.import('Module:Widget/Basic/Link') local MatchPageButton = Lua.import('Module:Widget/Match/PageButton') local HtmlWidgets = Lua.import('Module:Widget/Html/All') +local TableWidgets = Lua.import('Module:Widget/Table2/All') +local WidgetUtil = Lua.import('Module:Widget/Util') local Condition = Lua.import('Module:Condition') local ConditionTree = Condition.Tree @@ -42,6 +44,7 @@ local ConditionNode = Condition.Node local Comparator = Condition.Comparator local BooleanOperator = Condition.BooleanOperator local ColumnName = Condition.ColumnName +local ConditionUtil = Condition.Util local DRAW_WINNER = 0 local INVALID_TIER_DISPLAY = 'Undefined' @@ -52,6 +55,7 @@ local BO1_SCORE_CONCAT = ' - ' local SECONDS_ONE_DAY = 3600 * 24 ---@alias MatchTableMode `Opponent.solo` | `Opponent.team` +---@alias WDLCount {w: number, d: number, l: number} ---@class MatchTableConfig ---@field mode MatchTableMode @@ -87,11 +91,12 @@ local SECONDS_ONE_DAY = 3600 * 24 ---@field result MatchTableMatchResult ---@class MatchTableMatchResult ----@field opponent match2opponent +---@field opponent standardOpponent ---@field gameOpponents table[] ----@field vs match2opponent +---@field vs standardOpponent ---@field gameVsOpponents table[] ----@field winner number +---@field winner number? +---@field flipped boolean ---@field countGames boolean ---@field countRounds boolean @@ -201,7 +206,7 @@ function MatchTable:_readOpponentInputsFromBase(base) if Logic.isNotEmpty(inputs) or Logic.isEmpty(self.args[base .. 's']) then return inputs end - return Array.map(mw.text.split(self.args[base .. 's'], ',', true), String.trim) + return Array.parseCommaSeparatedString(self.args[base .. 's']) end ---@param mode MatchTableMode @@ -223,7 +228,7 @@ function MatchTable:readAliases(mode) local aliases = {} if String.isEmpty(self.args.aliases) then return aliases end - local aliasInput = Array.map(mw.text.split(self.args.aliases, ','), String.trim) + local aliasInput = Array.parseCommaSeparatedString(self.args.aliases) Array.forEach(aliasInput, function(alias) alias = alias:gsub(' ', '_') @@ -282,10 +287,10 @@ function MatchTable:readTimeRange() end --build year range from subpage name (or input) - local yearRange = Array.map(mw.text.split(yearsString, '-'), String.trim) - yearRange = { - tonumber(yearRange[1]), - tonumber(yearRange[2] or yearRange[1]), + local yearInput = Array.parseCommaSeparatedString(yearsString, '-') + local yearRange = { + tonumber(yearInput[1]), + tonumber(yearInput[2] or yearInput[1]), } --sort @@ -304,7 +309,7 @@ function MatchTable:query() self.matches = {} Lpdb.executeMassQuery('match2', { - conditions = self:buildConditions(), + conditions = tostring(self:buildConditions()), order = 'date desc', query = 'match2id, match2opponents, match2games, date, dateexact, icon, icondark, liquipediatier, game, type,' .. 'liquipediatiertype, tournament, pagename, parent, section, tickername, vod, winner, match2bracketdata,' @@ -326,14 +331,13 @@ function MatchTable:query() return self end ----@return string +---@return ConditionTree function MatchTable:buildConditions() return ConditionTree(BooleanOperator.all) - :add{ConditionNode(ColumnName('finished'), Comparator.eq, 1)} - :add{self:buildDateConditions()} - :add{self:buildOpponentConditions()} - :add{self:buildAdditionalConditions()} - :toString() + :add(ConditionNode(ColumnName('finished'), Comparator.eq, 1)) + :add(self:buildDateConditions()) + :add(self:buildOpponentConditions()) + :add(self:buildAdditionalConditions()) end ---@return ConditionTree? @@ -385,17 +389,14 @@ end ---@return ConditionTree? function MatchTable:buildAdditionalConditions() local args = self.args - local conditions = ConditionTree(BooleanOperator.all) - :add{ConditionNode(ColumnName('status'), Comparator.neq, 'notplayed')} + local conditions = ConditionTree(BooleanOperator.all):add( + ConditionNode(ColumnName('status'), Comparator.neq, 'notplayed') + ) local getOrCondition = function(lpdbKey, input) if Logic.isEmpty(input) then return end - local orConditions = ConditionTree(BooleanOperator.any) - Array.forEach(mw.text.split(input, ','), function(value) - orConditions:add{ConditionNode(ColumnName(lpdbKey), Comparator.eq, String.trim(value))} - end) - conditions:add(orConditions) + conditions:add(ConditionUtil.anyOf(ColumnName(lpdbKey), Array.parseCommaSeparatedString(input))) end getOrCondition('liquipediatier', args.tier) @@ -412,21 +413,17 @@ function MatchTable:buildAdditionalConditions() return conditions end ----@param record table +---@param record match2 ---@return MatchTableMatch? function MatchTable:matchFromRecord(record) - local result = self:resultFromRecord(record) + local match = MatchGroupUtil.matchFromRecord(record) --[[@as MatchTableMatch]] + local result = self:resultFromRecord(match) if not result then return end - record.extradata = record.extradata or {} - - ---@type MatchTableMatch - local match = Table.merge({ - vods = self:vodsFromRecord(record), - result = result, - }, MatchGroupUtil.matchFromRecord(record)) + match.result = result + match.vods = self:vodsFromRecord(match) local tournament = Tournament.partialTournamentFromMatch(match) @@ -440,16 +437,16 @@ function MatchTable:matchFromRecord(record) return match end ----@param record table +---@param record MatchGroupUtilMatch ---@return {index: number, link: string}[] function MatchTable:vodsFromRecord(record) local vods = {} if String.nilIfEmpty(record.vod) then - vods = {{index = 0, link = record.vod}} + vods[1] = {index = 0, link = record.vod} end - Array.forEach(record.match2games, function(game, gameIndex) - if String.nilIfEmpty(game.vod) then + Array.forEach(record.games, function(game, gameIndex) + if String.isNotEmpty(game.vod) then table.insert(vods, {link = game.vod, index = gameIndex}) end end) @@ -457,10 +454,10 @@ function MatchTable:vodsFromRecord(record) return vods end ----@param record table +---@param record MatchGroupUtilMatch ---@return MatchTableMatchResult? function MatchTable:resultFromRecord(record) - if #record.match2opponents ~= 2 then + if #record.opponents ~= 2 then return self:resultFromNonStandardRecord(record) end @@ -468,23 +465,27 @@ function MatchTable:resultFromRecord(record) local countGames = false local countRounds = false + ---@param opponentRecord standardOpponent + ---@return boolean local foundInAlias = function(opponentRecord) if aliases[opponentRecord.name] then countGames = true countRounds = self.config.showRoundStats return true end - return self.config.mode == Opponent.solo and Array.any(opponentRecord.match2players, function(player) - return aliases[player.name] or false + return self.config.mode == Opponent.solo and Array.any(opponentRecord.players, function(player) + return aliases[player.pageName] or false end) end - local winner = tonumber(record.winner) + local winner = record.winner + local flipped = false local indexes - if foundInAlias(record.match2opponents[1]) then + if foundInAlias(record.opponents[1]) then indexes = {1, 2} - elseif foundInAlias(record.match2opponents[2]) then + elseif foundInAlias(record.opponents[2]) then indexes = {2, 1} + flipped = true winner = winner == 2 and 1 or winner == 1 and 2 or winner else mw.ext.TeamLiquidIntegration.add_category('MatchesTables with invalid matches') @@ -492,11 +493,14 @@ function MatchTable:resultFromRecord(record) return end - local gameOpponents = Array.map(record.match2games, Operator.property('opponents')) + local gameOpponents = Array.map(record.games, Operator.property('opponents')) + + ---@type MatchTableMatchResult local result = { - opponent = record.match2opponents[indexes[1]], - vs = record.match2opponents[indexes[2]], + opponent = record.opponents[indexes[1]], + vs = record.opponents[indexes[2]], winner = winner, + flipped = flipped, countGames = countGames, countRounds = countRounds, gameOpponents = Array.map(gameOpponents, Operator.property(indexes[1])), @@ -507,12 +511,12 @@ function MatchTable:resultFromRecord(record) end ---overwritable for wikis that have BR/FFA matches ----@param record table +---@param record MatchGroupUtilMatch ---@return table? function MatchTable:resultFromNonStandardRecord(record) end ----@return {matches: {w: number, d: number, l: number}, games: {w: number, d: number, l: number}} +---@return {matches: WDLCount, games: WDLCount, rounds: WDLCount} function MatchTable:statsFromMatches() local totalMatches = {w = 0, d = 0, l = 0} local totalGames = {w = 0, d = 0, l = 0} @@ -522,6 +526,8 @@ function MatchTable:statsFromMatches() return math.max(tonumber(value) or 0, 0) end + ---@param opponent standardOpponent + ---@return boolean local hasWalkoverStatus = function(opponent) return Logic.isNotEmpty(opponent.status) and opponent.status ~= 'S' end @@ -559,7 +565,7 @@ function MatchTable:statsFromMatches() } end ----@return Html +---@return Widget function MatchTable:buildDisplay() local display = mw.html.create('table') :addClass('wikitable wikitable-striped sortable') @@ -567,18 +573,6 @@ function MatchTable:buildDisplay() :node(self:_titleRow(self.config.title)) :node(self:headerRow()) - if Table.isEmpty(self.matches) then - local text = 'This ' .. (self.config.mode == Opponent.solo and Opponent.solo or Opponent.team) - .. ' has not played any matches yet.' - - return display:tag('tr') - :tag('td') - :attr('colspan', '100') - :css('font-style', 'italic') - :wikitext(text) - :allDone() - end - local currentYear Array.forEach(self.matches, function(match) local year = tonumber(match.date:sub(1, 4)) @@ -589,28 +583,25 @@ function MatchTable:buildDisplay() display:node(self:matchRow(match)) end) - if self.config.linkSubPage then - local pagename = self.title.text .. '/Matches' - display:tag('tr') - :tag('th') - :attr('colspan', 42) - :css('font-style', 'italic') - :wikitext('[[' .. pagename .. '|Extended list of matches]]') - end - - return display + return TableWidgets.Table{ + classes = {'match-table-wrapper'}, + sortable = true, + columns = self:buildColumnDefinitions(), + title = String.nilIfEmpty(self.config.title), + children = WidgetUtil.collect( + self:headerRow(), + TableWidgets.TableBody{children = self:buildBody()} + ), + footer = self:buildFooter() + } end ----@return Html +---@return Widget function MatchTable:build() - local wrappedTableNode = mw.html.create('div') - :addClass('match-table-wrapper') - :addClass('table-responsive') - :node(self:buildDisplay()) - - return mw.html.create('div') - :node(self:displayStats()) - :node(wrappedTableNode) + return HtmlWidgets.Fragment{children = WidgetUtil.collect( + self:displayStats(), + self:buildDisplay() + )} end ---@param title string @@ -624,80 +615,183 @@ function MatchTable:_titleRow(title) :done() end +---@protected +---@return table[] +function MatchTable:buildColumnDefinitions() + local config = self.config + return WidgetUtil.collect( + { + -- Date column + align = 'left', + sortType = 'number', + }, + config.showTier and {align = 'left'} or nil, + config.showType and {align = 'center'} or nil, + config.displayGameIcons and {align = 'center'} or nil, + config.showIcon and { + align = 'center', + unsortable = true, + } or nil, + { + -- Tournament column + align = 'left', + }, + config.showResult and WidgetUtil.collect( + config.showOpponent and {align = 'center'} or nil, + { + -- Score column + align = 'center', + }, + { + -- vs Opponent column + align = 'left' + } + ) or nil, + config.showVod and { + align = 'left', + unsortable = true, + } or nil, + config.showMatchPage and { + align = 'center', + unsortable = true, + } or nil + ) +end + ---@param year number? ----@return Html? +---@return Widget? function MatchTable:_yearRow(year) if not year then return end - return mw.html.create('tr') - :addClass('sortbottom') - :tag('td') - :attr('colspan', '100') - :addClass('match-table-year-header') - :wikitext(year) - :done() + return TableWidgets.Row{ + section = 'subhead', + classes = {'sortbottom'}, + css = {['font-weight'] = 'bold'}, + children = TableWidgets.CellHeader{ + align = 'center', + colspan = 100, + children = year + } + } end ---@return Html function MatchTable:headerRow() - local makeHeaderCell = function(text, width) - return mw.html.create('th'):css('max-width', width):node(text) + ---@param text string? + ---@return Widget + local makeHeaderCell = function(text) + return TableWidgets.CellHeader{children = text} end local config = self.config - return mw.html.create('tr') - :node(makeHeaderCell('Date', '100px')) - :node(config.showTier and makeHeaderCell('Tier', '70px') or nil) - :node(config.showType and makeHeaderCell('Type', '70px') or nil) - :node(config.displayGameIcons and makeHeaderCell(nil, '25px') or nil) - :node(config.showIcon and makeHeaderCell(nil, '25px'):addClass('unsortable') or nil) - :node(makeHeaderCell('Tournament')) - :node(config.showResult and config.showOpponent and makeHeaderCell('Participant', '120px') or nil) - :node(config.showResult and makeHeaderCell('Score', '68px'):addClass('unsortable') or nil) - :node(config.showResult and makeHeaderCell('vs. Opponent', '120px') or nil) - :node(config.showVod and makeHeaderCell('VOD(s)', '80px'):addClass('unsortable') or nil) - :node(config.showMatchPage and makeHeaderCell(''):addClass('unsortable') or nil) + return TableWidgets.TableHeader{children = { + TableWidgets.Row{children = WidgetUtil.collect( + makeHeaderCell('Date'), + config.showTier and makeHeaderCell('Tier') or nil, + config.showType and makeHeaderCell('Type') or nil, + config.displayGameIcons and makeHeaderCell() or nil, + config.showIcon and makeHeaderCell() or nil, + makeHeaderCell('Tournament'), + config.showResult and WidgetUtil.collect( + config.showOpponent and makeHeaderCell('Participant') or nil, + makeHeaderCell('Score'), + TableWidgets.CellHeader{ + align = 'center', + children = 'vs. Opponent' + } + ) or nil, + config.showVod and TableWidgets.CellHeader{ + align = 'center', + children = 'VOD(s)' + } or nil, + config.showMatchPage and makeHeaderCell() or nil + )} + }} +end + +---@return Widget[] +function MatchTable:buildBody() + if Table.isEmpty(self.matches) then + local text = 'This ' .. (self.config.mode == Opponent.solo and Opponent.solo or Opponent.team) + .. ' has not played any matches yet.' + return {TableWidgets.Row{ + css = {['font-style'] = 'italic'}, + children = TableWidgets.Cell{ + colspan = 100, + children = text, + } + }} + end + + return self:buildRows() +end + +---@return Widget[] +function MatchTable:buildRows() + ---@type Widget[] + local rows = {} + + local currentYear = math.huge + Array.forEach(self.matches, function(match) + local year = DateExt.getYearOf(match.date) + if self.config.showYearHeaders and year ~= currentYear then + currentYear = year + table.insert(rows, self:_yearRow(year)) + end + table.insert(rows, self:matchRow(match)) + end) + + return rows +end + +---@return Widget? +function MatchTable:buildFooter() + if not self.config.linkSubPage then + return + end + return Link{ + link = self.title.text .. '/Matches', + children = 'Extended list of matches' + } end ---@param match MatchTableMatch ----@return Html? +---@return Widget function MatchTable:matchRow(match) - return mw.html.create('tr') - :addClass(self:_getBackgroundClass(match.result.winner)) - :node(self:_displayDate(match)) - :node(self:_displayTier(match)) - :node(self:_displayType(match)) - :node(self:_displayGameIcon(match)) - :node(self:_displayIcon(match)) - :node(self:_displayTournament(match)) - :node(self:_displayMatch(match)) - :node(self:_displayVods(match)) - :node(self:_displayMatchPage(match)) + return TableWidgets.Row{ + classes = {self:getBackgroundClass(match.result.winner)}, + children = WidgetUtil.collect( + self:_displayDate(match), + self:displayTier(match), + self:_displayType(match), + self:_displayGameIcon(match), + self:_displayIcon(match), + self:_displayTournament(match), + self:_displayMatch(match), + self:_displayVods(match), + self:_displayMatchPage(match) + ) + } end ---@param match MatchTableMatch ----@return Html +---@return Widget function MatchTable:_displayDate(match) - local cell = mw.html.create('td') - :css('text-align', 'left') - :css('min-width', '5rem') - :attr('data-sort-value', match.timestamp) - - if match.timestamp == DateExt.defaultTimestamp then - return cell - end - - return cell:node(Countdown.create{ - finished = match.finished, - date = DateExt.toCountdownArg(match.timestamp, match.timezoneId, match.dateIsExact), - rawdatetime = true, - format = self.config.dateFormat - } or nil) + return TableWidgets.Cell{ + attributes = {['data-sort-value'] = match.timestamp}, + children = not DateExt.isDefaultTimestamp(match.timestamp) and Countdown.create{ + finished = match.finished, + date = DateExt.toCountdownArg(match.timestamp, match.timezoneId, match.dateIsExact), + rawdatetime = true, + format = self.config.dateFormat + } or nil + } end +---@protected ---@param match MatchTableMatch ----@return Html? -function MatchTable:_displayTier(match) +---@return Widget? +function MatchTable:displayTier(match) if not self.config.showTier then return end local tier, tierType, options = Tier.parseFromQueryData(match) @@ -705,60 +799,64 @@ function MatchTable:_displayTier(match) options.onlyTierTypeIfBoth = true if not Tier.isValid(tier, tierType) then - return mw.html.create('td') - :attr('data-sort-value', INVALID_TIER_SORT) - :wikitext(INVALID_TIER_DISPLAY) + return TableWidgets.Cell{ + attributes = {['data-sort-value'] = INVALID_TIER_SORT}, + children = INVALID_TIER_DISPLAY + } end - return mw.html.create('td') - :attr('data-sort-value', Tier.toSortValue(tier, tierType)) - :wikitext(Tier.display(tier, tierType, options)) + return TableWidgets.Cell{ + attributes = {['data-sort-value'] = Tier.toSortValue(tier, tierType)}, + children = Tier.display(tier, tierType, options) + } end ---@param match MatchTableMatch ----@return Html? +---@return Widget? function MatchTable:_displayType(match) if not self.config.showType then return end - return mw.html.create('td') - :wikitext(match.type and mw.getContentLanguage():ucfirst(match.type) or nil) + return TableWidgets.Cell{ + children = match.type and String.upperCaseFirst(match.type) or nil + } end ---@param match MatchTableMatch ----@return Html? +---@return Widget? function MatchTable:_displayGameIcon(match) if not self.config.displayGameIcons then return end - return mw.html.create('td') - :node(Game.icon{game = match.game}) + return TableWidgets.Cell{ + children = Game.icon{game = match.game} + } end ---@param match MatchTableMatch ----@return Html? +---@return Widget? function MatchTable:_displayIcon(match) if not self.config.showIcon then return end - return mw.html.create('td') - :node(LeagueIcon.display{ + return TableWidgets.Cell{ + children = LeagueIcon.display{ icon = match.icon, iconDark = match.iconDark, link = match.pageName, name = match.displayName, options = {noTemplate = true}, - }) + } + } end ---@param match MatchTableMatch ---@return Widget function MatchTable:_displayTournament(match) - return HtmlWidgets.Td{ - css = {['text-align'] = 'left'}, + return TableWidgets.Cell{ children = Link{children = match.displayName, link = match.pageName} } end ---@param match MatchTableMatch ----@return Html? +---@return Widget|Widget[]? function MatchTable:_displayMatch(match) if not self.config.showResult then return @@ -766,37 +864,36 @@ function MatchTable:_displayMatch(match) return self:nonStandardMatch(match) end - return mw.html.create() - :node(self.config.showOpponent and self:_displayOpponent(match.result.opponent, true) or nil) - :node(self:_displayScore(match)) - :node(self:_displayOpponent(match.result.vs):css('text-align', 'left')) + return WidgetUtil.collect( + self.config.showOpponent and self:_displayOpponent(match.result.opponent, true) or nil, + self:_displayScore(match), + self:_displayOpponent(match.result.vs) + ) end ---overwritable for wikis that have BR/FFA matches ---@param match MatchTableMatch ----@return Html +---@return Widget function MatchTable:nonStandardMatch(match) - return mw.html.create('td') - :attr('colspan', self.config.showOpponent and 3 or 2) - :wikitext('') + return TableWidgets.Cell{ + colspan = self.config.showOpponent and 3 or 2, + children = '', + } end ----@param opponentRecord match2opponent +---@param opponent standardOpponent ---@param flipped boolean? ----@return Html -function MatchTable:_displayOpponent(opponentRecord, flipped) - local cell = mw.html.create('td') - local opponent = Opponent.fromMatch2Record(opponentRecord) - if Logic.isEmpty(opponent) then return cell:wikitext('Unknown') end - - return cell - :node(OpponentDisplay.BlockOpponent{ +---@return Widget +function MatchTable:_displayOpponent(opponent, flipped) + return TableWidgets.Cell{ + attributes = {['data-sort-value'] = Opponent.toName(opponent)}, + children = OpponentDisplay.BlockOpponent{ opponent = opponent, flip = flipped, overflow = 'wrap', teamStyle = self.config.teamStyle, - }) - :attr('data-sort-value', opponent.name) + } + } end ---@param match MatchTableMatch @@ -807,13 +904,13 @@ function MatchTable:_displayScore(match) return opponent.status == 'S' end) local bestof1Score = match.bestof == 1 and Info.config.match2.gameScoresIfBo1 and hasOnlyScores - ---@param opponentRecord match2opponent + ---@param opponent standardOpponent ---@param gameOpponents table[] - ---@return Html|string - local toScore = function(opponentRecord, gameOpponents) - if Table.isEmpty(opponentRecord) or not opponentRecord.status then return 'Unkn' end - local score = OpponentDisplay.InlineScore(opponentRecord) - local status = opponentRecord.status + ---@return string|Widget + local toScore = function(opponent, gameOpponents) + if Table.isEmpty(opponent) or not opponent.status then return 'Unkn' end + local score = OpponentDisplay.InlineScore(opponent) + local status = opponent.status local game1Opponent = gameOpponents[1] if bestof1Score and game1Opponent then @@ -821,16 +918,17 @@ function MatchTable:_displayScore(match) status = game1Opponent.status end - return mw.html.create(tonumber(opponentRecord.placement) == 1 and 'b' or nil) - :wikitext(status == SCORE_STATUS and (score or '–') or status) + return HtmlWidgets.Span{ + css = {['font-weight'] = tonumber(opponent.placement) == 1 and 'bold' or nil}, + children = status == SCORE_STATUS and (score or '–') or status, + } end - return mw.html.create('td') - :addClass('match-table-score') - :css('white-space', 'nowrap') - :node(toScore(result.opponent, result.gameOpponents)) - :node(bestof1Score and BO1_SCORE_CONCAT or SCORE_CONCAT) - :node(toScore(result.vs, result.gameVsOpponents)) + return TableWidgets.Cell{children = { + toScore(result.opponent, result.gameOpponents), + bestof1Score and BO1_SCORE_CONCAT or SCORE_CONCAT, + toScore(result.vs, result.gameVsOpponents) + }} end ---@param match MatchTableMatch @@ -838,15 +936,11 @@ end function MatchTable:_displayVods(match) if not self.config.showVod then return end - local vodsNode = mw.html.create('td'):css('text-align', 'left') - Array.forEach(match.vods, function(vod, vodIndex) - if vodIndex ~= 1 then - vodsNode:node(' ') - end - vodsNode:node(VodLink.display{vod = vod.link, gamenum = vod.index ~= 0 and vod.index or nil}) - end) - - return vodsNode + return TableWidgets.Cell{ + children = Array.interleave(Array.map(match.vods, function (vod) + return VodLink.display{vod = vod.link, gamenum = vod.index ~= 0 and vod.index or nil} + end), ' ') + } end ---@param match MatchTableMatch @@ -854,19 +948,22 @@ end function MatchTable:_displayMatchPage(match) if not self.config.showMatchPage then return end - return mw.html.create('td'):node(MatchPageButton{match = match, buttonText = self.config.matchPageButtonText}) + return TableWidgets.Cell{ + children = MatchPageButton{match = match, buttonText = self.config.matchPageButtonText} + } end ----@param winner any +---@protected +---@param winner integer ---@return string? -function MatchTable:_getBackgroundClass(winner) - return winner == 1 and 'recent-matches-bg-win' or - winner == 0 and 'recent-matches-bg-tie' or - winner == 2 and 'recent-matches-bg-lose' or +function MatchTable:getBackgroundClass(winner) + return winner == 1 and 'match-table-row__win' or + winner == 0 and 'match-table-row__draw' or + winner == 2 and 'match-table-row__loss' or nil end ----@return Html? +---@return Widget? function MatchTable:displayStats() if not self.config.showStats or Table.isEmpty(self.matches) then return end @@ -886,11 +983,11 @@ function MatchTable:displayStats() data.l .. 'L' ), ' : ') - local percentage = Math.round((data.w + 0.5 * data.d) / sum, 4) * 100 + local percentage = Math.formatPercentage((data.w + 0.5 * data.d) / sum, 2) local parts = { scoreText, - '(' .. percentage .. '%)', + '(' .. percentage .. ')', 'in', statsType, } @@ -899,9 +996,9 @@ function MatchTable:displayStats() end local makeStatsTitle = function() - if startTimeStamp == DateExt.defaultTimestamp and endTimeStamp == DateExt.defaultTimestamp then + if DateExt.isDefaultTimestamp(startTimeStamp) and DateExt.isDefaultTimestamp(endTimeStamp) then return 'For all matches:' - elseif startTimeStamp == DateExt.defaultTimestamp then + elseif DateExt.isDefaultTimestamp(startTimeStamp) then return 'For all matches before '.. DateExt.formatTimestamp('M d, Y', endTimeStamp) .. ':' end @@ -910,9 +1007,10 @@ function MatchTable:displayStats() return 'For matches between ' .. startDate .. ' and ' .. endDate .. ':' end - local titleNode = mw.html.create('div') - :css('font-weight', 'bold') - :wikitext(makeStatsTitle()) + local titleNode = HtmlWidgets.Div{ + css = {['font-weight'] = 'bold'}, + children = makeStatsTitle(), + } local stats = Array.append({}, self.config.showOnlyGameStats and '' or displayScores(self.stats.matches, 'matches'), @@ -920,12 +1018,10 @@ function MatchTable:displayStats() self.config.showOnlyGameStats and '' or displayScores(self.stats.rounds, 'rounds') ) - return mw.html.create('div') - :node(titleNode) - :tag('div') - :wikitext(table.concat(stats, self.config.showOnlyGameStats and '' or ' and ')) - :wikitext() - :done() + return HtmlWidgets.Div{children = { + titleNode, + HtmlWidgets.Div{children = Array.interleave(stats, self.config.showOnlyGameStats and '' or ' and ')} + }} end return MatchTable diff --git a/lua/wikis/commons/MatchTable/Custom.lua b/lua/wikis/commons/MatchTable/Custom.lua index 9e38bd4a7d2..787c8a33d2e 100644 --- a/lua/wikis/commons/MatchTable/Custom.lua +++ b/lua/wikis/commons/MatchTable/Custom.lua @@ -14,7 +14,7 @@ local MatchTable = Lua.import('Module:MatchTable') local CustomMatchTable = {} ---@param args table ----@return Html +---@return Widget function CustomMatchTable.results(args) return MatchTable(args):readConfig():query():build() end diff --git a/lua/wikis/counterstrike/MatchTable/Custom.lua b/lua/wikis/counterstrike/MatchTable/Custom.lua index 72216c89bac..66fe317e612 100644 --- a/lua/wikis/counterstrike/MatchTable/Custom.lua +++ b/lua/wikis/counterstrike/MatchTable/Custom.lua @@ -13,28 +13,30 @@ local Tier = Lua.import('Module:Tier/Custom') local MatchTable = Lua.import('Module:MatchTable') +local TableWidgets = Lua.import('Module:Widget/Table2/All') + local INVALID_TIER_DISPLAY = 'Undefined' local INVALID_TIER_SORT = 'ZZ' -local CustomMatchTable = {} +---@class CounterstrikeMatchTable: MatchTable +---@operator call(table): CounterstrikeMatchTable +local CustomMatchTable = Class.new(MatchTable) ---@param args table ----@return Html +---@return Widget function CustomMatchTable.results(args) args.showRoundStats = Logic.nilOr(Logic.readBoolOrNil(args.showRoundStats), true) args.gameIcons = Logic.nilOr(Logic.readBoolOrNil(args.gameIcons), true) args.vod = Logic.nilOr(Logic.readBoolOrNil(args.vod), true) args.showType = Logic.nilOr(Logic.readBoolOrNil(args.showType), true) - local matchtable = MatchTable(args) - matchtable._displayTier = CustomMatchTable._displayTier - - return matchtable:readConfig():query():build() + return CustomMatchTable(args):readConfig():query():build() end +---@protected ---@param match MatchTableMatch ----@return Html? -function CustomMatchTable:_displayTier(match) +---@return Widget? +function CustomMatchTable:displayTier(match) if not self.config.showTier then return end local tier, tierType, options = Tier.parseFromQueryData(match) @@ -42,14 +44,16 @@ function CustomMatchTable:_displayTier(match) options.onlyDisplayPrioritized = true if not Tier.isValid(tier, tierType) then - return mw.html.create('td') - :attr('data-sort-value', INVALID_TIER_DISPLAY) - :wikitext(INVALID_TIER_SORT) + return TableWidgets.Cell{ + attributes = {['data-sort-value'] = INVALID_TIER_SORT}, + children = INVALID_TIER_DISPLAY + } end - return mw.html.create('td') - :attr('data-sort-value', Tier.toSortValue(tier, tierType)) - :wikitext(Tier.display(tier, tierType, options)) + return TableWidgets.Cell{ + attributes = {['data-sort-value'] = Tier.toSortValue(tier, tierType)}, + children = Tier.display(tier, tierType, options) + } end return Class.export(CustomMatchTable, {exports = {'results'}}) diff --git a/lua/wikis/deadlock/GameTable/Character/Custom.lua b/lua/wikis/deadlock/GameTable/Character/Custom.lua index b976950ee86..ad429e1972b 100644 --- a/lua/wikis/deadlock/GameTable/Character/Custom.lua +++ b/lua/wikis/deadlock/GameTable/Character/Custom.lua @@ -12,6 +12,8 @@ local Class = Lua.import('Module:Class') local GameTableCharacter = Lua.import('Module:GameTable/Character') +---@class DeadlockCharacterGameTable: CharacterGameTable +---@operator call(table): CharacterGameTable local CustomGameTableCharacter = Class.new(GameTableCharacter) ---@return integer @@ -27,7 +29,7 @@ function CustomGameTableCharacter:getCharacterKey(opponentIndex, playerIndex) end ---@param frame Frame ----@return Html +---@return Widget function CustomGameTableCharacter.results(frame) local args = Arguments.getArgs(frame) diff --git a/lua/wikis/dota2/GameTable/Character/Custom.lua b/lua/wikis/dota2/GameTable/Character/Custom.lua index c9d71ef36a0..3a2ff227373 100644 --- a/lua/wikis/dota2/GameTable/Character/Custom.lua +++ b/lua/wikis/dota2/GameTable/Character/Custom.lua @@ -12,7 +12,10 @@ local Class = Lua.import('Module:Class') local GameTableCharacter = Lua.import('Module:GameTable/Character') +local Link = Lua.import('Module:Widget/Basic/Link') + ---@class Dota2CharacterGameTable: CharacterGameTable +---@operator call(table): Dota2CharacterGameTable local CustomCharacterGameTable = Class.new(GameTableCharacter) ---@return integer @@ -27,8 +30,15 @@ function CustomCharacterGameTable:getCharacterKey(opponentIndex, playerIndex) return 'team' .. opponentIndex .. 'hero' .. playerIndex end +---@protected +---@param game CharacterGameTableGame +---@return Widget? +function CustomCharacterGameTable:getPatchLink(game) + return Link{link = 'Version ' .. game.patch, children = game.patch} +end + ---@param frame Frame ----@return Html +---@return Widget function CustomCharacterGameTable.results(frame) local args = Arguments.getArgs(frame) diff --git a/lua/wikis/valorant/GameTable/Character/Custom.lua b/lua/wikis/valorant/GameTable/Character/Custom.lua index 1f876f3521a..192fe2dc56b 100644 --- a/lua/wikis/valorant/GameTable/Character/Custom.lua +++ b/lua/wikis/valorant/GameTable/Character/Custom.lua @@ -9,16 +9,19 @@ local Lua = require('Module:Lua') local Arguments = Lua.import('Module:Arguments') local Array = Lua.import('Module:Array') -local CharacterIcon = Lua.import('Module:CharacterIcon') local Class = Lua.import('Module:Class') local MathUtil = Lua.import('Module:MathUtil') -local Page = Lua.import('Module:Page') - local Opponent = Lua.import('Module:Opponent/Custom') local CharacterGameTable = Lua.import('Module:GameTable/Character') +local LinkWidget = Lua.import('Module:Widget/Basic/Link') +local MatchSummaryCharacters = Lua.import('Module:Widget/Match/Summary/Characters') +local TableWidgets = Lua.import('Module:Widget/Table2/All') +local WidgetUtil = Lua.import('Module:Widget/Util') + ---@class ValorantCharacterGameTable: CharacterGameTable +---@operator call(table): ValorantCharacterGameTable local CustomCharacterGameTable = Class.new(CharacterGameTable, function (self) self.args.showBans = false @@ -65,46 +68,122 @@ function CustomCharacterGameTable:getCharacterKey(opponentIndex, playerIndex) return 't' .. opponentIndex .. 'p' .. playerIndex .. 'agent' end +---@protected +---@return table[] +function CustomCharacterGameTable:buildColumnDefinitions() + local config = self.config + return WidgetUtil.collect( + { + -- Date column + align = 'left', + sortType = 'number', + }, + config.showTier and {align = 'left'} or nil, + config.showType and {align = 'center'} or nil, + config.showIcon and { + align = 'center', + unsortable = true, + } or nil, + { + -- Tournament column + align = 'left', + }, + { + -- Map column + align = 'left', + }, + config.showResult and WidgetUtil.collect( + config.mode == Opponent.solo and {align = 'left'} or nil, + config.mode ~= Opponent.team and { + {align = 'right'}, -- Kills + {align = 'right'}, -- Deaths + {align = 'right'}, -- Assists + {align = 'right'} -- Ratio + } or nil, + { + -- Picks column + align = 'center', + unsortable = true, + }, + { + -- Team column + align = 'center', + }, + { + -- Score column + align = 'center', + }, + { + -- vs. Team column + align = 'center', + }, + { + -- vs. Picks column + align = 'center', + } + ) or nil, + config.showLength and { + align = 'left', + } or nil, + config.showPatch and { + align = 'left', + } or nil, + config.showVod and { + align = 'left', + unsortable = true, + } or nil, + config.showMatchPage and { + align = 'center', + unsortable = true, + } or nil + ) +end + ---@return Html function CustomCharacterGameTable:headerRow() - local makeHeaderCell = function(text, width) - return mw.html.create('th'):css('max-width', width):node(text) + ---@param text string? + ---@return Widget + local makeHeaderCell = function(text) + return TableWidgets.CellHeader{children = 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, - config.showMatchPage and makeHeaderCell('') or nil - ) - - local header = mw.html.create('tr') - Array.forEach(nodes, function (node) - header:node(node) - end) - - return header + return TableWidgets.TableHeader{children = { + TableWidgets.Row{children = WidgetUtil.collect( + makeHeaderCell('Date'), + config.showTier and makeHeaderCell('Tier') or nil, + config.showType and makeHeaderCell('Type') or nil, + config.showIcon and makeHeaderCell() or nil, + makeHeaderCell('Tournament'), + makeHeaderCell('Map'), + config.showResult and WidgetUtil.collect( + config.mode == Opponent.solo and makeHeaderCell('Pick') or nil, + config.mode ~= Opponent.team and { + makeHeaderCell('K'), + makeHeaderCell('D'), + makeHeaderCell('A'), + makeHeaderCell('Ratio'), + } or nil, + makeHeaderCell(config.mode == Opponent.solo and 'Team Picks' or 'Picks'), + makeHeaderCell(), + makeHeaderCell('Score'), + makeHeaderCell(), + makeHeaderCell('vs. Picks') + ) or nil, + config.showLength and makeHeaderCell('Length') or nil, + config.showPatch and makeHeaderCell('Patch') or nil, + config.showVod and TableWidgets.CellHeader{ + align = 'center', + children = 'VOD' + } or nil, + config.showMatchPage and makeHeaderCell() or nil + )} + }} end ---@param participant table ----@return number? +---@return string? function CustomCharacterGameTable:_getRatio(participant) local kills = tonumber(participant.kills) or 0 local deaths = tonumber(participant.deaths) or 0 @@ -112,35 +191,31 @@ function CustomCharacterGameTable:_getRatio(participant) return nil end - return MathUtil.round(kills / deaths, 1) + return MathUtil.formatRounded{value = kills / deaths, precision = 1} end ----@param match GameTableMatch +---@param match CharacterGameTableMatch ---@param game CharacterGameTableGame ----@return Html? +---@return Widget[]? function CustomCharacterGameTable:displayGame(match, game) - local makeCell = function (text) - return mw.html.create('td'):node(text) + ---@param children Renderable|Renderable[]? + ---@return Table2Cell + local makeCell = function (children) + return TableWidgets.Cell{children = children} 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 indexes = ((self.isCharacterTable and game.pickedBy == game.winner) or match.result.flipped) and {2, 1} or {1, 2} - 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 opponent = match.opponents[indexes[1]] + local opponentVs = match.opponents[indexes[2]] - local node = mw.html.create() - :node(makeCell(Page.makeInternalLink(game.map))) + ---@type Widget[] + local cells = {makeCell(LinkWidget{link = game.map})} + + ---@param cell Widget + local function addCell(cell) + table.insert(cells, cell) + end if self.config.mode ~= Opponent.team then local participant = game.opponents[game.pickedBy].players[game.pickedByplayer] @@ -148,25 +223,27 @@ function CustomCharacterGameTable:displayGame(match, game) 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()) + addCell(makeCell(index > 0 and MatchSummaryCharacters{ + characters = {table.remove(game.picks[game.pickedBy], index)} + } or nil)) 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)) + addCell(makeCell(participant and participant.kills or nil)) + addCell(makeCell(participant and participant.deaths or nil)) + addCell(makeCell(participant and participant.assists or nil)) + addCell(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')) + addCell(self:_displayCharacters(game, indexes[1], 'picks')) + addCell(self:_displayOpponent(opponent, false)) + addCell(self:_displayScore(game, indexes[1], indexes[2])) + addCell(self:_displayOpponent(opponentVs, true)) + addCell(self:_displayCharacters(game, indexes[2], 'picks')) + + return cells end ---@param frame Frame ----@return Html +---@return Widget function CustomCharacterGameTable.results(frame) local args = Arguments.getArgs(frame) diff --git a/stylesheets/Main.scss b/stylesheets/Main.scss index b5f39d63f02..91b2d1400b9 100644 --- a/stylesheets/Main.scss +++ b/stylesheets/Main.scss @@ -26,6 +26,7 @@ @use "commons/Infobox"; @use "commons/Mainpage"; @use "commons/Matchseries"; +@use "commons/MatchTable"; @use "commons/NavigationCard"; @use "commons/Miscellaneous"; @use "commons/BigMatch"; diff --git a/stylesheets/commons/BigMatch.scss b/stylesheets/commons/BigMatch.scss index c4d36717bfb..1d1665b090d 100644 --- a/stylesheets/commons/BigMatch.scss +++ b/stylesheets/commons/BigMatch.scss @@ -1105,19 +1105,12 @@ span.slash { border-color: var( --clr-on-surface-dark-primary-8 ); } - &.match-table-wrapper { - display: block; + &:has( > .match-table-wrapper ) { + display: contents; padding: unset; - @media ( max-width: 767px ) { - overflow-x: auto; - } - - & > table.wikitable { - border-radius: 0.5rem; - overflow: hidden; - height: 100%; - width: 100%; + > .match-table-wrapper { + width: unset; } } } diff --git a/stylesheets/commons/MatchTable.scss b/stylesheets/commons/MatchTable.scss new file mode 100644 index 00000000000..86b979e783f --- /dev/null +++ b/stylesheets/commons/MatchTable.scss @@ -0,0 +1,38 @@ +/******************************************************************************* +Template(s): MatchTable +*******************************************************************************/ +.recent-matches-bg-win { + background-color: var( --table-green-background-color, #f0fff0 ) !important; +} + +.recent-matches-bg-tie { + background-color: var( --table-yellow-background-color, #f9f9c7 ) !important; +} + +.recent-matches-bg-lose { + background-color: var( --table-red-background-color, #f9f0f2 ) !important; +} + +@mixin match-table-colors($row-type, $color-type) { + &.match-table-row__#{$row-type} { + background-color: var( --clr-#{$color-type}-90 ); + + &:hover { + background-color: var( --clr-#{$color-type}-80 ); + } + + .theme--dark & { + background-color: var( --clr-#{$color-type}-10 ); + + &:hover { + background-color: var( --clr-#{$color-type}-20 ); + } + } + } +} + +.match-table-wrapper table.table2__table tr.table2__row--body { + @include match-table-colors( win, semantic-positive ); + @include match-table-colors( draw, semantic-gold ); + @include match-table-colors( loss, semantic-negative ); +} diff --git a/stylesheets/commons/Miscellaneous.scss b/stylesheets/commons/Miscellaneous.scss index 6816da7dfc7..56301a8ecb6 100644 --- a/stylesheets/commons/Miscellaneous.scss +++ b/stylesheets/commons/Miscellaneous.scss @@ -2322,22 +2322,6 @@ Author(s): iMarbot color: #8b0000; } -/******************************************************************************* -Template(s): Template:Team matches table -Author(s): iMarbot via ??? -*******************************************************************************/ -.recent-matches-bg-win { - background-color: var( --table-green-background-color, #f0fff0 ) !important; -} - -.recent-matches-bg-tie { - background-color: var( --table-yellow-background-color, #f9f9c7 ) !important; -} - -.recent-matches-bg-lose { - background-color: var( --table-red-background-color, #f9f0f2 ) !important; -} - /******************************************************************************* Template(s): white-space as classes Author(s): iMarbot