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 00000000000..580ce5afbe6 --- /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 00000000000..4d13365b951 --- /dev/null +++ b/components/match2/wikis/hearthstone/match_group_input_custom.lua @@ -0,0 +1,261 @@ +--- +-- @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 = {} + +---@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) + Array.forEach(opponents, function(opponent, opponentIndex) + opponent.placement = MatchGroupInputUtil.placementFromWinner(match.resulttype, match.winner, opponentIndex) + end) + end + + match.mode = Variables.varDefault('tournament_mode', 'singles') + Table.mergeInto(match, MatchGroupInputUtil.getTournamentContext(match)) + + match.stream = Streams.processStreams(match) + + match.games = games + match.opponents = opponents + + return 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']), + } + end, + function(playerIndex, playerIdData, playerInputData) + local prefix = 'o' .. opponentIndex .. 'p' .. playerIndex + return { + player = playerIdData.name or playerInputData.link, + character = MapFunctions.readCharacter(Logic.nilIfEmpty(mapInput[prefix .. 'char']).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_legacy.lua b/components/match2/wikis/hearthstone/match_legacy.lua new file mode 100644 index 00000000000..63021e1f3fc --- /dev/null +++ b/components/match2/wikis/hearthstone/match_legacy.lua @@ -0,0 +1,92 @@ +--- +-- @Liquipedia +-- wiki=hearthstone +-- page=Module:Match/Legacy +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local MatchLegacy = {} + +local Array = require('Module:Array') +local Json = require('Module:Json') +local Logic = require('Module:Logic') +local String = require('Module:StringUtils') +local Table = require('Module:Table') +local Opponent = require('Module:Opponent') + +function MatchLegacy.storeMatch(match2) + local match = MatchLegacy._convertParameters(match2) + + match.games = MatchLegacy.storeGames(match, match2) + + return mw.ext.LiquipediaDB.lpdb_match( + 'legacymatch_' .. match2.match2id, + match + ) +end + +function MatchLegacy._convertParameters(match2) + ---@type table + local match = Table.filterByKey(Table.deepCopy(match2), function(key) return not String.startsWith(key, 'match2') end) + match.links = nil + + if Logic.isNotEmpty(match.walkover) then + match.resulttype = match.walkover + match.walkover = match.winner + end + + match.staticid = match2.match2id + + + -- Handle Opponents + local handleOpponent = function (index) + local prefix = 'opponent'..index + local opponent = match2.match2opponents[index] or {} + match[prefix .. 'score'] = tonumber(opponent.score) or 0 + if opponent.type == Opponent.team then + match[prefix] = opponent.name + local players = {} + Array.forEach(opponent.match2players or {}, function(player, playerIndex) + players['p' .. playerIndex] = player.name or '' + players['p' .. playerIndex .. 'flag'] = player.flag or '' + players['p' .. playerIndex .. 'dn'] = player.displayname or '' + end) + match[prefix .. 'players'] = players + elseif opponent.type == Opponent.solo then + local player = (opponent.match2players or {})[1] or {} + match[prefix] = player.name + match[prefix..'flag'] = player.flag + elseif opponent.type == Opponent.literal then + match[prefix] = 'TBD' + end + end + + handleOpponent(1) + handleOpponent(2) + + return Json.stringifySubTables(match) +end + +function MatchLegacy.storeGames(match, match2) + local games = Array.map(match2.match2games or {}, function(game2, gameIndex) + local game = Table.deepCopy(game2) + + -- Other stuff + game.opponent1 = match.opponent1 + game.opponent2 = match.opponent2 + game.opponent1flag = match.opponent1flag + game.opponent2flag = match.opponent2flag + game.date = match.date + local winner = tonumber(game.winner) + game.opponent1score = winner == 1 and 1 or 0 + game.opponent2score = winner == 2 and 1 or 0 + return mw.ext.LiquipediaDB.lpdb_game( + 'legacygame_' .. match2.match2id .. '_' .. gameIndex, + Json.stringifySubTables(game) + ) + end) + return table.concat(games) +end + +return MatchLegacy diff --git a/components/match2/wikis/hearthstone/match_summary.lua b/components/match2/wikis/hearthstone/match_summary.lua new file mode 100644 index 00000000000..f493bb168c7 --- /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 diff --git a/standard/info/wikis/hearthstone/info.lua b/standard/info/wikis/hearthstone/info.lua index a63f8538dec..7d5a1236639 100644 --- a/standard/info/wikis/hearthstone/info.lua +++ b/standard/info/wikis/hearthstone/info.lua @@ -33,7 +33,7 @@ return { allowManual = true, }, match2 = { - status = 0, + status = 1, }, }, }