diff --git a/components/match2/commons/match.lua b/components/match2/commons/match.lua index 93f72511c4..dc2a4a6222 100644 --- a/components/match2/commons/match.lua +++ b/components/match2/commons/match.lua @@ -313,7 +313,7 @@ function Match._prepareRecordsForStore(records) for opponentIndex, opponentRecord in ipairs(records.opponentRecords) do Match.clampFields(opponentRecord, Match.opponentFields) for _, playerRecord in ipairs(records.playerRecords[opponentIndex]) do - Match.clampFields(playerRecord, Match.playerFields) + Match._preparePlayerRecordForStore(playerRecord) end end for _, gameRecord in ipairs(records.gameRecords) do @@ -387,6 +387,13 @@ function Match._prepareGameRecordForStore(matchRecord, gameRecord) Match.clampFields(gameRecord, Match.gameFields) end +---@param playerRecord table +function Match._preparePlayerRecordForStore(playerRecord) + playerRecord.extradata = playerRecord.extradata or {} + playerRecord.extradata.playerteam = playerRecord.team + Match.clampFields(playerRecord, Match.playerFields) +end + ---Adds fields needed for backwards compatibility with API v3. ---walkover and resulttype are added to record. ---@param record table #game or match record diff --git a/components/match2/commons/match_group_input_util.lua b/components/match2/commons/match_group_input_util.lua index 028633a603..2ba81c66b4 100644 --- a/components/match2/commons/match_group_input_util.lua +++ b/components/match2/commons/match_group_input_util.lua @@ -265,6 +265,7 @@ function MatchGroupInputUtil.mergeRecordWithOpponent(record, opponent, substitut displayname = player.displayName, flag = player.flag, name = player.pageName, + team = player.team, extradata = player.faction and {faction = player.faction} } end) diff --git a/components/match2/commons/match_group_util.lua b/components/match2/commons/match_group_util.lua index ff8dde217b..c487cf6890 100644 --- a/components/match2/commons/match_group_util.lua +++ b/components/match2/commons/match_group_util.lua @@ -676,6 +676,7 @@ function MatchGroupUtil.playerFromRecord(record) extradata = extradata, flag = nilIfEmpty(record.flag), pageName = record.name, + team = Table.extract(extradata, 'playerteam'), } end diff --git a/components/match2/commons/match_summary_ffa.lua b/components/match2/commons/match_summary_ffa.lua index caabcbf154..f2ad64c4e3 100644 --- a/components/match2/commons/match_summary_ffa.lua +++ b/components/match2/commons/match_summary_ffa.lua @@ -107,6 +107,7 @@ local MATCH_OVERVIEW_COLUMNS = { showLink = true, overflow = 'ellipsis', teamStyle = 'hybrid', + showPlayerTeam = true, } end, }, @@ -270,6 +271,7 @@ local GAME_STANDINGS_COLUMNS = { showLink = true, overflow = 'ellipsis', teamStyle = 'hybrid', + showPlayerTeam = true, } end, }, diff --git a/components/match2/wikis/fortnite/game_summary.lua b/components/match2/wikis/fortnite/game_summary.lua new file mode 100644 index 0000000000..f029a4e247 --- /dev/null +++ b/components/match2/wikis/fortnite/game_summary.lua @@ -0,0 +1,89 @@ +--- +-- @Liquipedia +-- wiki=fortnite +-- page=Module:GameSummary +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local CustomGameSummary = {} + +local Array = require('Module:Array') +local FnUtil = require('Module:FnUtil') +local Lua = require('Module:Lua') +local Page = require('Module:Page') +local Table = require('Module:Table') + +local MatchGroupUtil = Lua.import('Module:MatchGroup/Util') + +local SummaryHelper = Lua.import('Module:MatchSummary/Ffa') +local MatchSummaryWidgets = Lua.import('Module:Widget/Match/Summary/Ffa/All') +local HtmlWidgets = Lua.import('Module:Widget/Html/All') +local IconWidget = Lua.import('Module:Widget/Image/Icon/Fontawesome') + +---@class FortniteMatchGroupUtilGame: MatchGroupUtilGame +---@field stream table + +---@param props {bracketId: string, matchId: string, gameIdx: integer} +---@return Html +function CustomGameSummary.getGameByMatchId(props) + ---@class FortniteMatchGroupUtilMatch + local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + + local game = match.games[props.gameIdx] + assert(game, 'Error Game ID ' .. tostring(props.gameIdx) .. ' not found') + + game.stream = match.stream + + CustomGameSummary._opponents(game, match.opponents) + local scoringData = SummaryHelper.createScoringData(match) + + return MatchSummaryWidgets.Tab{ + matchId = match.matchId, + idx = props.gameIdx, + children = { + CustomGameSummary._createGameDetails(game), + MatchSummaryWidgets.PointsDistribution{killScore = scoringData.kill, placementScore = scoringData.placement}, + SummaryHelper.standardGame(game) + } + } +end + +---@param game table +---@return Widget +function CustomGameSummary._createGameDetails(game) + return MatchSummaryWidgets.ContentItemContainer{contentClass = 'panel-content__game-schedule', + items = { + { + icon = MatchSummaryWidgets.CountdownIcon{game = game}, + content = MatchSummaryWidgets.GameCountdown{game = game}, + }, + game.map and { + icon = IconWidget{iconName = 'map'}, + content = HtmlWidgets.Span{children = Page.makeInternalLink(game.map)}, + } or nil, + } + } +end + +---@param game table +---@param matchOpponents table[] +function CustomGameSummary._opponents(game, matchOpponents) + -- Add match opponent data to game opponent + game.opponents = Array.map(game.opponents, + function(gameOpponent, opponentIdx) + local matchOpponent = matchOpponents[opponentIdx] + local newGameOpponent = Table.merge(matchOpponent, gameOpponent) + -- These values are only allowed to come from Game and not Match + newGameOpponent.placement = gameOpponent.placement + newGameOpponent.score = gameOpponent.score + newGameOpponent.status = gameOpponent.status + return newGameOpponent + end + ) + + -- Sort game level based on placement + Array.sortInPlaceBy(game.opponents, FnUtil.identity, SummaryHelper.placementSortFunction) +end + +return CustomGameSummary diff --git a/components/match2/wikis/fortnite/get_match_group_copy_paste_wiki.lua b/components/match2/wikis/fortnite/get_match_group_copy_paste_wiki.lua new file mode 100644 index 0000000000..e2c6b215b0 --- /dev/null +++ b/components/match2/wikis/fortnite/get_match_group_copy_paste_wiki.lua @@ -0,0 +1,81 @@ +--- +-- @Liquipedia +-- wiki=fortnite +-- 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 Lua = require('Module:Lua') + +local OpponentLibraries = require('Module:OpponentLibraries') +local Opponent = OpponentLibraries.Opponent + +local BaseCopyPaste = Lua.import('Module:GetMatchGroupCopyPaste/wiki/Base') + +---WikiSpecific Code for MatchList and Bracket Code Generators +---@class FortniteMatchCopyPaste: 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 defaultScoring = '|p1= |p2= |p3= |p4= |p5= |p6= |p7= |p8= |p9= |p10= |p11= |p12= |p13= |p14= |p15= |p_kill=4' + if mode == Opponent.duo then + defaultScoring = '|p1=65|p2=56|p3=52|p4=48|p5=44|p6=40|p7=38|p8=36|p9=34|p10=32|p11=30|p12=28|p13=26|p14=24' .. + '|p15=22|p16=20|p17=18|p18=16|p19=14|p20=12|p21=10|p22=8|p23=6|p24=4|p25=2|p_kill=4' + elseif mode == Opponent.trio or mode == Opponent.team then + defaultScoring = '|p1=65|p2=54|p3=48|p4=44|p5=40|p6=36|p7=33|p8=30|p9=27|p10=24|p11=21|p12=18|p13=15|p14=12'.. + '|p15=9|p16=6|p17=3|p_kill=4' + end + + local lines = Array.extend( + '{{Match|finished=', + INDENT .. defaultScoring, + {INDENT .. '|twitch=|youtube='}, + Array.map(Array.range(1, bestof), function(mapIndex) + return INDENT .. '|map' .. mapIndex .. '={{Map|date=|finished=|map=|vod=}}' + end), + Array.map(Array.range(1, opponents), function(opponentIndex) + return INDENT .. '|opponent' .. opponentIndex .. '=' .. WikiCopyPaste._getOpponent(mode, bestof) + end), + '}}' + ) + + return table.concat(lines, '\n') +end + +--subfunction used to generate the code for the Opponent template, depending on the type of opponent +---@param mode string +---@param mapCount integer +---@return string +function WikiCopyPaste._getOpponent(mode, mapCount) + local mapScores = table.concat(Array.map(Array.range(1, mapCount), function(idx) + return '|m' .. idx .. '={{MS||}}' + end)) + + if mode == Opponent.solo then + return '{{SoloOpponent||flag=' .. mapScores .. '}}' + elseif mode == Opponent.duo then + return '{{2Opponent|' .. mapScores .. '}}' + elseif mode == Opponent.trio then + return '{{3Opponent|' .. mapScores .. '}}' + elseif mode == Opponent.team then + return '{{TeamOpponent|' .. mapScores .. '}}' + elseif mode == Opponent.literal then + return '{{Literal|' .. mapScores .. '}}' + end + + return '' +end + +return WikiCopyPaste diff --git a/components/match2/wikis/fortnite/match_group_input_custom.lua b/components/match2/wikis/fortnite/match_group_input_custom.lua new file mode 100644 index 0000000000..9f4999761b --- /dev/null +++ b/components/match2/wikis/fortnite/match_group_input_custom.lua @@ -0,0 +1,180 @@ +--- +-- @Liquipedia +-- wiki=fortnite +-- page=Module:MatchGroup/Input/Custom +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local Array = require('Module:Array') +local Json = require('Module:Json') +local Logic = require('Module:Logic') +local Lua = require('Module:Lua') +local Operator = require('Module:Operator') +local Table = require('Module:Table') + +local MatchGroupInputUtil = Lua.import('Module:MatchGroup/Input/Util') + +local MapFunctions = {} +local MatchFunctions = { + OPPONENT_CONFIG = { + resolveRedirect = true, + applyUnderScores = true, + maxNumPlayers = 3, + }, + DEFAULT_MODE = 'team' +} + +local CustomMatchGroupInput = {} + +---@param match table +---@param options table? +---@return table +function CustomMatchGroupInput.processMatch(match, options) + return MatchGroupInputUtil.standardProcessFfaMatch(match, MatchFunctions) +end + +-- +-- match related functions +-- +---@param match table +---@param opponents table[] +---@param scoreSettings table +---@return table[] +function MatchFunctions.extractMaps(match, opponents, scoreSettings) + local maps = {} + for key, map, mapIndex in Table.iter.pairsByPrefix(match, 'map', {requireIndex = true}) do + local finishedInput = map.finished --[[@as string?]] + local winnerInput = map.winner --[[@as string?]] + + Table.mergeInto(map, MatchGroupInputUtil.readDate(map.date)) + map.finished = MatchGroupInputUtil.mapIsFinished(map) + + map.opponents = Array.map(opponents, function(matchOpponent) + local opponentMapInput = Json.parseIfString(matchOpponent['m' .. mapIndex]) + return MapFunctions.makeMapOpponentDetails(opponentMapInput, scoreSettings) + end) + + map.scores = Array.map(map.opponents, Operator.property('score')) + if map.finished then + map.status = MatchGroupInputUtil.getMatchStatus(winnerInput, finishedInput) + map.winner = MatchGroupInputUtil.getWinner(map.status, winnerInput, map.opponents) + end + + map.extradata = MapFunctions.getExtraData(map) + + table.insert(maps, map) + match[key] = nil + end + + return maps +end + +---@param opponents table[] +---@param maps table[] +---@return fun(opponentIndex: integer): integer? +function MatchFunctions.calculateMatchScore(opponents, maps) + return function(opponentIndex) + return Array.reduce(Array.map(maps, function(map) + return map.opponents[opponentIndex].score or 0 + end), Operator.add, 0) + (opponents[opponentIndex].extradata.startingpoints or 0) + end +end + + +---@param match table +---@return {score: table, status: table} +function MatchFunctions.parseSettings(match) + -- Score Settings + local scoreSettings = { + kill = tonumber(match.p_kill) or 1, + placement = Array.mapIndexes(function(idx) + return match['opponent' .. idx] and (tonumber(match['p' .. idx]) or 0) or nil + end) + } + + -- Up/Down colors + local statusSettings = Array.flatMap(Array.parseCommaSeparatedString(match.bg, ','), function (status) + local placements, color = unpack(Array.parseCommaSeparatedString(status, '=')) + local pStart, pEnd = unpack(Array.parseCommaSeparatedString(placements, '-')) + local pStartNumber = tonumber(pStart) --[[@as integer]] + local pEndNumber = tonumber(pEnd) or pStartNumber + return Array.map(Array.range(pStartNumber, pEndNumber), function() + return color + end) + end) + + return { + score = scoreSettings, + status = statusSettings, + settings = { + showGameDetails = Logic.nilOr(Logic.readBoolOrNil(match.showgamedetails), true), + } + } +end + +---@param match table +---@param games table[] +---@param opponents table[] +---@param settings table +---@return table +function MatchFunctions.getExtraData(match, games, opponents, settings) + return { + scoring = settings.score, + status = settings.status, + settings = settings.settings, + } +end + +-- +-- map related functions +-- + +---@param map table +---@return table +function MapFunctions.getExtraData(map) + return { + dateexact = map.dateexact, + comment = map.comment, + } +end + +---@param scoreDataInput table? +---@param scoreSettings table +---@return table +function MapFunctions.makeMapOpponentDetails(scoreDataInput, scoreSettings) + if not scoreDataInput then + return {} + end + + local scoreBreakdown = {} + + local placement, kills = tonumber(scoreDataInput[1]), tonumber(scoreDataInput[2]) + local points = tonumber(scoreDataInput.p) + if placement or kills then + if placement then + scoreBreakdown.placePoints = scoreSettings.placement[placement] or 0 + end + if kills then + scoreBreakdown.killPoints = kills * scoreSettings.kill + scoreBreakdown.kills = kills + end + scoreBreakdown.totalPoints = (scoreBreakdown.placePoints or 0) + (scoreBreakdown.killPoints or 0) + end + + local opponent = { + status = MatchGroupInputUtil.STATUS.SCORE, + scoreBreakdown = scoreBreakdown, + placement = placement, + score = points or scoreBreakdown.totalPoints, + } + + if scoreDataInput[1] == '-' then + opponent.status = MatchGroupInputUtil.STATUS.FORFEIT + opponent.score = 0 + end + + return opponent +end + +return CustomMatchGroupInput diff --git a/components/match2/wikis/fortnite/match_legacy.lua b/components/match2/wikis/fortnite/match_legacy.lua new file mode 100644 index 0000000000..725ace4dd1 --- /dev/null +++ b/components/match2/wikis/fortnite/match_legacy.lua @@ -0,0 +1,44 @@ +--- +-- @Liquipedia +-- wiki=fortnite +-- page=Module:Match/Legacy +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local MatchLegacy = {} + +local DisplayHelper = require('Module:MatchGroup/Display/Helper') +local Json = require('Module:Json') +local String = require('Module:StringUtils') +local Table = require('Module:Table') + + +function MatchLegacy.storeMatch(match2) + for gameIndex, game2 in ipairs(match2.match2games or {}) do + local match = Table.deepCopy(match2) + local g2extradata = Json.parseIfString(game2.extradata) or {} + + match.date = game2.date + match.vod = game2.vod + match.dateexact = g2extradata.dateexact + match.finished = String.isNotEmpty(game2.winner) + match.staticid = match2.match2id .. '_' .. gameIndex + + -- Handle extradata fields + local bracketData = Json.parseIfString(match2.match2bracketdata) + if type(bracketData) == 'table' and bracketData.inheritedheader then + match.header = (DisplayHelper.expandHeader(bracketData.inheritedheader) or {})[1] + end + local m1extradata = {} + + m1extradata.map = game2.map + m1extradata.round = tostring(gameIndex) + + match.extradata = mw.ext.LiquipediaDB.lpdb_create_json(m1extradata) + + mw.ext.LiquipediaDB.lpdb_match('legacymatch_' .. match2.match2id .. '_' .. gameIndex, match) + end +end + +return MatchLegacy diff --git a/components/match2/wikis/fortnite/match_summary.lua b/components/match2/wikis/fortnite/match_summary.lua new file mode 100644 index 0000000000..e0b091e2d6 --- /dev/null +++ b/components/match2/wikis/fortnite/match_summary.lua @@ -0,0 +1,64 @@ +--- +-- @Liquipedia +-- wiki=fortnite +-- page=Module:MatchSummary +-- +-- Please see https://github.com/Liquipedia/Lua-Modules to contribute +-- + +local CustomMatchSummary = {} + +local Array = require('Module:Array') +local FnUtil = require('Module:FnUtil') +local Lua = require('Module:Lua') + +local MatchGroupUtil = Lua.import('Module:MatchGroup/Util') +local SummaryHelper = Lua.import('Module:MatchSummary/Ffa') + +local MatchSummaryWidgets = Lua.import('Module:Widget/Match/Summary/Ffa/All') +local HtmlWidgets = Lua.import('Module:Widget/Html/All') + +---@class FortniteMatchGroupUtilMatch: MatchGroupUtilMatch +---@field games ApexMatchGroupUtilGame[] + +---@param props {bracketId: string, matchId: string} +---@return Widget +function CustomMatchSummary.getByMatchId(props) + ---@class ApexMatchGroupUtilMatch + local match = MatchGroupUtil.fetchMatchForBracketDisplay(props.bracketId, props.matchId) + CustomMatchSummary._opponents(match) + local scoringData = SummaryHelper.createScoringData(match) + + return HtmlWidgets.Fragment{children = { + MatchSummaryWidgets.Header{matchId = match.matchId, games = match.games}, + MatchSummaryWidgets.Tab{ + matchId = match.matchId, + idx = 0, + children = { + MatchSummaryWidgets.GamesSchedule{games = match.games}, + MatchSummaryWidgets.PointsDistribution{killScore = scoringData.kill, placementScore = scoringData.placement}, + SummaryHelper.standardMatch(match), + } + } + }} +end + +---@param match table +function CustomMatchSummary._opponents(match) + -- Add games opponent data to the match opponent + Array.forEach(match.opponents, function (opponent, idx) + opponent.games = Array.map(match.games, function (game) + return game.opponents[idx] + end) + end) + + -- Sort match level based on final placement & score + Array.sortInPlaceBy(match.opponents, FnUtil.identity, SummaryHelper.placementSortFunction) + + -- Set the status of the current placement + Array.forEach(match.opponents, function(opponent, idx) + opponent.placementStatus = match.extradata.status[idx] + end) +end + +return CustomMatchSummary diff --git a/spec/golden_masters/match2_matchlist_smoke_apexlegends.txt b/spec/golden_masters/match2_matchlist_smoke_apexlegends.txt index 119498d8d9..9e48a2e5f0 100644 --- a/spec/golden_masters/match2_matchlist_smoke_apexlegends.txt +++ b/spec/golden_masters/match2_matchlist_smoke_apexlegends.txt @@ -1 +1 @@ -