Module:Bracket

From Call of Duty Esports Wiki
Jump to: navigation, search

To edit the documentation or categories for this module, click here. This module has an i18n file. Click here to edit it.


local util_args = require('Module:ArgsUtil')
local util_cargo = require('Module:CargoUtil')
local util_esports = require('Module:EsportsUtil')
local util_map = require("Module:MapUtil")
local util_table = require('Module:TableUtil')
local util_text = require('Module:TextUtil')
local util_vars = require('Module:VarsUtil')
local bracket_wiki = require('Module:Bracket/Wiki') -- wiki localization per game
local i18n = require('Module:i18nUtil')

local m_team = require('Module:Team')
local lang = mw.getLanguage('en')

local ROWS_PER_TEAM = 6
local ROWS_PER_TITLE = 2
local ROWS_PER_HLINE = 1
local ROUNDWIDTH = 12
local LINEWIDTH = '3em'
local SCOREWIDTH = 2

local TEAM_STYLE = nil -- constant from args
local SHOW_BESTOF = false
local CURRENT_ROUND = nil -- index used in too many places to pass in every function signature

local p = {}
local h = {}

function p.main(frame)
	local tpl_args = util_args.merge()
	i18n.init('Bracket')
	h.setConstants(tpl_args)
	-- use require instead of loadData so that we can use next() and #
	local settings
	local function assignBracket()
		settings = require('Module:Bracket/'.. tpl_args[1])
	end
	
	if not tpl_args[1] then
		error('No bracket definition provided!')
	elseif pcall(assignBracket) then
		-- pass
	else
		error(('Bracket %s is not a valid input'):format(tpl_args[1]))
	end
	
	local args = h.processArgs(tpl_args)
	h.processSettings(settings, args)
	if util_args.castAsBool(args.cargo) then
		h.addCargoData(args, settings)
	end
	return h.makeOutput(args, settings)
end

function h.setConstants(tpl_args)
	TEAM_STYLE = tpl_args.teamstyle
	SHOW_BESTOF = util_args.castAsBool(tpl_args.show_bestof)
end

function h.processArgs(tpl_args)
	-- format tpl_args
	local args = {}
	for k, v in pairs(tpl_args) do
		if type(k) ~= 'string' then
			-- pass
		elseif k:find('R%d+M%d+_.*_') then
			-- team-specific arg
			local r, m, val, team = k:match('R(%d+)M(%d+)_(.*)_(%d+)')
			r = tonumber(r)
			m = tonumber(m)
			h.initializeMatch(args, r, m)
			args[r][m]['team' .. team][val] = h.castArg(val, v)
		elseif k:find('R%d+M%d+_.*') then
			-- match-specific arg
			local r, m, val = k:match('R(%d+)M(%d+)_(.*)')
			r = tonumber(r)
			m = tonumber(m)
			h.initializeMatch(args, r, m)
			args[r][m][val] = h.castArg(val, v)
		elseif k:find('R%d+_') then
			-- round-specific arg
			local r, val = k:match('R(%d+)_(.*)')
			r = tonumber(r)
			h.initializeMatch(args, r)
			args[r][val] = v
		else
			-- global arg
			args[k] = v
		end
	end
	h.addImpliedArgs(args)
	return args
end	

function h.castArg(val, v)
	if val == 'team' then return m_team.teamlinkname(v) end
	if val == 'bye' then return util_args.castAsBool(v) end
	if val == 'winner' or val == 'bestof' then return tonumber(v) end
	if val == 'winners' then return h.castWinnersArg(v) end
	if val == 'score' then return h.castScoreArg(v) end
	return v
end

function h.castWinnersArg(v)
	return util_map.split(v, nil, tonumber)
end

function h.castScoreArg(v)
	return util_map.split(v, nil, h.castPartScoreArg)
end

function h.castPartScoreArg(str)
	return tonumber(str) or str
end

function h.initializeMatch(args, r, m)
	if not args[r] then
		args[r] = {}
	end
	if not args[r][m] and m then
		args[r][m] = { team1 = {}, team2 = {} }
	end
end

function h.addImpliedArgs(args)
	for round, roundData in pairs(args) do
		if type(round) == 'number' then
			for match, matchData in pairs(roundData) do
				if type(match) == 'number' then
					h.addImpliedArgsToMatch(matchData)
				end
			end
		end
	end
end

function h.addImpliedArgsToMatch(matchData)
	if matchData.class == 'qualified' then
		matchData.label = i18n.print('qualMatch')
	elseif matchData.class == 'relegated' then
		matchData.label = i18n.print('relMatch')
	end
	for i, v in ipairs({ 'team1', 'team2' }) do
		if matchData[v] then
			h.addImpliedArgsToTeam(matchData[v], matchData, i)
		end
	end
end

function h.addImpliedArgsToTeam(team, matchData, i)
	team.teamfinal = team.teamfinal or team.team
	team.iswinner = matchData.winner == i
	team.bestof = matchData.bestof
	
	local function mapWinners(winner)
		return winner == i
	end
	if matchData.winners then
		team.iswinners = util_map.copy(matchData.winners, mapWinners)
	end
end

function h.processSettings(settings, args)
	-- in theory this could be done in the settings module before returning but
	-- this way the code is a bit more hidden from users editing stuff
	-- and also this makes the settings module closer to a read-only table that you
	-- import (and clone) here which i guess is nice?
	-- tbh im not sure if this was the right way to do it tho
	for r, col in ipairs(settings) do
		local m = #col.matches
		while m >= 1 do
			-- need to iterate backwards bc we'll delete third-place matches if hidden
			local match = col.matches[m]
			local lines = col.lines and col.lines[m]
			if lines and lines.reseed then
				lines.class = lines.class:format(lang:lc(args.reseed or 'reseeding'))
			end
			if match.argtoshow then
				if not util_args.castAsBool(args[match.argtoshow]) then
					if col.matches[m+1] then
						col.matches[m+1].above = (col.matches[m+1].above or 0) + (match.above or 0) + 6
					end
					table.remove(col.matches,m)
				end
			end
			m = m - 1
		end
	end
end

-- cargo
function h.addCargoData(args, settings)
	local overviewPage = util_esports.getOverviewPage(args.page)
	local data = h.doCargoQuery(overviewPage)
	if #data == 0 then
		return
	end
	local processed = h.processCargoData(data)
	h.addProcessedToArgs(args, settings, processed, overviewPage)
end

function h.doCargoQuery(page)
	local query = {
		tables = {
			'MatchSchedule=MS',
			'Teams=Teams1',
			'TournamentRosters=Ros1',
			'Teams=Teams2',
			'TournamentRosters=Ros2',
		},
		join = {
			'MS.Team1=Teams1._pageName',
			'MS.PageAndTeam1=Ros1.PageAndTeam',
			'MS.Team2=Teams2._pageName',
			'MS.PageAndTeam2=Ros2.PageAndTeam',
		},
		fields = h.getFields(),
		where = ('MS.OverviewPage="%s"'):format(page),
	}
	return util_cargo.queryAndCast(query)
end

function h.getFields()
	local fields = {
		'MS.Team1',
		'MS.Team2',
		'MS.Team1Final',
		'MS.Team2Final',
		'MS.Winner [number]',
		'MS.Player1',
		'MS.Player2',
		'MS.FF [number]',
		'MS.WinnerScoreUnknown [number]',
		'MS.Team1Score [number]',
		'MS.Team2Score [number]',
		'MS.Tab',
		'MS.N_MatchInTab',
		'MS.UniqueMatch',
		'MS.BestOf [number]',
	}
	if util_vars.getVar('Event Region') == 'International' then
		fields[#fields+1] = 'COALESCE(Ros1.Region, Teams1.Region)=Team1Region'
		fields[#fields+1] = 'COALESCE(Ros2.Region,Teams2.Region)=Team2Region'
	end
	return fields
end

function h.processCargoData(data)
	local processed = {}
	for _, row in ipairs(data) do
		h.sortFF(row)
		h.scoreUnknown(row)
		processed[('%s_%s'):format(row.Tab,row.N_MatchInTab)] = {
			winner = row.Winner,
			team1 = h.getTeamCargoFromRow(row, 1),
			team2 = h.getTeamCargoFromRow(row, 2),
		}
	end
	return processed
end

function h.getTeamCargoFromRow(row, i)
	local ret = {
		score = row['Team' .. i .. 'Score'] and { row['Team' .. i .. 'Score'] },
		team = row['Team' .. i],
		teamfinal = row['Team' .. i .. 'Final'],
		player = row['Player' .. i],
		iswinner = row.Winner == i,
		bestof = row.BestOf,
	}
	return ret
end

function h.sortFF(row)
	if row.FF == 1 then
		row.Team1Score = 'FF'
		row.Team2Score = 'W'
	elseif row.FF == 2 then
		row.Team1Score = 'W'
		row.Team2Score = 'FF'
	end
end

function h.scoreUnknown(row)
	if row.WinnerScoreUnknown == 1 then
		row.Team1Score = 'W'
		row.Team2Score = 'L'
		row.Winner = 1
	elseif row.WinnerScoreUnknown == 2 then
		row.Team1Score = 'L'
		row.Team2Score = 'W'
		row.Winner = 2
	end
end	

function h.addProcessedToArgs(args, settings, processed, overviewPage)
	for r, col in ipairs(settings) do
		h.initializeMatch(args, r)
		local title = args[r] and args[r].title or col.matches.title or ''
		for m, _ in ipairs(col.matches) do
			h.initializeMatch(args, r, m)
			local argmatch = args[r] and args[r][m]
			if argmatch and argmatch.cargomatch then
				h.addMatchCargoToMatch(argmatch, processed[argmatch.cargomatch])
			else
				-- the uniquematch does NOT include page number in it
				local uniquematch = ('%s_%s'):format(title, m)
				if not argmatch then
					h.initializeMatch(args, r, m)
					argmatch = args[r][m]
				end
				h.addMatchCargoToMatch(argmatch, processed[uniquematch])
			end
		end
	end
end

function h.addMatchCargoToMatch(argMatch, cargoDataMatch)
	if not cargoDataMatch then
		return
	end
	-- allow arg data to overwrite cargo data always if applicable
	argMatch.winner = argMatch.winner or cargoDataMatch.winner
	for _, team in ipairs({ 'team1', 'team2' }) do
		for k, v in pairs(cargoDataMatch[team]) do
			argMatch[team][k] = argMatch[team][k] or v
		end
	end
end

-- print
function h.makeOutput(args, settings)
	local output = mw.html.create()
	if settings.togglers then
		h.printAllBrackets(args, settings, output)
	else
		h.printBracket(args, settings, output:tag('div'), {})
	end
	return output
end

function h.printAllBrackets(args, settings, output)
	local toggleN = util_vars.setGlobalIndex('BracketToggler')
	local togglers = h.makeTogglerButtons(settings.togglers, toggleN)
	local tblRound1 = h.printNextBracketDiv(output, toggleN, 1)
	h.printBracket(args, settings, tblRound1, togglers)
	local tableList = { tblRound1 }
	for i, toggle in ipairs(settings.togglers) do
		h.setupNextToggle(settings, args, togglers, toggle, i)
		local tbl = h.printNextBracketDiv(output, toggleN, i + 1)
		h.printBracket(args, toggle.bracket, tbl, togglers)
		tableList[#tableList+1] = tbl
	end
	h.setTableHidden(tableList, args.initround)
end

function h.setupNextToggle(settings, args, togglers, toggle, i)
	h.fixColumnLabelsForToggle(settings, toggle.bracket, i)
	table.remove(args, 1)
	table.remove(togglers, 1)
	h.processSettings(toggle.bracket, args)
end

function h.fixColumnLabelsForToggle(settings, bracket, i)
	for k, col in ipairs(bracket) do
		col.matches.title = settings[k + i].matches.title
	end
end

function h.printNextBracketDiv(output, toggleN, i)
	local div = output:tag('div')
		:addClass(h.allToggleClass(toggleN, false))
		:addClass(h.roundToggleClass(toggleN, i, false))
	return div
end

function h.allToggleClass(n, isAttr)
	local dot = isAttr and '.' or ''
	return ('%sbracket-toggle-allrounds-%s'):format(dot, n)
end

function h.roundToggleClass(n, i, isAttr)
	local dot = isAttr and '.' or ''
	return ('%sbracket-toggle-round-%s-%s'):format(dot, n, i)
end

function h.makeTogglerButtons(togglers, n)
	local tbl = {}
	tbl[1] = h.makeToggler(n, 1)
	for i, _ in ipairs(togglers) do
		if i == #togglers then
			tbl[#tbl+1] = h.makeLastToggler(n)
		else
			-- first add 1 because we already did 1 from the default bracket
			tbl[#tbl+1] = h.makeToggler(n, i + 1)
		end
	end
	return tbl
end

function h.makeToggler(n, i)
	local div = mw.html.create('div')
		:addClass('bracket-toggler')
		:wikitext('[')
	div:tag('span')
		:addClass('alwaysactive-toggler')
		:attr('data-toggler-hide', h.allToggleClass(n, true))
		:attr('data-toggler-show', h.roundToggleClass(n, i + 1, true))
		:wikitext('x')
	div:wikitext(']')
	return div
end

function h.makeLastToggler(n)
	local div = mw.html.create('div')
		:addClass('bracket-toggler')
	div:tag('span')
		:addClass('alwaysactive-toggler')
		:attr('data-toggler-hide', h.allToggleClass(n, true))
		:attr('data-toggler-show', h.roundToggleClass(n, 1, true))
		:wikitext('<<')
	return div
end

function h.setTableHidden(tableList, initround)
	initround = tonumber(initround or 1) or 1
	for k, tbl in ipairs(tableList) do
		if k ~= initround then
			tbl:addClass('toggle-section-hidden')
		end
	end
end

function h.printBracket(args, settings, tbl, togglers)
	tbl:addClass('bracket-grid')
		:css({
			['grid-template-columns'] = h.getGTC(settings, args),
			['grid-template-rows'] = h.getGTR(settings, args.notitle)
		})
	for r, col in ipairs(settings) do
		CURRENT_ROUND = 'round' .. (r - 1)
		h.addLinesColumn(tbl, col.lines, not args.notitle)
		CURRENT_ROUND = 'round' .. r
		h.addMatchesColumn(tbl, args, col.matches, r, not args.notitle, togglers[r])
	end
	return tbl
end

function h.getGTC(settings, args)
	local scores = {}
	for round, col in ipairs(settings) do
		scores[round] = args[round] and tonumber(args[round].extendedseries or '') or col.extendedseries or 1
	end
	local firstcol = settings[1].lines and next(settings[1].lines)
	local firstwidth = firstcol and LINEWIDTH or '0'
	return h.getCustomGTC(scores, args.roundwidth, args.roundminwidth, firstwidth)
end

function h.getCustomGTC(scores, roundwidth, minwidth, firstwidth)
	local linewidth = minwidth and ' minmax(2em,3em) ' or ' 3em '
	roundwidth = h.getRoundwidth(roundwidth)
	minwidth = h.parseWidth(minwidth) or roundwidth
	local widths = {}
	for k, v in ipairs(scores) do
		local min = (SCOREWIDTH * (v - 1) + minwidth)
		local max = (SCOREWIDTH * (v - 1) + roundwidth)
		widths[#widths+1] = ('minmax(%sem, %sem)'):format(min, max)
	end
	return firstwidth .. ' ' .. table.concat(widths, linewidth)
end

function h.getRoundwidth(roundwidth)
	if roundwidth then
		return h.parseWidth(roundwidth)
	else
		return ROUNDWIDTH
	end
end

function h.parseWidth(width)
	if not width then return nil end
	return tonumber(width:gsub('em','') or '')
end

function h.getGTR(settings, notitle)
	local max = 0
	for _, col in ipairs(settings) do
		local total = 0
		for _, match in ipairs(col.matches) do
			total = total + (match.above or 0)
			if match.display == 'match' then
				total = total + ROWS_PER_TEAM
			elseif match.display == 'hline' then
				total = total + ROWS_PER_HLINE
			end
		end
		if total > max then
			max = total
		end
	end
	if not notitle then max = max + ROWS_PER_TITLE end
	return ('repeat(%s,var(--grid-row-height))'):format(max)
end

function h.addLinesColumn(tbl, lineData, addtitle)
	if not lineData then
		return
	end
	for m, row in ipairs(lineData) do
		if m == 1 and addtitle then
			h.addBracketLine(tbl, row, 2)
		else
			h.addBracketLine(tbl, row, 0)
		end
	end
	return
end

function h.addBracketLine(tbl, linerow, extra)
	if linerow.above + extra > 0 then
		tbl:tag('div')
			:addClass('bracket-line')
			:addClass(CURRENT_ROUND)
			:cssText(('grid-row:span %s;'):format(linerow.above + extra))
	end
	tbl:tag('div')
		:addClass('bracket-line')
		:addClass(linerow.class)
		:addClass(CURRENT_ROUND)
		:cssText(('grid-row:span %s;'):format(linerow.height))
	return
end

function h.addMatchesColumn(tbl, args, data, r, addtitle, toggler)
	if addtitle then
		local title = args[r] and args[r].title or data.title or ''
		h.makeTitle(tbl, title, toggler)
	end
	for m, row in ipairs(data) do
		local game = args[r] and args[r][m] or { team1 = {}, team2 = {} }
		if row.above then
			h.printSpacer(tbl, row.above)
		end
		if row.display == 'match' then
			h.makeMatch(tbl, game, not args.nolabels and row.label)
		elseif row.display == 'hline' then
			h.makeHorizontalCell(tbl)
		end
	end
	return
end

function h.makeTitle(tbl, text, toggler)
	local outerdiv = tbl:tag('div')
		:addClass('bracket-grid-header')
		:addClass(CURRENT_ROUND)
	local innerdiv = outerdiv:tag('div')
		:addClass('bracket-header-content')
		:wikitext(text)
	if toggler then
		innerdiv:node(toggler)
	end
end

function h.makeHorizontalCell(tbl)
	tbl:tag('div')
		:addClass('bracket-spacer')
		:addClass('horizontal')
		:addClass(CURRENT_ROUND)
	return
end

function h.makeMatch(tbl, game, label)
	if game.label then label = game.label end
	h.printSpacer(tbl, nil, label)
	h.printTeam(tbl, game, game.team1)
	h.printTeam(tbl, game, game.team2)
	h.printSpacer(tbl, nil, nil)
	return
end

function h.printSpacer(tbl, above, label)
	local div = tbl:tag('div')
		:addClass('bracket-spacer')
		:addClass(CURRENT_ROUND)
	if label then
		div:wikitext(label)
	end
	if above then
		div:cssText(('grid-row:span %s;'):format(above))
	end
	return
end

function h.printTeam(tbl, game, data)
	local line = tbl:tag('div')
		:addClass('bracket-team')
		:addClass(CURRENT_ROUND)
		:addClass(game.class)
	if not data.bye then
		util_esports.addTeamHighlighter(line, data.player or data.teamfinal or data.team)
	end
	if data.iswinner then
		line:addClass('bracket-winner')
	end
	local team = line:tag('div')
		:addClass('bracket-team-name')
	if data.free then
		team:wikitext(data.free)
	elseif data.bye then
		team:wikitext('BYE')
		line:addClass('bracket-bye')
	else
		bracket_wiki.teamDisplay(team, data, TEAM_STYLE)
	end
	h.printScore(line, data)
end

function h.printScore(line, data)
	if SHOW_BESTOF and not data.score then
		h.printBestof(line, data)
		return
	end
	for i, v in ipairs(data.score or { '' }) do
		local div = line:tag('div')
			:addClass('bracket-team-points')
			:wikitext(v or (data.bye and '-') or '')
		if data.iswinner then
			div:addClass('bracket-score-winner')
		elseif data.iswinners and data.iswinners[i] then
			div:addClass('bracket-score-loser')
		end
	end
end

function h.printBestof(line, data)
	if not SHOW_BESTOF then return end
	if data.bye then return end
	local div = line:tag('div')
		:addClass('bracket-team-points')
		:addClass('bracket-team-bestof')
	if data.bestof then
		div:wikitext(i18n.print('bestof', data.bestof))
	end
end

return p