diff --git a/lua/wikis/commons/PrizePool/Base.lua b/lua/wikis/commons/PrizePool/Base.lua index 143635d94f4..680786f4314 100644 --- a/lua/wikis/commons/PrizePool/Base.lua +++ b/lua/wikis/commons/PrizePool/Base.lua @@ -10,10 +10,12 @@ local Lua = require('Module:Lua') local Abbreviation = Lua.import('Module:Abbreviation') local Array = Lua.import('Module:Array') local Class = Lua.import('Module:Class') +local DateExt = Lua.import('Module:Date/Ext') local Json = Lua.import('Module:Json') local LeagueIcon = Lua.import('Module:LeagueIcon') local Logic = Lua.import('Module:Logic') local Lpdb = Lua.import('Module:Lpdb') +local MathUtil = Lua.import('Module:MathUtil') local PageVariableNamespace = Lua.import('Module:PageVariableNamespace') local String = Lua.import('Module:StringUtils') local Table = Lua.import('Module:Table') @@ -49,8 +51,6 @@ local BasePrizePool = Class.new(function(self, ...) self:init(...) end) ---@field index integer ---@field data table -local TODAY = os.date('%Y-%m-%d') --[[@as string]] - local LANG = mw.language.getContentLanguage() local DASH = '-' local NON_BREAKING_SPACE = ' ' @@ -89,14 +89,14 @@ BasePrizePool.config = { cutafter = { default = 4, read = function(args) - return tonumber(args.cutafter) + return MathUtil.toInteger(args.cutafter) end }, hideafter = { default = math.huge, read = function(args) - local hideAfter = tonumber(args.hideafter) - local cutAfter = tonumber(args.cutafter) or 4 + local hideAfter = MathUtil.toInteger(args.hideafter) + local cutAfter = MathUtil.toInteger(args.cutafter) or 4 if not hideAfter then return end @@ -386,7 +386,7 @@ function BasePrizePool:init(args) self.args = self:_parseArgs(args) self.pagename = mw.title.getCurrentTitle().text - self.date = BasePrizePool._getTournamentDate() + self.date = DateExt.getContextualDateOrNow() self.opponentType = self.args.type self.options = {} @@ -773,12 +773,9 @@ function BasePrizePool:_currencyExchangeInfo() end -- The exchange date display should not be in the future, as the extension uses current date for those. - local exchangeDate = self.date - if exchangeDate > TODAY then - exchangeDate = TODAY - end - - local exchangeDateText = LANG:formatDate('M j, Y', exchangeDate) + local exchangeDateText = DateExt.formatTimestamp( + 'M j, Y', math.min(DateExt.getCurrentTimestamp(), DateExt.readTimestamp(self.date)) + ) local wrapper = mw.html.create('small') @@ -798,7 +795,7 @@ end ---@return string function BasePrizePool._CurrencyConvertionText(prize) local exchangeRate = BasePrizePool.prizeTypes[PRIZE_TYPE_LOCAL_CURRENCY].convertToBaseCurrency( - prize.data, 1, BasePrizePool._getTournamentDate() + prize.data, 1, DateExt.getContextualDateOrNow() ) return Currency.display(prize.data.currency, 1) .. ' ≃ ' .. @@ -846,12 +843,6 @@ function BasePrizePool:assertOpponentStructType(typeStruct) end end ---- Returns the default date based on wiki-variables set in the Infobox League ----@return string -function BasePrizePool._getTournamentDate() - return Variables.varDefault('tournament_enddate', TODAY) -end - ---@return self function BasePrizePool:storeData() local prizePoolIndex = (tonumber(Variables.varDefault('prizepool_index')) or 0) + 1 diff --git a/lua/wikis/counterstrike/Infobox/League/Custom.lua b/lua/wikis/counterstrike/Infobox/League/Custom.lua index 99a22eca738..a0aa775aa81 100644 --- a/lua/wikis/counterstrike/Infobox/League/Custom.lua +++ b/lua/wikis/counterstrike/Infobox/League/Custom.lua @@ -9,8 +9,10 @@ local Lua = require('Module:Lua') local Array = Lua.import('Module:Array') local Class = Lua.import('Module:Class') -local Logic = Lua.import('Module:Logic') +local FnUtil = Lua.import('Module:FnUtil') local Json = Lua.import('Module:Json') +local Logic = Lua.import('Module:Logic') +local Operator = Lua.import('Module:Operator') local Page = Lua.import('Module:Page') local String = Lua.import('Module:StringUtils') local Table = Lua.import('Module:Table') @@ -30,6 +32,9 @@ local Widgets = Lua.import('Module:Widget/All') local Cell = Widgets.Cell local Title = Widgets.Title local Center = Widgets.Center +local IconFontawesome = Lua.import('Module:Widget/Image/Icon/Fontawesome') +local Link = Lua.import('Module:Widget/Basic/Link') +local WidgetUtil = Lua.import('Module:Widget/Util') ---@class CounterstrikeLeagueInfobox: InfoboxLeague ---@field gameData table @@ -85,11 +90,21 @@ local VALVE_TIERS = { ['rmr event'] = {meta = 'Regional Major Rankings event', name = 'RMR Event', link = 'Regional Major Rankings'}, ['tier 1'] = {meta = 'Valve Tier 1 event', name = 'Tier 1', link = 'Valve Tier 1 Events'}, ['tier 1 qualifier'] = {meta = 'Valve Tier 1 qualifier', name = 'Tier 1 Qualifier', link = 'Valve Tier 1 Events'}, + ['tier 1 wildcard'] = {meta = 'Valve Tier 1 Wildcard event', name = 'Tier 1 Wildcard', link = 'Valve Wildcard Events'}, ['tier 2'] = {meta = 'Valve Tier 2 event', name = 'Tier 2', link = 'Valve Tier 2 Events'}, ['tier 2 qualifier'] = {meta = 'Valve Tier 2 qualifier', name = 'Tier 2 Qualifier', link = 'Valve Tier 2 Events'}, + ['tier 2 wildcard'] = {meta = 'Valve Tier 2 Wildcard event', name = 'Tier 2 Wildcard', link = 'Valve Wildcard Events'}, ['wildcard'] = {meta = 'Valve Wildcard event', name = 'Wildcard', link = 'Valve Wildcard Events'}, } +local VALVE_TOR_START_DATE = '2025-01-01' +local VALVE_TOR_ENABLED_TIERS = { + VALVE_TIERS.major, + VALVE_TIERS['tier 1'], + VALVE_TIERS['tier 1 qualifier'], + VALVE_TIERS['tier 1 wildcard'] +} + local RESTRICTIONS = { female = { name = 'Female Players Only', @@ -170,20 +185,17 @@ function CustomInjector:parse(id, widgets) table.insert(widgets, Center{children = {table.concat(maps, ' • ')}}) end elseif id == 'liquipediatier' then - table.insert( - widgets, + Array.appendWith(widgets, Cell{ name = '[[File:ESL 2019 icon.png|20x20px|link=|ESL|alt=ESL]] Pro Tour Tier', children = {self.caller:_createEslProTierCell(args.eslprotier)}, classes = {'infobox-icon-small'} - } - ) - table.insert( - widgets, + }, Cell{ name = Template.safeExpand(mw.getCurrentFrame(), 'Valve/infobox') .. ' Tier', - children = {self.caller:_createValveTierCell()}, - classes = {'valvepremier-highlighted'} + content = self.caller:_createValveTierCell(), + classes = {'valvepremier-highlighted'}, + options = {separator = ' '} } ) elseif id == 'gamesettings' then @@ -377,10 +389,25 @@ function CustomLeague:_createEslProTierCell(eslProTier) end end ----@return string? +---@return Widget[]? function CustomLeague:_createValveTierCell() if self.valveTier then - return '[[' .. self.valveTier.link .. '|' .. self.valveTier.name .. ']]' + local showInfoIcon = self.data.endDate + and self.data.endDate >= VALVE_TOR_START_DATE + and Array.find(VALVE_TOR_ENABLED_TIERS, FnUtil.curry(Operator.eq, self.valveTier)) + return WidgetUtil.collect( + Link{ + children = {self.valveTier.name}, + link = self.valveTier.link + }, + showInfoIcon and Link{ + children = {IconFontawesome{ + iconName = 'general-info', + hover = 'Click for further details', + }}, + link = '#Valve Operational Requirements' + } or nil + ) end end diff --git a/lua/wikis/counterstrike/MainPageLayout/data.lua b/lua/wikis/counterstrike/MainPageLayout/data.lua index 7fdf0d9e218..525fd1f35f1 100644 --- a/lua/wikis/counterstrike/MainPageLayout/data.lua +++ b/lua/wikis/counterstrike/MainPageLayout/data.lua @@ -18,6 +18,7 @@ local FilterButtonsWidget = Lua.import('Module:Widget/FilterButtons') local ThisDayWidgets = Lua.import('Module:Widget/MainPage/ThisDay') local TransfersList = Lua.import('Module:Widget/MainPage/TransfersList') local WantToHelp = Lua.import('Module:Widget/MainPage/WantToHelp') +local VRSStandings = Lua.import('Module:Widget/VRSStandings') local CONTENT = { @@ -74,6 +75,16 @@ local CONTENT = { padding = true, boxid = MainPageLayoutUtil.BoxId.TOURNAMENTS_TICKER, }, + vrsStandings = { + heading = 'Valve Regional Standings', + body = VRSStandings{ + shouldFetch = 1, + fetchLimit = 5, + mainpage = 1, + }, + padding = false, + boxid = 1521, + } } return { @@ -151,13 +162,17 @@ return { mobileOrder = 1, content = CONTENT.specialEvents, }, + { + mobileOrder = 3, + content = CONTENT.vrsStandings, + }, { mobileOrder = 4, - content = CONTENT.thisDay, + content = CONTENT.transfers, }, { mobileOrder = 5, - content = CONTENT.transfers, + content = CONTENT.thisDay, }, { mobileOrder = 7, diff --git a/lua/wikis/counterstrike/VRSStandings.lua b/lua/wikis/counterstrike/VRSStandings.lua index 6dfcd65f840..c051d73ceb5 100644 --- a/lua/wikis/counterstrike/VRSStandings.lua +++ b/lua/wikis/counterstrike/VRSStandings.lua @@ -30,8 +30,13 @@ local Condition = Lua.import('Module:Condition') local BooleanOperator = Condition.BooleanOperator local Comparator = Condition.Comparator +local Link = Lua.import('Module:Widget/Basic/Link') +local Icon = Lua.import('Module:Icon') -local DATAPOINT_TYPE = 'vrs_ranking' + +local DATAPOINT_TYPE_LIVE = 'vrs_ranking_live' +local DATAPOINT_TYPE_MAIN = 'vrs_ranking' +local FOOTER_LINK = 'Valve_Regional_Standings' ---@class VRSStandings: Widget ---@operator call(table): VRSStandings @@ -39,26 +44,64 @@ local DATAPOINT_TYPE = 'vrs_ranking' local VRSStandings = Class.new(Widget) VRSStandings.defaultProps = { title = 'VRS Standings', + filterRegion = nil, + filterSubregion = nil, + filterCountry = nil, + mainpage = nil, + rankingType = 'live', } ---@return Widget? function VRSStandings:render() local standings, settings = self:_parse() - local headerRow = TableWidgets.TableHeader{children = { - TableWidgets.Row{children = WidgetUtil.collect( + local headerCells + if settings.filterType ~= 'none' then + headerCells = WidgetUtil.collect( + TableWidgets.CellHeader{children = 'Rank'}, + TableWidgets.CellHeader{children = 'Global Rank'}, + TableWidgets.CellHeader{children = 'Points'}, + TableWidgets.CellHeader{children = 'Team'} + ) + else + headerCells = WidgetUtil.collect( TableWidgets.CellHeader{children = 'Rank'}, TableWidgets.CellHeader{children = 'Points'}, TableWidgets.CellHeader{children = 'Team'}, - TableWidgets.CellHeader{children = 'Roster'} - )} - }} + TableWidgets.CellHeader{children = 'Region'} + ) + end + + if not settings.mainpage then + table.insert(headerCells, TableWidgets.CellHeader{children = 'Roster'}) + end + + local headerRow = TableWidgets.TableHeader{ + children = { + TableWidgets.Row{children = headerCells} + } + } + local regionMap = { + AS = 'Asia', + AM = 'Americas', + EU = 'Europe' + } + + local titleName = 'Global' + + if settings.filterType == 'region' then + titleName = regionMap[settings.filterRegion] or settings.filterRegion + elseif settings.filterType == 'subregion' then + titleName = 'Subregion' + elseif settings.filterType == 'country' then + titleName = settings.filterCountryDisplay + end local title = HtmlWidgets.Div { children = { HtmlWidgets.Div { children = { - HtmlWidgets.B { children = 'Unofficial VRS' }, + HtmlWidgets.B { children = 'Unofficial ' .. titleName .. ' VRS' }, HtmlWidgets.Span { children = 'Last updated: ' .. settings.updated } }, classes = { 'ranking-table__top-row-text' } @@ -73,49 +116,134 @@ function VRSStandings:render() classes = { 'ranking-table__top-row' }, } - return TableWidgets.Table{ - title = title, - sortable = false, + local columns + if settings.filterType ~= 'none' then columns = WidgetUtil.collect( - { - align = 'right', - sortType = 'number' - }, - { - align = 'right', - sortType = 'number', - }, - { - align = 'left' - }, - { - align = 'left' + {align = 'center', sortType = 'number'}, + {align = 'center', sortType = 'number'}, + {align = 'center', sortType = 'number'}, + {align = 'left'} + ) + else + columns = WidgetUtil.collect( + {align = 'center', sortType = 'number'}, + {align = 'center', sortType = 'number'}, + {align = 'left'}, + {align = 'center'} + ) + end + + if settings.mainpage then + for _, col in ipairs(columns) do + col.width = (100 / #columns) .. '%' + end + end + + if not settings.mainpage then + table.insert(columns, {align = 'left'}) + end + + local footer = Link { + link = FOOTER_LINK, + linktype = 'internal', + children = { + HtmlWidgets.Div { + children = { 'See Rankings Page', Icon.makeIcon { iconName = 'goto' } }, + classes = { 'ranking-table__footer-button' }, } - ), + }, + } + + if #standings == 0 then + return HtmlWidgets.Div{ + children = { + HtmlWidgets.B{ children = 'No teams found for the selected filter.' } + }, + css = { padding = '12px' } + } + end + + local tableWidget = TableWidgets.Table{ + title = title, + sortable = false, + columns = columns, + footer = settings.mainpage and footer or nil, + css = settings.mainpage and { width = '100%' } or nil, children = { headerRow, - TableWidgets.TableBody{children = Array.map(standings, VRSStandings._row)} + TableWidgets.TableBody{ + children = Array.map(standings, function(entry) + return VRSStandings._row(entry, settings.mainpage) + end) + } }, } + + if settings.mainpage then + return HtmlWidgets.Div{ + css = { width = '100%' }, + children = { tableWidget } + } + else + return tableWidget + end end ---@private ----@return {place: number, points: number, opponent: standardOpponent}[] ----@return {title: string, updated: string, shouldStore: boolean, shouldFetch: boolean} function VRSStandings:_parse() local props = self.props + local rankingType = (props.rankingType == 'main') and 'main' or 'live' + local datapointType = (rankingType == 'main') and DATAPOINT_TYPE_MAIN or DATAPOINT_TYPE_LIVE + local settings = { title = props.title, - updated = DateExt.toYmdInUtc(props.updated or DateExt.getCurrentTimestamp()), shouldFetch = Logic.readBool(props.shouldFetch), - fetchLimit = tonumber(props.fetchLimit) + fetchLimit = tonumber(props.fetchLimit), + filterRegion = props.filterRegion, + filterSubregion = props.filterSubregion, + filterCountry = props.filterCountry, + mainpage = Logic.readBool(props.mainpage), + rankingType = rankingType, + datapointType = datapointType, } + if props.updated == 'latest' or not props.updated then + settings.updated = VRSStandings._fetchLatestDate(datapointType) + else + settings.updated = DateExt.toYmdInUtc(props.updated) + end + + -- Only one filter can be applied at once + settings.filterType = 'none' + + if settings.filterRegion then + settings.filterType = 'region' + elseif settings.filterSubregion then + settings.filterType = 'subregion' + elseif settings.filterCountry then + settings.filterType = 'country' + end + + settings.filterCountries = nil + settings.filterCountryDisplay = 'Filtered' + + if settings.filterCountry then + local rawList = mw.text.split(settings.filterCountry, ',') + local countrySet = {} + + for _, raw in ipairs(rawList) do + countrySet[mw.text.trim(raw)] = true + end + + settings.filterCountries = countrySet + settings.filterCountryDisplay = #rawList > 1 and 'Filtered' or mw.text.trim(rawList[1]) + end + ---@type {points: number, opponent: standardOpponent}[] local standings = {} if settings.shouldFetch then - standings = self._fetch(settings.updated, settings.fetchLimit) + standings = VRSStandings._fetch(settings.updated, settings.datapointType) else Table.iter.forEachPair(self.props, function(key, value) if not string.match(key, '^%d+$') then @@ -123,92 +251,195 @@ function VRSStandings:_parse() end local data = Json.parse(value) + local opponent = Opponent.readOpponentArgs(Table.merge(data, { type = Opponent.team, })) - -- Remove template from data to not confuse it with first player data[1] = nil - opponent.players = Array.map(Array.range(1, 5), FnUtil.curry(Opponent.readPlayerArgs, data)) + opponent.players = Array.map(Array.range(1,5), FnUtil.curry(Opponent.readPlayerArgs, data)) + + opponent.extradata = opponent.extradata or {} + opponent.extradata.region = data.region + opponent.extradata.subregion = data.subregion + opponent.extradata.country = data.country - table.insert(standings, { + table.insert(standings,{ place = tonumber(key), points = tonumber(data.points), opponent = opponent }) end) - self._store(settings.updated, standings) + VRSStandings._store(settings.updated, settings.datapointType, standings) end Array.sortInPlaceBy(standings, Operator.property('place')) + if settings.filterType ~= 'none' then + for i, entry in ipairs(standings) do + entry.global_place = i + end + end + -- filtering + standings = Array.filter(standings, function(entry) + local extradata = entry.opponent.extradata or {} + + if settings.filterType == 'region' then + return extradata.region == settings.filterRegion + end + + if settings.filterType == 'subregion' then + return extradata.subregion == settings.filterSubregion + end + + if settings.filterType == 'country' then + local matchingPlayers = Array.filter(entry.opponent.players, function(player) + return player ~= nil + and player.flag ~= nil + and settings.filterCountries[player.flag] == true + end) + return #matchingPlayers >= 3 + end + + return true + end) + + if settings.fetchLimit then + standings = Array.sub(standings, 1, settings.fetchLimit) + end + + for i, entry in ipairs(standings) do + entry.place = i + end + return standings, settings end ---@private ----@param standing {place: number, points: number, opponent: standardOpponent} ----@return Widget -function VRSStandings._row(standing) - return TableWidgets.Row{children = WidgetUtil.collect( - TableWidgets.Cell{ - children = standing.place, - }, - TableWidgets.Cell{ - children = MathUtil.formatRounded{value = standing.points, precision = 1} - }, - TableWidgets.Cell{ - children = OpponentDisplay.InlineTeamContainer{ - template = standing.opponent.template +function VRSStandings._row(standing, mainpage) + local extradata = standing.opponent.extradata or {} + + local cells + if standing.global_place then + cells = WidgetUtil.collect( + TableWidgets.Cell{children = standing.place}, + TableWidgets.Cell{children = standing.global_place}, + TableWidgets.Cell{ + children = MathUtil.formatRounded{value = standing.points, precision = 1} + }, + TableWidgets.Cell{ + children = OpponentDisplay.InlineTeamContainer{ + template = standing.opponent.template + } } - }, - TableWidgets.Cell{ - children = Array.map(standing.opponent.players, function(player) - return HtmlWidgets.Span{ - css = { - display = "inline-block", - width = "160px" - }, - children = PlayerDisplay.InlinePlayer({player = player}) + ) + else + cells = WidgetUtil.collect( + TableWidgets.Cell{children = standing.place}, + TableWidgets.Cell{ + children = MathUtil.formatRounded{value = standing.points, precision = 1} + }, + TableWidgets.Cell{ + children = OpponentDisplay.InlineTeamContainer{ + template = standing.opponent.template } - end), - } - )} + }, + TableWidgets.Cell{children = extradata.region or ''} + ) + end + + if not mainpage then + table.insert(cells, + TableWidgets.Cell{ + children = Array.map(standing.opponent.players,function(player) + return HtmlWidgets.Span{ + css = {display="inline-block", width="160px"}, + children = PlayerDisplay.InlinePlayer({player = player}) + } + end) + } + ) + end + + return TableWidgets.Row{children = cells} end ---@private ----@param updated string ----@param standings {place: number, points: number, opponent: standardOpponent}[] -function VRSStandings._store(updated, standings) +function VRSStandings._store(updated, datapointType, standings) if Lpdb.isStorageDisabled() then return end + local dataPoint = Lpdb.DataPoint:new{ - objectname = 'vrs_' .. updated, - type = DATAPOINT_TYPE, + objectname = datapointType .. '_' .. updated, + type = datapointType, name = 'Inofficial VRS (' .. updated .. ')', date = updated, extradata = standings } + dataPoint:save() end ---@private ----@param updated string ----@param fetchLimit integer ----@return {place: number, points: number, opponent: standardOpponent}[] -function VRSStandings._fetch(updated, fetchLimit) +function VRSStandings._fetch(updated, datapointType) + local conditions = Condition.Tree(BooleanOperator.all):add{ + Condition.Node(Condition.ColumnName('date'), Comparator.eq, updated), + Condition.Node(Condition.ColumnName('namespace'), Comparator.eq, 0), + } + + if datapointType == DATAPOINT_TYPE_MAIN then + conditions:add{ + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN), + } + else + conditions:add{ + Condition.Tree(BooleanOperator.any):add{ + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_LIVE), + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN), + } + } + end + local data = mw.ext.LiquipediaDB.lpdb('datapoint', { - conditions = Condition.Tree(BooleanOperator.all):add{ - Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE), - Condition.Node(Condition.ColumnName('date'), Comparator.eq, updated), - }:toString(), + conditions = conditions:toString(), query = 'extradata', limit = 1, }) - assert(data[1], 'No VRS data found') - return Array.sub(data[1].extradata, 1, fetchLimit) + assert(data[1], 'No VRS data found for type "' .. datapointType .. '" on date "' .. updated .. '"') + return data[1].extradata +end + +---@private +function VRSStandings._fetchLatestDate(datapointType) + local conditions = Condition.Tree(BooleanOperator.all):add{ + Condition.Node(Condition.ColumnName('namespace'), Comparator.eq, 0), + } + + if datapointType == DATAPOINT_TYPE_MAIN then + conditions:add{ + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN), + } + else + conditions:add{ + Condition.Tree(BooleanOperator.any):add{ + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_LIVE), + Condition.Node(Condition.ColumnName('type'), Comparator.eq, DATAPOINT_TYPE_MAIN), + } + } + end + + local data = mw.ext.LiquipediaDB.lpdb('datapoint', { + conditions = conditions:toString(), + query = 'date', + order = 'date desc', + limit = 1, + }) + + assert(data[1], 'No VRS data found for type "' .. datapointType .. '"') + return DateExt.toYmdInUtc(DateExt.parseIsoDate(data[1].date)) end return VRSStandings