diff --git a/lua/wikis/commons/Icon/Data.lua b/lua/wikis/commons/Icon/Data.lua index d52e1b27d34..b3fad44a647 100644 --- a/lua/wikis/commons/Icon/Data.lua +++ b/lua/wikis/commons/Icon/Data.lua @@ -36,7 +36,7 @@ return { defuse = 'fas fa-wrench', outoftime = 'fas fa-hourglass', ace_valorant = 'fas fa-dagger', - round_winner = 'far fa-trophy-alt', + flawless_valorant = 'fas fa-gem', -- Usage: Rumors, Predictions, etc. correct = 'fas fa-check', diff --git a/lua/wikis/valorant/MatchGroup/Input/Custom.lua b/lua/wikis/valorant/MatchGroup/Input/Custom.lua index 0a220fb2a08..3ee7fb41410 100644 --- a/lua/wikis/valorant/MatchGroup/Input/Custom.lua +++ b/lua/wikis/valorant/MatchGroup/Input/Custom.lua @@ -42,7 +42,9 @@ local VALORANT_REGIONS = {'eu', 'na', 'ap', 'kr', 'latam', 'br', 'pbe1', 'esport ---@field t1side ValorantSides ---@field t2side ValorantSides ---@field winningSide ValorantSides +---@field flawless boolean ---@field ceremony string +---@field ceremonyFor string? ---@class ValorantMapParserInterface ---@field getMap fun(mapInput: table): table @@ -207,6 +209,7 @@ function MatchFunctions.calculateOverallStatsForOpponent(maps, opponentIndex) return { firstKills = getSumOf('firstKills'), + flawless = getSumOf('flawless'), thrifties = getSumOf('thrifties'), clutches = getSumOf('clutches'), postPlant = postPlant, diff --git a/lua/wikis/valorant/MatchGroup/Input/Custom/MatchPage.lua b/lua/wikis/valorant/MatchGroup/Input/Custom/MatchPage.lua index d445c871d73..5f7a7fa3608 100644 --- a/lua/wikis/valorant/MatchGroup/Input/Custom/MatchPage.lua +++ b/lua/wikis/valorant/MatchGroup/Input/Custom/MatchPage.lua @@ -222,6 +222,31 @@ function CustomMatchGroupInputMatchPage.getRounds(map) return ceremonyCode:sub(9) end + ---@param ceremony string? + ---@param roundKills valorantMatchApiRoundKill[] + ---@param winningTeam integer + ---@return string? + local function processCeremony(ceremony, roundKills, winningTeam) + if ceremony == 'Clutch' then + local killsFromWinningTeam = Array.filter( + roundKills, + function (roundKill) + return Table.includes(map.teams[winningTeam].puuids, roundKill.killer) + end + ) + return killsFromWinningTeam[#killsFromWinningTeam].killer + elseif ceremony == 'Ace' then + local _, killsByPlayer = Array.groupBy(roundKills, function (roundKill) + return roundKill.killer + end) + for killer, kills in pairs(killsByPlayer) do + if #kills >= 5 then + return killer + end + end + end + end + local t1start = CustomMatchGroupInputMatchPage.getFirstSide(map, 1, 'normal') local t1startot = CustomMatchGroupInputMatchPage.getFirstSide(map, 1, 'ot') local nextOvertimeSide = t1startot @@ -249,20 +274,30 @@ function CustomMatchGroupInputMatchPage.getRounds(map) return nil end - ---@type valorantMatchApiRoundKill - local firstKill = Array.min( + ---@type valorantMatchApiRoundKill[] + local roundKills = Array.sortBy( Array.flatMap(round.player_stats, function (player) return player.kills or {} end), - function (kill, fastestKill) - return (kill.time_since_round_start_millis or math.huge) < ( - fastestKill.time_since_round_start_millis or math.huge) + Operator.property('time_since_round_start_millis') + ) + + local winningTeam = (t1side == makeShortSideName(round.winning_team_role)) and 1 or 2 + local ceremony = mapCeremonyCodes(round.round_ceremony) + local flawless = Array.all( + roundKills, + function (roundKill) + return Table.includes(map.teams[winningTeam].puuids, roundKill.killer) end ) + ---@type valorantMatchApiRoundKill + local firstKill = roundKills[1] + ---@type ValorantRoundData return { - ceremony = mapCeremonyCodes(round.round_ceremony), + ceremony = ceremony, + ceremonyFor = processCeremony(ceremony, roundKills, winningTeam), firstKill = Logic.isNotEmpty(firstKill) and { killer = firstKill.killer, victim = firstKill.victim, @@ -270,6 +305,7 @@ function CustomMatchGroupInputMatchPage.getRounds(map) } or {}, planted = round.plant_round_time > 0, defused = round.defuse_round_time > 0, + flawless = flawless, round = roundNumber, t1side = t1side, t2side = t2side, @@ -302,16 +338,23 @@ function CustomMatchGroupInputMatchPage.extendMapOpponent(map, opponentIndex) return round[teamSideKey] == 'atk' and round.planted end) + ---@param ceremony string + ---@return integer + local function countCeremonies(ceremony) + return #Array.filter(rounds, function (round) + return round[teamSideKey] == round.winningSide and round.ceremony == ceremony + end) + end + return { - thrifties = #Array.filter(rounds, function (round) - return round[teamSideKey] == round.winningSide and round.ceremony == 'Thrifty' + thrifties = countCeremonies('Thrifty'), + flawless = #Array.filter(rounds, function (round) + return round[teamSideKey] == round.winningSide and round.flawless end), firstKills = #Array.filter(rounds, function (round) return round.firstKill.byTeam == opponentIndex end), - clutches = #Array.filter(rounds, function (round) - return round[teamSideKey] == round.winningSide and round.ceremony == 'Clutch' - end), + clutches = countCeremonies('Clutch'), postPlant = { #Array.filter(plantedRounds, function (round) return round.winningSide == 'atk' diff --git a/lua/wikis/valorant/MatchPage.lua b/lua/wikis/valorant/MatchPage.lua index a53d0e9e41f..fc49e03aca5 100644 --- a/lua/wikis/valorant/MatchPage.lua +++ b/lua/wikis/valorant/MatchPage.lua @@ -346,6 +346,12 @@ function MatchPage:_renderTeamStats(game) team1Value = game.teams[1].thrifties, team2Value = game.teams[2].thrifties }, + { + icon = IconFa{iconName = 'flawless_valorant'}, + name = 'Flawless', + team1Value = game.teams[1].flawless, + team2Value = game.teams[2].flawless, + }, { icon = IconImage{ imageLight = 'VALORANT Spike lightmode.png', @@ -377,6 +383,9 @@ end ---@param puuid string ---@return {player: string, displayName: string}? function MatchPage._findPlayerByPuuid(game, puuid) + if Logic.isEmpty(puuid) then + return + end for _, opponent in ipairs(game.opponents) do for _, player in ipairs(opponent.players) do if player.puuid == puuid then @@ -409,6 +418,8 @@ MatchPage._displayCeremony = FnUtil.memoize(function (ceremony) imageDark = 'VALORANT Creds darkmode.png', size = '16px', } + elseif ceremony == 'Flawless' then + return IconFa{iconName = 'flawless_valorant'} end end @@ -452,6 +463,7 @@ end ---@return Widget function MatchPage:_renderRoundDetail(findPlayer, round, roundIndex) local firstKillPlayer = findPlayer(round.firstKill.killer) or {} + local ceremonyPlayer = findPlayer(round.ceremonyFor) local roundWinType = WIN_TYPES[round.winBy] or {} return Div{ @@ -496,7 +508,13 @@ function MatchPage:_renderRoundDetail(findPlayer, round, roundIndex) ' ', Link{link = firstKillPlayer.player, children = firstKillPlayer.displayName} }}, - MatchPage._displayCeremony(round.ceremony) + Span{children = WidgetUtil.collect( + MatchPage._displayCeremony(Logic.emptyOr(round.ceremony, round.flawless and 'Flawless' or nil)), + ceremonyPlayer and { + ' ', + Link{link = ceremonyPlayer.player, children = ceremonyPlayer.displayName} + } or nil + )} } }