diff --git a/lua/wikis/commons/TeamList/Starcraft.lua b/lua/wikis/commons/TeamList/Starcraft.lua
new file mode 100644
index 00000000000..57f05d89cac
--- /dev/null
+++ b/lua/wikis/commons/TeamList/Starcraft.lua
@@ -0,0 +1,443 @@
+---
+-- @Liquipedia
+-- page=Module:TeamList/Starcraft
+--
+-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
+--
+
+local Arguments = require('Module:Arguments')
+local Array = require('Module:Array')
+local Class = require('Module:Class')
+local Json = require('Module:Json')
+local Logic = require('Module:Logic')
+local Lua = require('Module:Lua')
+local Table = require('Module:Table')
+
+local TeamCard = Lua.import('Module:TeamList/Starcraft/TeamCard')
+local TournamentStructure = Lua.import('Module:TournamentStructure')
+
+local Opponent = Lua.import('Module:Opponent/Custom')
+
+local TeamParticipantsController = Lua.import('Module:TeamParticipants/Controller')
+local Tabs = Lua.import('Module:Tabs')
+
+local TeamListWrapper = {}
+
+---@class StarcraftTeamList
+---@operator call(table): StarcraftTeamList
+---@field args table
+---@field config StarcraftTeamListConfig
+---@field sections StarcraftTeamListSection[]
+---@field root Html?
+local TeamList = Class.new(
+ function(self, args)
+ self.args = args
+ end
+)
+
+---@param frame Frame
+---@return Renderable?
+function TeamListWrapper.TemplateTeamList(frame)
+ local args = Arguments.getArgs(frame)
+
+ for _, item in pairs(args) do
+ if item:find('<%s*br%s*/?>') then
+ mw.ext.TeamLiquidIntegration.add_category('TeamList with br')
+ end
+ end
+
+ local newArgs = TeamList(args):read():map()
+
+ if Logic.readBool(args.generate) then
+ TeamListWrapper.generate(newArgs)
+ end
+
+ if Array.any(newArgs, function(section)
+ return Array.any(section, function(opp)
+ return Logic.isNotEmpty(opp.notes)
+ end)
+ end) then
+ mw.ext.TeamLiquidIntegration.add_category('TeamList with notes')
+ end
+
+ if not newArgs[2] then
+ return TeamParticipantsController.fromTemplate(newArgs[1])
+ end
+
+ local tabArgs = {}
+ Array.forEach(newArgs, function(tpArgs, index)
+ if not tpArgs.title then
+ mw.ext.TeamLiquidIntegration.add_category('TeamList with missing section title')
+ end
+ tabArgs['name' .. index] = tpArgs.title
+ tabArgs['content' .. index] = TeamParticipantsController.fromTemplate(tpArgs)
+ end)
+
+ return Tabs.dynamic(tabArgs)
+end
+
+---@param args table[]
+---@return string
+function TeamListWrapper.generate(args)
+ if not args[2] then
+ return TeamListWrapper.generateSingle(args[1])
+ end
+
+ local parts = {'{{Tabs dynamic'}
+ Array.forEach(args, function(tpArgs, index)
+ table.insert(parts, '|name' .. index .. '=' .. tpArgs.title)
+ end)
+ table.insert(parts, '|This=1')
+ table.insert(parts, '}}')
+
+ Array.forEach(args, function(tpArgs, index)
+ table.insert(parts, '{{Tabs dynamic/tab|' .. index .. '}}')
+ table.insert(parts, TeamListWrapper.generateSingle(tpArgs))
+ end)
+
+ table.insert(parts, '{{Tabs dynamic/end}}')
+
+ return table.concat(parts, '\n')
+end
+
+---@param args table
+---@return string
+function TeamListWrapper.generateSingle(args)
+ local parts = {
+ '{{TeamParticipants',
+ TeamListWrapper.generateOuterConfig(args),
+ }
+
+ Array.forEach(args, function(oppArgs)
+ table.insert(parts, TeamListWrapper.generateOpponent(oppArgs))
+ end)
+ table.insert(parts, '}}')
+
+ return table.concat(parts, '\n')
+end
+
+---@param args table
+---@return string?
+function TeamListWrapper.generateOuterConfig(args)
+ local params = {
+ 'showplayerinfo',
+ 'date',
+ }
+
+ local parts = Array.map(params, function(param)
+ local value = args[param]
+ if Logic.isEmpty(value) then return end
+ return '|' .. param .. '=' .. value
+ end)
+
+ local store = args.store == false and 'false' or 'false'
+ table.insert(parts, '|store=' .. store)
+
+ return table.concat(parts)
+end
+
+
+---@param args table
+---@return string
+function TeamListWrapper.generateOpponent(args)
+ local parts = {
+ '\t|{{Opponent|' .. args.template,
+ '\t\t|import=false',
+ Logic.isNotEmpty(args.date) and ('\t\t|date=' .. args.date) or nil,
+ }
+
+ table.insert(parts, '\t\t|players={{Persons')
+ Array.forEach(args.players, function(playerArgs)
+ table.insert(parts, TeamListWrapper.generatePlayer(playerArgs))
+ end)
+ table.insert(parts, '\t\t}}')
+
+ if Logic.isNotEmpty(args.notes) then
+ table.insert(parts, '\t\t|notes={{Json')
+ Array.forEach(args.notes, function(note)
+ table.insert(parts, '\t\t\t|' .. note)
+ end)
+ table.insert(parts, '\t\t}}')
+ end
+
+ table.insert(parts, '\t}}')
+
+ return table.concat(parts, '\n')
+end
+
+---@param args table
+---@return string
+function TeamListWrapper.generatePlayer(args)
+ local parts = {
+ '\t\t\t|{{Person|' .. args.name,
+ }
+
+ local add = function(param)
+ local value = args[param]
+ if Logic.isEmpty(value) then return end
+ table.insert(parts, '|' .. param .. '=' .. tostring(value))
+ end
+
+ local params = {
+ 'link',
+ 'flag',
+ 'faction',
+ 'team',
+ 'role',
+ 'played',
+ 'results',
+ 'status',
+ }
+ Array.forEach(params, add)
+
+ table.insert(parts, '}}')
+
+ return table.concat(parts)
+end
+
+---@return self
+function TeamList:read()
+ self.config = TeamList.readConfig(self.args)
+ self:readSections()
+
+ return self
+end
+
+---@return table[]
+function TeamList:map()
+ return Array.map(self.sections, function(section)
+ local config = section.config
+
+ local args = {
+ title = section.title or config.title,
+ showplayerinfo = config.playerInfoButton and 'true' or nil,
+ date = config.resolveDate,
+ store = not config.noStorage,
+ }
+
+ if args.title and config.showCountBySection then
+ args.title = args.title .. ' (' .. (config.count or #section.entries) .. ')'
+ end
+
+ if config.sortTeams then
+ Array.sortInPlaceBy(section.entries, function(entry) return entry.name:lower() end)
+ end
+
+ return Table.mergeInto(args, Array.map(section.entries, TeamList.mapEntry))
+ end)
+end
+
+---@param entry StarcraftTeamCard
+---@return table
+function TeamList.mapEntry(entry)
+ local opp = entry.opponent
+ local notes = {opp.note}
+ local args = {
+ import = 'false', --- disallow this shit as it just fucks up things ...
+ players = Array.map(opp.players, function(player)
+ table.insert(notes, player.note)
+ return TeamList.mapPlayer(player)
+ end),
+ template = opp.template,
+ date = opp.date,
+ }
+ args.notes = notes
+
+ if opp.dq then
+ mw.ext.TeamLiquidIntegration.add_category('TeamList with dq opponent')
+ end
+
+ return args
+end
+
+---@param player StarcraftTeamCardPlayer
+---@return table
+function TeamList.mapPlayer(player)
+ local args = {
+ name = player.displayName or player.pageName,
+ link = player.pageName,
+ flag = player.flag,
+ faction = player.faction,
+ team = player.mainTeamPage,
+ }
+
+ if player.dnp then
+ args.played = 'false'
+ end
+ args.role = player.captain and 'Captain' or nil
+
+ if player.dq then
+ args.status = 'former'
+ args.result = 'false'
+ elseif player.withdraw then
+ args.status = 'former'
+ end
+
+ return args
+end
+
+---@class StarcraftTeamListConfig: StarcraftTeamCardConfig
+---@field showCountBySection boolean
+---@field count number?
+---@field title string?
+---@field sortTeams boolean
+---@field playerInfoButton boolean
+---@field matchGroupSpec {matchGroupIds: string[], pageNames: string[]}?
+---@field import boolean
+---@field importOnlyQualified boolean
+
+---@param args table
+---@param parentConfig StarcraftTeamListConfig?
+---@return StarcraftTeamListConfig
+function TeamList.readConfig(args, parentConfig)
+ parentConfig = parentConfig or {}
+
+ local matchGroupSpec = TournamentStructure.readMatchGroupsSpec(args)
+ local import = matchGroupSpec ~= nil
+
+ local config = {
+ --display
+ showCountBySection = Logic.readBool(args.showCountBySection or parentConfig.showCountBySection),
+ count = tonumber(args.count),
+ title = args.title,
+ sortTeams = Logic.nilOr(Logic.readBoolOrNil(args.sortTeams), parentConfig.sortTeams, true),
+ playerInfoButton = Logic.readBool(args.playerInfoButton),
+ isAdhoc = Logic.nilOr(Logic.readBoolOrNil(args.adhoc), parentConfig.isAdhoc, false),
+ --import
+ matchGroupSpec = matchGroupSpec,
+ import = import,
+ importOnlyQualified = Logic.readBool(args.onlyQualified),
+ autoDnp = Logic.nilOr(Logic.readBoolOrNil(args.autoDnp), import or parentConfig.import or nil),
+ }
+
+ return Table.merge(TeamCard.readConfig(args, parentConfig), config)
+end
+
+function TeamList:readSections()
+ self.sections = {}
+
+ local firstSection = Json.parseIfTable(self.args[1])
+ if not firstSection then
+ return
+ elseif firstSection.type ~= 'section' then
+ --assume no sections and treat whole list as first section
+ table.insert(self.sections, self:readSection(self.args))
+ return
+ end
+
+ Array.forEach(self.args, function(potentialSection)
+ local sectionArgs = Json.parseIfTable(potentialSection)
+ assert(sectionArgs and sectionArgs.type == 'section', 'Invalid input: "' .. potentialSection .. '" is not a section')
+ table.insert(self.sections, self:readSection(sectionArgs))
+ end)
+end
+
+---@class StarcraftTeamListSection
+---@field config StarcraftTeamListConfig
+---@field title string?
+---@field entries StarcraftTeamCard[]
+
+---@param sectionArgs table
+---@return StarcraftTeamListSection
+function TeamList:readSection(sectionArgs)
+ local section = {config = TeamList.readConfig(sectionArgs, self.config), title = sectionArgs.title}
+ local entriesByName = {}
+
+ sectionArgs = Array.extractValues(Table.filterByKey(sectionArgs, function(key) return type(key) == 'number' end))
+
+ Array.forEach(sectionArgs, function(teamCardArgs)
+ local entry = TeamCard(Table.merge({date = section.config.resolveDate}, Json.parseIfTable(teamCardArgs)))
+ entriesByName[entry.name] = entry
+ end)
+
+ section.entries = Array.map(self:import(section.config, entriesByName), function(entry)
+ return entry:getConfig(section.config):sync(self.config.matchGroupSpec)
+ end)
+
+ return section
+end
+
+---@param config StarcraftTeamListConfig
+---@param entriesByName table
+---@return StarcraftTeamCard[]
+function TeamList:import(config, entriesByName)
+ if not config.import then
+ return Array.extractValues(entriesByName)
+ end
+
+ local matchRecords = TeamList._fetchMatchRecords(config.matchGroupSpec)
+ if Table.isEmpty(matchRecords) then
+ return Array.extractValues(entriesByName)
+ end
+ ---@cast matchRecords -nil
+ return TeamList._entriesFromMatchRecords(matchRecords, config, entriesByName)
+end
+
+---@param matchGroupSpec {matchGroupIds: string[], pageNames: string[]}
+---@return table[]
+function TeamList._fetchMatchRecords(matchGroupSpec)
+ return mw.ext.LiquipediaDB.lpdb('match2', {
+ conditions = tostring(TournamentStructure.getMatch2Filter(matchGroupSpec)),
+ query = 'pagename, match2bracketdata, match2opponents, winner',
+ order = 'date asc',
+ limit = 5000,
+ })
+end
+
+---@param matchRecords table[]
+---@param config StarcraftTeamListConfig
+---@param entriesByName table
+---@return StarcraftTeamCard[]
+function TeamList._entriesFromMatchRecords(matchRecords, config, entriesByName)
+ Array.forEach(matchRecords, function(matchRecord)
+ Array.forEach(matchRecord.match2opponents, function(opponentRecord, opponentIndex)
+ if not TeamList._shouldInclude(opponentIndex, matchRecord, config.importOnlyQualified) then
+ return
+ end
+
+ if entriesByName[opponentRecord.name] then
+ return
+ end
+
+ entriesByName[opponentRecord.name] = TeamList._entryFromOpponentRecord(opponentRecord, config.resolveDate)
+ end)
+ end)
+
+ return Array.extractValues(entriesByName)
+end
+
+---@param opponentIndex integer
+---@param matchRecord table
+---@param importOnlyQualified boolean?
+---@return boolean
+function TeamList._shouldInclude(opponentIndex, matchRecord, importOnlyQualified)
+ local bracketData = matchRecord.match2bracketdata
+ return not importOnlyQualified or Logic.readBool(bracketData.quallose) or
+ Logic.readBool(bracketData.qualwin) and tonumber(matchRecord.winner) == opponentIndex
+end
+
+---@param opponentRecord table
+---@param resolveDate string
+---@return StarcraftTeamCard?
+function TeamList._entryFromOpponentRecord(opponentRecord, resolveDate)
+ if opponentRecord.type ~= Opponent.team or not opponentRecord.template or opponentRecord.template:lower() == 'tbd' then
+ return
+ end
+
+ local opponentArgs = {
+ team = opponentRecord.template,
+ date = resolveDate
+ }
+
+ Array.forEach(opponentRecord.match2players, function(playerRecord, playerIndex)
+ local prefix = 'p' .. playerIndex
+ opponentArgs[prefix] = playerRecord.displayname
+ opponentArgs[prefix .. 'link'] = playerRecord.name
+ opponentArgs[prefix .. 'flag'] = playerRecord.flag
+ opponentArgs[prefix .. 'faction'] = (playerRecord.extradata or {}).faction
+ end)
+
+ return TeamCard(opponentArgs)
+end
+
+return TeamListWrapper
diff --git a/lua/wikis/commons/TeamList/Starcraft/TeamCard.lua b/lua/wikis/commons/TeamList/Starcraft/TeamCard.lua
new file mode 100644
index 00000000000..f18e7a578c2
--- /dev/null
+++ b/lua/wikis/commons/TeamList/Starcraft/TeamCard.lua
@@ -0,0 +1,306 @@
+---
+-- @Liquipedia
+-- page=Module:TeamList/Starcraft/TeamCard
+--
+-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
+--
+
+local Lua = require('Module:Lua')
+
+local Array = Lua.import('Module:Array')
+local Class = Lua.import('Module:Class')
+local Faction = Lua.import('Module:Faction')
+local Flags = Lua.import('Module:Flags')
+local FnUtil = Lua.import('Module:FnUtil')
+local Json = Lua.import('Module:Json')
+local Logic = Lua.import('Module:Logic')
+local Lpdb = Lua.import('Module:Lpdb')
+local Namespace = Lua.import('Module:Namespace')
+local String = Lua.import('Module:StringUtils')
+local Table = Lua.import('Module:Table')
+local TeamTemplate = Lua.import('Module:TeamTemplate')
+local Variables = Lua.import('Module:Variables')
+
+local PlayerExt = Lua.import('Module:Player/Ext')
+local PlayerExtCustom = Lua.import('Module:Player/Ext/Custom')
+local TournamentStructure = Lua.import('Module:TournamentStructure')
+
+local Opponent = Lua.import('Module:Opponent/Custom')
+
+-- can't use the DateExt function
+-- due to the wiki vars not existing if using subst bot run
+local getContextualDateOrNow = function()
+ local date = Variables.varDefault('tournament_enddate')
+ or Variables.varDefault('tournament_startdate')
+
+ if Logic.isNotEmpty(date) then return date end
+
+ local pageName = mw.title.getCurrentTitle().prefixedText:gsub(' ', '_')
+
+ local data = mw.ext.LiquipediaDB.lpdb('tournament', {
+ conditions = '.[[pagename::' .. pageName .. ']]',
+ query = 'startdate, enddate',
+ limit = 1,
+ })[1] or {}
+
+ return Logic.nilIfEmpty(data.enddate)
+ or Logic.nilIfEmpty(data.startdate)
+ or os.date('%F') --[[@as string]]
+end
+
+---@class StarcraftTeamCard
+---@operator call(table): StarcraftTeamCard
+---@field args table
+---@field config StarcraftTeamCardConfig
+---@field opponent StarcraftTeamCardOpponent
+---@field name string
+---@field root Html?
+local TeamCard = Class.new(
+ function(self, args)
+ self.args = args
+ self.opponent = self:readOpponent()
+ local opponentName = Opponent.toName(self.opponent)
+ assert(opponentName, 'Missing Team Template for "' .. (args.team or '') .. '"')
+ self.name = opponentName:gsub(' ', '_')
+ end
+)
+
+---@class StarcraftTeamCardOpponent: StarcraftStandardOpponent
+---@field players StarcraftTeamCardPlayer[]
+---@field note string?
+---@field dq boolean
+---@field subtitle string?
+---@field date string
+
+---@return StarcraftTeamCardOpponent
+function TeamCard:readOpponent()
+ local args = self.args
+ local date = args.date or getContextualDateOrNow()
+ local team = (args.team or 'tbd'):lower():gsub('_', ' ')
+ local opponent = Opponent.resolve(
+ Opponent.readOpponentArgs{team, type = Opponent.team}, date
+ ) --[[@as StarcraftTeamCardOpponent]]
+
+ opponent.dq = Logic.readBool(args.dq)
+ opponent.date = date
+ opponent.note = args.note
+
+ opponent.players = Array.extractValues(Table.mapArgumentsByPrefix(args, {'p', 'player'}, function(key, index)
+ return self:readPlayer(key, index, date)
+ end))
+
+ if #opponent.players >= 35 then
+ mw.ext.TeamLiquidItegration.add_category('TeamCards with 35 players')
+ elseif #opponent.players >= 25 then
+ mw.ext.TeamLiquidIntegration.add_category('TeamCards with 25 players')
+ elseif #opponent.players >= 20 then
+ mw.ext.TeamLiquidIntegration.add_category('TeamCards with 20 players')
+ end
+
+ return opponent
+end
+
+---@class StarcraftTeamCardPlayer: StarcraftStandardPlayer
+---@field ace boolean?
+---@field captain boolean?
+---@field dnp boolean?
+---@field dq boolean?
+---@field joker boolean?
+---@field mainTeam string?
+---@field mainTeamPage string?
+---@field note boolean?
+---@field tag string?
+---@field tagTitle string?
+---@field two boolean?
+---@field withdraw boolean?
+
+---@param key any
+---@param index integer
+---@param date string
+---@return StarcraftTeamCardPlayer
+function TeamCard:readPlayer(key, index, date)
+ local args = self.args
+
+ local getArg = function(field)
+ return args['p' .. index .. field] or args[field .. index]
+ end
+
+ local mainTeamInput = getArg('team')
+ if mainTeamInput and mainTeamInput:lower() == 'noteam' then
+ mainTeamInput = nil
+ end
+
+ local mainTeam, mainTeamPage
+ if mainTeamInput then
+ mainTeam = TeamTemplate.resolve(mainTeamInput, date)
+ assert(mainTeam, 'missing team template "' .. mainTeamInput .. '"')
+ mainTeamPage = TeamTemplate.getPageName(mainTeam) or nil
+ end
+
+ return {
+ displayName = args[key],
+ flag = String.nilIfEmpty(Flags.CountryName{flag = getArg('flag')}),
+ pageName = getArg('link'),
+ faction = Faction.read(getArg('faction') or getArg('race')),
+
+ ace = Logic.readBoolOrNil(getArg('ace')),
+ captain = Logic.readBoolOrNil(getArg('captain')),
+ dnp = Logic.readBoolOrNil(getArg('dnp')),
+ dq = Logic.readBoolOrNil(getArg('dq') or getArg('out')),
+ joker = Logic.readBoolOrNil(getArg('joker')),
+ mainTeam = mainTeam,
+ mainTeamPage = mainTeamPage,
+ note = getArg('note'),
+ tag = getArg('tag'),
+ tagTitle = getArg('tagTitle'),
+ two = Logic.readBoolOrNil(getArg('two')),
+ withdraw = Logic.readBoolOrNil(getArg('withdraw')),
+ }
+end
+
+---@class StarcraftTeamCardConfig
+---@field cardWidth string
+---@field teamStyle string?
+---@field showFlags boolean
+---@field display boolean
+---@field collapsed boolean
+---@field collapsible boolean?
+---@field autoDnp boolean
+---@field syncPlayers boolean
+---@field resolveDate string
+---@field sortPlayers boolean
+---@field noStorage boolean
+---@field isAdhoc boolean?
+
+---@param parentConfig StarcraftTeamListConfig?
+---@return self
+function TeamCard:getConfig(parentConfig)
+ self.config = TeamCard.readConfig(self.args, parentConfig)
+
+ return self
+end
+
+---@param args table
+---@param parentConfig StarcraftTeamListConfig?
+---@return StarcraftTeamListConfig
+function TeamCard.readConfig(args, parentConfig)
+ parentConfig = parentConfig or {}
+
+ local width = tonumber(args.cardWidth or args.width)
+
+ return {
+ --display
+ cardWidth = width and (width .. 'px') or args.cardWidth or args.width or parentConfig.cardWidth or '240px',
+ teamStyle = Logic.readBool(args.short) and 'short' or parentConfig.teamStyle,
+ showFlags = Logic.nilOr(Logic.readBoolOrNil(args.showFlags), parentConfig.showFlags, true),
+ display = not Logic.readBool(args.hidden),
+ collapsed =Logic.nilOr(Logic.readBoolOrNil(args.collapsed), not Logic.readBoolOrNil(args.uncollapsed)),
+ collapsible = Logic.nilOr(Logic.readBoolOrNil(args.collapsible), parentConfig.collapsible, true),
+ --sync
+ autoDnp = Logic.nilOr(Logic.readBoolOrNil(args.autoDnp), parentConfig.autoDnp, true),
+ syncPlayers = Logic.nilOr(Logic.readBoolOrNil(args.syncPlayers), parentConfig.syncPlayers, true),
+ resolveDate = args.date or parentConfig.resolveDate or getContextualDateOrNow(),
+ sortPlayers = Logic.nilOr(Logic.readBoolOrNil(args.sortPlayers), parentConfig.sortPlayers, true),
+ --storage
+ noStorage = Logic.readBool(args.noStorage or parentConfig.noStorage or
+ Lpdb.isStorageDisabled() or not Namespace.isMain()),
+ isAdhoc = Logic.nilOr(Logic.readBoolOrNil(args.adhoc), parentConfig.isAdhoc),
+ }
+end
+
+---@param parentMatchGroupSpec {matchGroupIds: string[], pageNames: string[]}?
+---@return self
+function TeamCard:sync(parentMatchGroupSpec)
+ local config = self.config
+
+ local players = self.opponent.players
+
+ if Table.isEmpty(players) then
+ return self
+ end
+
+ local date = self.opponent.date
+
+ if config.syncPlayers then
+ players = Array.map(players, function(player)
+ player = Table.merge(player, PlayerExtCustom.syncPlayer(player, {date = date}))
+ player.pageName = player.pageName:gsub(' ', '_')
+ player.mainTeam = config.isAdhoc and PlayerExt.syncTeam(player.pageName, player.mainTeam, {}) or player.mainTeam
+ player.mainTeamPage = player.mainTeamPage or
+ player.mainTeam and TeamTemplate.getPageName(TeamTemplate.resolve(player.mainTeam, date) --[[@as string]]) or
+ nil
+
+ return player
+ end)
+ end
+
+ if config.autoDnp then
+ local matchGroupSpec = parentMatchGroupSpec or TournamentStructure.currentPageSpec()
+ players = self:dnp(players, matchGroupSpec)
+ end
+
+ if config.sortPlayers then
+ Array.sortInPlaceBy(players, function(player) return player.displayName:lower() end)
+ end
+
+ self.opponent.players = players
+
+ return self
+end
+
+---@param players StarcraftTeamCardPlayer[]
+---@param matchGroupSpec {matchGroupIds: string[], pageNames: string[]}
+---@return StarcraftTeamCardPlayer[]
+function TeamCard:dnp(players, matchGroupSpec)
+ local dnpData = TeamCard.fetchDnp(matchGroupSpec)
+
+ Array.forEach(players, function(player)
+ player.dnp = player.dnp or (dnpData[self.name] and not dnpData[self.name][player.pageName])
+ end)
+
+ return players
+end
+
+TeamCard.fetchDnp = FnUtil.memoize(function(matchGroupSpec)
+ return TeamCard.fetchDnpData(matchGroupSpec)
+end)
+
+---@param matchGroupSpec {matchGroupIds: string[], pageNames: string[]}
+---@return table>
+function TeamCard.fetchDnpData(matchGroupSpec)
+ local matchRecords = mw.ext.LiquipediaDB.lpdb('match2', {
+ conditions = tostring(TournamentStructure.getMatch2Filter(matchGroupSpec)),
+ query = 'pagename, match2bracketdata, match2opponents, winner, match2games',
+ order = 'date asc',
+ limit = 5000,
+ })
+
+ local playersByTeam = {}
+ Array.forEach(matchRecords, function(matchRecord)
+ local teams = Array.map(matchRecord.match2opponents, function(opponent, opponentIndex)
+ playersByTeam[opponent.name] = playersByTeam[opponent.name] or {}
+ return {name = opponent.name, players = Array.map(opponent.match2players, function(player) return player.name end)}
+ end)
+
+ Array.forEach(matchRecord.match2games, function(game)
+ local gameOpponents = game.opponents
+ if type(gameOpponents) ~= 'table' then
+ gameOpponents = Json.parseIfTable(gameOpponents) or {}
+ end
+ Array.forEach(gameOpponents, function(opp, opponentIndex)
+ for playerIndex, player in pairs(opp.players or {}) do
+ if Logic.isNotEmpty(player) then
+ local matchPlayer = teams[opponentIndex].players[playerIndex]
+ if player then
+ playersByTeam[teams[opponentIndex].name][matchPlayer] = true
+ end
+ end
+ end
+ end)
+ end)
+ end)
+
+ return playersByTeam
+end
+
+return TeamCard
diff --git a/lua/wikis/starcraft2/InGameRoles.lua b/lua/wikis/starcraft2/InGameRoles.lua
new file mode 100644
index 00000000000..cbe3ef6b41d
--- /dev/null
+++ b/lua/wikis/starcraft2/InGameRoles.lua
@@ -0,0 +1,13 @@
+---
+-- @Liquipedia
+-- page=Module:InGameRoles
+--
+-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
+--
+
+---@type table
+local inGameRoles = {
+ ['captain'] = {category = 'Captain', display = 'Captain'},
+}
+
+return inGameRoles