diff --git a/components/match2/wikis/hearthstone/get_match_group_copy_paste_wiki.lua b/components/match2/wikis/hearthstone/get_match_group_copy_paste_wiki.lua new file mode 100644 index 0000000000..580ce5afbe --- /dev/null +++ b/components/match2/wikis/hearthstone/get_match_group_copy_paste_wiki.lua @@ -0,0 +1,73 @@ +--- +-- @Liquipedia +-- wiki=hearthstone +-- 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') +local OpponentLibrary = Lua.import('Module:OpponentLibraries') +local Opponent = OpponentLibrary.Opponent + +---WikiSpecific Code for MatchList and Bracket Code Generators +---@class HearthstoneMatchCopyPaste: 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), true) + + local lines = Array.extend( + '{{Match|bestof=' .. bestof, + INDENT .. '|date=', + INDENT .. '|twitch=|vod=', + Array.map(Array.range(1, opponents), function(opponentIndex) + return INDENT .. '|opponent' .. opponentIndex .. '=' .. WikiCopyPaste.getOpponent(mode, showScore) + end), + Array.map(Array.range(1, bestof), function(mapIndex) + return INDENT .. '|map' .. mapIndex .. WikiCopyPaste._getMap(mode, opponents) + end), + '}}' + ) + + return table.concat(lines, '\n') +end + +--subfunction used to generate code for the Map template, depending on the type of opponent +---@param mode string +---@param opponents integer +---@return string +function WikiCopyPaste._getMap(mode, opponents) + if mode == Opponent.team then + return '={{Map|o1p1=|o2p1=|o1p1char=|o2p1char=|winner=}}' + elseif mode == Opponent.literal then + return '={{Map|winner=}}' + end + + local parts = Array.extend({}, + Array.map(Array.range(1, opponents), function(opponentIndex) + return table.concat(Array.map(Array.range(1, Opponent.partySize(mode) --[[@as integer]]), function(playerIndex) + return '|o' .. opponentIndex .. 'p' .. playerIndex .. '=' + end)) + end), + '}}' + ) + + return table.concat(parts) +end + +return WikiCopyPaste diff --git a/components/match2/wikis/hearthstone/match_group_input_custom.lua b/components/match2/wikis/hearthstone/match_group_input_custom.lua new file mode 100644 index 0000000000..4dcd0126b3 --- /dev/null +++ b/components/match2/wikis/hearthstone/match_group_input_custom.lua @@ -0,0 +1,267 @@ +--- +-- @Liquipedia +-- wiki=hearthstone +-- page=Module:MatchGroup/Input/Custom +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local CharacterStandardization = mw.loadData('Module:CharacterStandardization') +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 MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') +local OpponentLibraries = require('Module:OpponentLibraries') +local Opponent = OpponentLibraries.Opponent +local Streams = Lua.import('Module:Links/Stream') + +local OPPONENT_CONFIG = { + resolveRedirect = true, + pagifyTeamNames = true, + pagifyPlayerNames = true, +} +local TBD = 'TBD' + +local CustomMatchGroupInput = {} +local MatchFunctions = {} +local MapFunctions = {} + +CustomMatchGroupInput.processMap = FnUtil.identity + +---@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, OPPONENT_CONFIG) + 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 + + MatchFunctions.getTournamentVars(match) + + match.stream = Streams.processStreams(match) + + match.games = games + match.opponents = opponents + + return match +end + +---@param match table +---@return table +function MatchFunctions.getTournamentVars(match) + match.mode = Variables.varDefault('tournament_mode', 'singles') + return MatchGroupInputUtil.getCommonTournamentVars(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) + return tonumber(bestofInput) +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?]] + + if String.isNotEmpty(map.map) and string.upper(map.map) ~= TBD then + map.map = mw.ext.TeamLiquidIntegration.resolve_redirect(map.map) + end + + 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) + + map.participants = MapFunctions.getParticipants(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) + local extradata = {comment = mapInput.comment} + + Array.forEach(opponents, function(opponent, opponentIndex) + local prefix = 'o' .. opponentIndex .. 'p' + local chars = Array.mapIndexes(function(charIndex) + return Logic.nilIfEmpty(mapInput[prefix .. charIndex .. 'char']) or Logic.nilIfEmpty(mapInput[prefix .. charIndex]) + end) + Array.forEach(chars, function(char, charIndex) + extradata[prefix .. charIndex] = MapFunctions.readCharacter(char) + end) + end) + + return extradata +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 + +---@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['o' .. opponentIndex .. 'p' .. playerIndex]) + end) + + local participants, unattachedParticipants = MatchGroupInputUtil.parseParticipants( + opponent.match2players, + players, + function(playerIndex) + local prefix = 'o' .. opponentIndex .. 'p' .. playerIndex + return { + name = mapInput[prefix], + link = Logic.nilIfEmpty(mapInput[prefix .. 'link']), + character = Logic.nilIfEmpty(mapInput[prefix .. 'char']), + } + end, + function(playerIndex, playerIdData, playerInputData) + return { + player = playerIdData.name or playerInputData.link, + character = MapFunctions.readCharacter(playerInputData.character), + } + 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 = 'o' .. opponentIndex .. 'p' + + local participants = {} + + Array.forEach(players, function(player, playerIndex) + participants[opponentIndex .. '_' .. playerIndex] = { + character = MapFunctions.readCharacter(mapInput[prefix .. playerIndex]), + player = player.name, + } + end) + + return participants +end + +---@param input string? +---@return string? +function MapFunctions.readCharacter(input) + local getCharacterName = FnUtil.curry(MatchGroupInputUtil.getCharacterName, CharacterStandardization) + + return getCharacterName(input) +end + +return CustomMatchGroupInput diff --git a/components/match2/wikis/hearthstone/match_summary.lua b/components/match2/wikis/hearthstone/match_summary.lua new file mode 100644 index 0000000000..f493bb168c --- /dev/null +++ b/components/match2/wikis/hearthstone/match_summary.lua @@ -0,0 +1,163 @@ +--- +-- @Liquipedia +-- wiki=hearthstone +-- 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 CharacterIcon = require('Module:CharacterIcon') +local Icon = require('Module:Icon') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') + +local DisplayHelper = Lua.import('Module:MatchGroup/Display/Helper') +local MatchSummary = Lua.import('Module:MatchSummary/Base') + +local OpponentLibraries = require('Module:OpponentLibraries') +local Opponent = OpponentLibraries.Opponent + +local ICONS = { + winner = Icon.makeIcon{iconName = 'winner', color = 'forest-green-text', size = 'initial'}, + draw = Icon.makeIcon{iconName = 'draw', color = 'bright-sun-text', size = 'initial'}, + loss = Icon.makeIcon{iconName = 'loss', color = 'cinnabar-text', size = 'initial'}, + empty = '[[File:NoCheck.png|link=|16px]]', +} + +local CustomMatchSummary = {} + +---@param args table +---@return Html +function CustomMatchSummary.getByMatchId(args) + return MatchSummary.defaultGetByMatchId(CustomMatchSummary, args, { + width = CustomMatchSummary._determineWidth, + teamStyle = 'bracket', + }) +end + +---@param match MatchGroupUtilMatch +---@return string +function CustomMatchSummary._determineWidth(match) + return '350px' +end + +---@param match MatchGroupUtilMatch +---@return MatchSummaryBody +function CustomMatchSummary.createBody(match) + local body = MatchSummary.Body() + + if match.dateIsExact or (match.timestamp ~= DateExt.defaultTimestamp) then + body:addRow(MatchSummary.Row():addElement( + DisplayHelper.MatchCountdownBlock(match) + )) + end + + if not CustomMatchSummary._isSolo(match) then + return body + end + + Array.forEach(match.games, function(game) + if not game.map and not game.winner then return end + local row = MatchSummary.Row() + :addClass('brkts-popup-body-game') + :css('font-size', '0.75rem') + :css('padding', '4px') + :css('min-height', '24px') + + CustomMatchSummary._createGame(row, game, { + opponents = match.opponents, + game = match.game, + }) + body:addRow(row) + end) + + return body +end + +---@param match MatchGroupUtilMatch +---@param footer MatchSummaryFooter +---@return MatchSummaryFooter +function CustomMatchSummary.addToFooter(match, footer) + footer = MatchSummary.addVodsToFooter(match, footer) + + return footer +end + +---@param match MatchGroupUtilMatch +---@return boolean +function CustomMatchSummary._isSolo(match) + if type(match.opponents[1]) ~= 'table' or type(match.opponents[2]) ~= 'table' then + return false + end + return match.opponents[1].type == Opponent.solo and match.opponents[2].type == Opponent.solo +end + +---@param game MatchGroupUtilGame +---@param paricipantId string +---@return {displayName: string?, pageName: string?, flag: string?, character: string?} +function CustomMatchSummary._getPlayerData(game, paricipantId) + if not game or not game.participants then + return {} + end + return game.participants[paricipantId] or {} +end + +---@param row MatchSummaryRow +---@param game MatchGroupUtilGame +---@param props {game: string?, opponents: standardOpponent[]} +function CustomMatchSummary._createGame(row, game, props) + game.extradata = game.extradata or {} + + local char1 = + CustomMatchSummary._createCharacterDisplay(CustomMatchSummary._getPlayerData(game, '1_1').character, false) + local char2 = + CustomMatchSummary._createCharacterDisplay(CustomMatchSummary._getPlayerData(game, '2_1').character, true) + + row:addElement(char1:css('flex', '1 1 35%'):css('text-align', 'right')) + row:addElement(CustomMatchSummary._createCheckMark(game.winner, 1)) + row:addElement(CustomMatchSummary._createCheckMark(game.winner, 2)) + row:addElement(char2:css('flex', '1 1 35%')) +end + +---@param character string? +---@param reverse boolean? +---@return Html +function CustomMatchSummary._createCharacterDisplay(character, reverse) + local characterDisplay = mw.html.create('span'):addClass('draft faction') + + if not character then + return characterDisplay + end + + local charIcon = CharacterIcon.Icon{ + character = character, + size = '64px', + } + if reverse then + characterDisplay:wikitext(charIcon):wikitext(' '):wikitext(character) + else + characterDisplay:wikitext(character):wikitext(' '):wikitext(charIcon) + end + return characterDisplay +end + +---@param winner integer|string +---@param opponentIndex integer +---@return Html +function CustomMatchSummary._createCheckMark(winner, opponentIndex) + return mw.html.create('div') + :addClass('brkts-popup-spaced') + :css('line-height', '17px') + :css('margin-left', '2%') + :css('margin-right', '2%') + :wikitext( + winner == opponentIndex and ICONS.winner + or winner == 0 and ICONS.draw + or Logic.isNotEmpty(winner) and ICONS.loss + or ICONS.empty + ) +end + +return CustomMatchSummary