diff --git a/components/match2/commons/match_group_util.lua b/components/match2/commons/match_group_util.lua index c487cf6890..73e321fce2 100644 --- a/components/match2/commons/match_group_util.lua +++ b/components/match2/commons/match_group_util.lua @@ -207,7 +207,7 @@ MatchGroupUtil.types.Walkover = TypeUtil.literalUnion('l', 'ff', 'dq') ---@field map string? ---@field mapDisplayName string? ---@field mode string? ----@field opponents {players: table[]}[] +---@field opponents {status: string?, score: number?, players: table[]}[] ---@field participants table ---@field resultType ResultType? ---@field scores number[] diff --git a/components/match2/wikis/hearthstone/brkts_wiki_specific.lua b/components/match2/wikis/hearthstone/brkts_wiki_specific.lua new file mode 100644 index 0000000000..10ebf43c20 --- /dev/null +++ b/components/match2/wikis/hearthstone/brkts_wiki_specific.lua @@ -0,0 +1,24 @@ +--- +-- @Liquipedia +-- wiki=hearthstone +-- page=Module:Brkts/WikiSpecific +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local FnUtil = require('Module:FnUtil') +local Lua = require('Module:Lua') +local Table = require('Module:Table') + +local BaseWikiSpecific = Lua.import('Module:Brkts/WikiSpecific/Base') + +---@class HearthstoneBrktsWikiSpecific: BrktsWikiSpecific +local WikiSpecific = Table.copy(BaseWikiSpecific) + +WikiSpecific.matchFromRecord = FnUtil.lazilyDefineFunction(function() + local CustomMatchGroupUtil = Lua.import('Module:MatchGroup/Util/Custom') + return CustomMatchGroupUtil.matchFromRecord +end) + + +return WikiSpecific diff --git a/components/match2/wikis/hearthstone/match_group_input_custom.lua b/components/match2/wikis/hearthstone/match_group_input_custom.lua index 3cf9de3946..1b84fc5415 100644 --- a/components/match2/wikis/hearthstone/match_group_input_custom.lua +++ b/components/match2/wikis/hearthstone/match_group_input_custom.lua @@ -11,6 +11,7 @@ local CharacterStandardization = mw.loadData('Module:CharacterStandardization') local FnUtil = require('Module:FnUtil') local Logic = require('Module:Logic') local Lua = require('Module:Lua') +local Table = require('Module:Table') local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') local OpponentLibraries = require('Module:OpponentLibraries') @@ -18,12 +19,14 @@ local Opponent = OpponentLibraries.Opponent local CustomMatchGroupInput = {} local MatchFunctions = {} -local MapFunctions = {} MatchFunctions.OPPONENT_CONFIG = { resolveRedirect = true, pagifyTeamNames = true, pagifyPlayerNames = true, } +local MapFunctions = { + ADD_SUB_GROUP = true, +} ---@param match table ---@param options table? @@ -53,6 +56,14 @@ function MatchFunctions.getBestOf(bestofInput) return tonumber(bestofInput) end +---@param match table +---@param games table[] +---@param opponents table[] +---@return table +function MatchFunctions.getExtraData(match, games, opponents) + return Table.filterByKey(match, function(key) return key:match('subgroup%d+header') end) +end + ---@param match table ---@param map table ---@param opponents table[] diff --git a/components/match2/wikis/hearthstone/match_group_util_custom.lua b/components/match2/wikis/hearthstone/match_group_util_custom.lua new file mode 100644 index 0000000000..0619f69d1d --- /dev/null +++ b/components/match2/wikis/hearthstone/match_group_util_custom.lua @@ -0,0 +1,144 @@ +--- +-- @Liquipedia +-- wiki=hearthstone +-- page=Module:MatchGroup/Util/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 Table = require('Module:Table') + +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') +local MatchGroupUtil = Lua.import('Module:MatchGroup/Util') +-- can not use `Module:OpponentLibraries`/`Module:Opponent/Custom` to avoid loop +local Opponent = Lua.import('Module:Opponent') + +local SCORE_STATUS = 'S' + +local CustomMatchGroupUtil = Table.deepCopy(MatchGroupUtil) + +---@class HearthstoneMatchGroupUtilGameOpponent: GameOpponent +---@field placement number? + +---@class HearthstoneMatchGroupUtilSubmatch +---@field games MatchGroupUtilGame[] +---@field opponents HearthstoneMatchGroupUtilGameOpponent[] +---@field subgroup number +---@field winner number? +---@field header string? + +---@class HearthstoneMatchGroupUtilMatch: MatchGroupUtilMatch +---@field submatches StormgateMatchGroupUtilSubmatch[]? +---@field isTeamMatch boolean + +---@param record table +---@return HearthstoneMatchGroupUtilMatch +function CustomMatchGroupUtil.matchFromRecord(record) + local match = MatchGroupUtil.matchFromRecord(record) --[[@as HearthstoneMatchGroupUtilMatch]] + + -- Adjust game.opponents by looking up game.opponents.players in match.opponents + Array.forEach(match.games, function(game) + game.opponents = CustomMatchGroupUtil.computeGameOpponents(game, match.opponents) + end) + + match.isTeamMatch = Array.any(match.opponents, function(opponent) + return opponent.type == Opponent.team end + ) + + if not match.isTeamMatch then + return match + end + + -- Compute submatches + match.submatches = Array.map( + CustomMatchGroupUtil.groupBySubmatch(match.games), + function(games) return CustomMatchGroupUtil.constructSubmatch(games) end + ) + + local extradata = match.extradata + ---@cast extradata table + Array.forEach(match.submatches, function (submatch) + submatch.header = Table.extract(extradata, 'subgroup' .. submatch.subgroup .. 'header') + end) + + return match +end + +---@param game MatchGroupUtilGame +---@param matchOpponents standardOpponent[] +---@return table[] +function CustomMatchGroupUtil.computeGameOpponents(game, matchOpponents) + return Array.map(game.opponents, function (opponent, opponentIndex) + return Table.merge(opponent, { + players = Array.map(game.opponents[opponentIndex].players or {}, function (player, playerIndex) + if Logic.isEmpty(player) then return nil end + return Table.merge(matchOpponents[opponentIndex].players[playerIndex] or {}, player) + end) + }) + end) +end + +---Group games on the subgroup field to form submatches +---@param matchGames MatchGroupUtilGame[] +---@return MatchGroupUtilGame[][] +function CustomMatchGroupUtil.groupBySubmatch(matchGames) + -- Group games on adjacent subgroups + local previousSubgroup = nil + local currentGames = nil + local submatchGames = {} + Array.forEach(matchGames, function (game) + if previousSubgroup == nil or previousSubgroup ~= game.subgroup then + currentGames = {} + table.insert(submatchGames, currentGames) + previousSubgroup = game.subgroup + end + ---@cast currentGames -nil + table.insert(currentGames, game) + end) + return submatchGames +end + +---Constructs a submatch object whose properties are aggregated from that of its games. +---@param games MatchGroupUtilGame[] +---@return HearthstoneMatchGroupUtilSubmatch +function CustomMatchGroupUtil.constructSubmatch(games) + local firstGame = games[1] + local opponents = Table.deepCopy(firstGame.opponents) + local isSubmatch = string.find(firstGame.map or '', '^[sS]ubmatch %d+$') + if isSubmatch then + games = {firstGame} + end + + ---@param opponent table + ---@param opponentIndex number + local getOpponentScoreAndStatus = function(opponent, opponentIndex) + local statuses = Array.unique(Array.map(games, function(game) + return game.opponents[opponentIndex].status + end)) + opponent.status = #statuses == 1 and statuses[1] ~= SCORE_STATUS and statuses[1] or SCORE_STATUS + opponent.score = isSubmatch and firstGame.scores[opponentIndex] or Array.reduce(Array.map(games, function(game) + return (game.winner == opponentIndex and 1 or 0) + end), Operator.add) + end + + Array.forEach(opponents, getOpponentScoreAndStatus) + + local allPlayed = Array.all(games, function (game) return game.winner ~= nil end) + local winner = allPlayed and MatchGroupInputUtil.getWinner('', nil, opponents) or nil + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.placement = MatchGroupInputUtil.placementFromWinner('', winner, opponentIndex) + end) + + return { + games = games, + opponents = opponents, + subgroup = firstGame.subgroup, + winner = winner, + } +end + +return CustomMatchGroupUtil diff --git a/components/match2/wikis/hearthstone/match_summary.lua b/components/match2/wikis/hearthstone/match_summary.lua index d9363aa57c..bde923399c 100644 --- a/components/match2/wikis/hearthstone/match_summary.lua +++ b/components/match2/wikis/hearthstone/match_summary.lua @@ -8,10 +8,9 @@ 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 Table = require('Module:Table') local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper') local HtmlWidgets = Lua.import('Module:Widget/Html/All') @@ -21,8 +20,6 @@ local WidgetUtil = Lua.import('Module:Widget/Util') local OpponentLibraries = require('Module:OpponentLibraries') local Opponent = OpponentLibraries.Opponent -local OpponentDisplay = OpponentLibraries.OpponentDisplay -local PlayerDisplay = require('Module:Player/Display') local CustomMatchSummary = {} @@ -32,150 +29,126 @@ function CustomMatchSummary.getByMatchId(args) return MatchSummary.defaultGetByMatchId(CustomMatchSummary, args, {width = '350px', teamStyle = 'bracket'}) end ----@param match MatchGroupUtilMatch +---@param match HearthstoneMatchGroupUtilMatch ---@return MatchSummaryBody function CustomMatchSummary.createBody(match) local showCountdown = match.timestamp ~= DateExt.defaultTimestamp - CustomMatchSummary._fixGameOpponents(match.games, match.opponents) - - local isTeamMatch = Array.any(match.opponents, function(opponent) - return opponent.type == Opponent.team - end) + local submatches + if match.isTeamMatch then + submatches = match.submatches or {} + end return MatchSummaryWidgets.Body{children = WidgetUtil.collect( showCountdown and MatchSummaryWidgets.Row{children = DisplayHelper.MatchCountdownBlock(match)} or nil, - Array.map(match.games, function (game, gameIndex) - if isTeamMatch and String.startsWith(game.map or '', 'Submatch') then - return CustomMatchSummary._createSubmatch(game) - else - return CustomMatchSummary._createGame(isTeamMatch, game, gameIndex) - end - end) + submatches and Array.map(submatches, CustomMatchSummary.TeamSubmatch) + or Array.map(match.games, FnUtil.curry(CustomMatchSummary.Game, {isPartOfSubMatch = false})) )} end ----@param games MatchGroupUtilGame ----@param opponents standardOpponent[] -function CustomMatchSummary._fixGameOpponents(games, opponents) - Array.forEach(games, function (game) - game.opponents = Array.map(game.opponents, function (opponent, opponentIndex) - return Table.merge(opponent, { - players = Array.map(game.opponents[opponentIndex].players or {},function (player, playerIndex) - if Logic.isEmpty(player) then return nil end - return Table.merge(opponents[opponentIndex].players[playerIndex] or {}, player) - end) - }) - end) +---@param submatch HearthstoneMatchGroupUtilSubmatch +---@return MatchSummaryRow +function CustomMatchSummary.TeamSubmatch(submatch) + local hasDetails = CustomMatchSummary._submatchHasDetails(submatch) + return MatchSummaryWidgets.Row{ + classes = {'brkts-popup-body-game'}, + children = WidgetUtil.collect( + submatch.header and { + HtmlWidgets.Div{css = {margin = 'auto', ['font-weight'] = 'bold'}, children = {submatch.header}}, + MatchSummaryWidgets.Break{}, + } or nil, + CustomMatchSummary.TeamSubMatchOpponnetRow(submatch), + hasDetails and Array.map(submatch.games, function(game, gameIndex) + return CustomMatchSummary.Game( + {isPartOfSubMatch = true}, + game, + gameIndex + ) + end) or nil + ) + } +end + +---@param submatch HearthstoneMatchGroupUtilSubmatch +---@return boolean +function CustomMatchSummary._submatchHasDetails(submatch) + return #submatch.games > 0 and Array.any(submatch.games, function(game) + return not string.find(game.map or '', '^[sS]ubmatch %d+$') end) end ----@param game MatchGroupUtilGame ----@return Widget -function CustomMatchSummary._createSubmatch(game) - local opponents = game.opponents or {{}, {}} - local createOpponent = function(opponentIndex) - local players = (opponents[opponentIndex] or {}).players or {} +---@param submatch HearthstoneMatchGroupUtilSubmatch +---@return Html +function CustomMatchSummary.TeamSubMatchOpponnetRow(submatch) + local opponents = submatch.opponents or {{}, {}} + Array.forEach(opponents, function (opponent, opponentIndex) + local players = opponent.players or {} if Logic.isEmpty(players) then players = Opponent.tbd(Opponent.solo).players end - return OpponentDisplay.BlockOpponent{ - flip = opponentIndex == 1, - opponent = {players = players, type = Opponent.partyTypes[math.max(#players, 1)]}, - showLink = true, - overflow = 'ellipsis', - } - end - - ---@param opponentIndex any - ---@return Html - local createScore = function(opponentIndex) - local isWinner = opponentIndex == game.winner or game.resultType == 'draw' - if game.resultType == 'default' then - return OpponentDisplay.BlockScore{ - isWinner = isWinner, - scoreText = isWinner and 'W' or string.upper(game.walkover), - } - end - - local score = game.resultType ~= 'np' and (game.scores or {})[opponentIndex] or nil - return OpponentDisplay.BlockScore{ - isWinner = isWinner, - scoreText = score, - } - end + ---@cast players -nil + opponent.type = Opponent.partyTypes[math.max(#players, 1)] + opponent.players = players + end) - return HtmlWidgets.Div{ - classes = {'brkts-popup-header-dev'}, - css = {['justify-content'] = 'center', margin = 'auto'}, - children = WidgetUtil.collect( - HtmlWidgets.Div{ - classes = {'brkts-popup-header-opponent', 'brkts-popup-header-opponent-left'}, - children = { - createOpponent(1), - createScore(1):addClass('brkts-popup-header-opponent-score-left'), - }, - }, - HtmlWidgets.Div{ - classes = {'brkts-popup-header-opponent', 'brkts-popup-header-opponent-right'}, - children = { - createScore(2):addClass('brkts-popup-header-opponent-score-right'), - createOpponent(2), - }, - } - ) + return HtmlWidgets.Div { + css = {margin = 'auto'}, + children = MatchSummary.createDefaultHeader({opponents = opponents}):create() } end ----@param isTeamMatch boolean +---@param options {isPartOfSubMatch: boolean?} ---@param game MatchGroupUtilGame ---@param gameIndex number ---@return Widget -function CustomMatchSummary._createGame(isTeamMatch, game, gameIndex) - return MatchSummaryWidgets.Row{ +function CustomMatchSummary.Game(options, game, gameIndex) + local rowWidget = options.isPartOfSubMatch and HtmlWidgets.Div or MatchSummaryWidgets.Row + + ---@param opponentIndex any + ---@return table[] + local function createOpponentDisplay(opponentIndex) + return Array.extend({ + CustomMatchSummary.DisplayClass(game.opponents[opponentIndex], opponentIndex == 1), + MatchSummaryWidgets.GameWinLossIndicator{winner = game.winner, opponentIndex = opponentIndex}, + }) + end + + return rowWidget{ classes = {'brkts-popup-body-game'}, - css = {padding = '4px', ['min-height'] = '24px'}, + css = {width = options.isPartOfSubMatch and '100%' or nil, ['font-size'] = '0.75rem'}, children = WidgetUtil.collect( - CustomMatchSummary._displayOpponents(isTeamMatch, game.opponents[1].players, true), - MatchSummaryWidgets.GameWinLossIndicator{winner = game.winner, opponentIndex = 1}, - MatchSummaryWidgets.GameCenter{css = {['font-size'] = '80%'}, children = 'Game ' .. gameIndex}, - MatchSummaryWidgets.GameWinLossIndicator{winner = game.winner, opponentIndex = 2}, - CustomMatchSummary._displayOpponents(isTeamMatch, game.opponents[2].players) + MatchSummaryWidgets.GameTeamWrapper{children = createOpponentDisplay(1)}, + MatchSummaryWidgets.GameCenter{css = {flex = '0 0 16%'}, children = 'Game ' .. gameIndex}, + MatchSummaryWidgets.GameTeamWrapper{children = createOpponentDisplay(2), flipped = true} ) } end ----@param isTeamMatch boolean ----@param players table[] +---@param opponent table ---@param flip boolean? ---@return Html? -function CustomMatchSummary._displayOpponents(isTeamMatch, players, flip) - local playerDisplays = Array.map(players, function (player) - local char = Logic.isNotEmpty(player.class) and HtmlWidgets.Div{ - classes = {'brkts-champion-icon'}, - children = MatchSummaryWidgets.Character{ - character = player.class, - showName = not isTeamMatch, - flipped = flip, - } - } or nil - return HtmlWidgets.Div{ - css = { - display = 'flex', - ['flex-direction'] = flip and 'row-reverse' or 'row', - gap = '2px', - width = '100%' - }, - children = { - char, - isTeamMatch and PlayerDisplay.BlockPlayer{player = player, flip = flip} or nil, - }, - } +function CustomMatchSummary.DisplayClass(opponent, flip) + local player = Array.find(opponent.players or {}, function (player) + return Logic.isNotEmpty(player.class) end) - return MatchSummaryWidgets.GameTeamWrapper{ - flipped = flip, - children = playerDisplays + if Logic.isEmpty(player) then + return nil + end + ---@cast player -nil + + return HtmlWidgets.Div{ + classes = {'brkts-champion-icon'}, + css = { + display = 'flex', + flex = '1', + ['justify-content'] = flip and 'flex-end' or 'flex-start' + }, + children = MatchSummaryWidgets.Character{ + character = player.class, + showName = true, + flipped = flip, + } } end