From 93fcda94c253518fb26bdfe1f94c5fa610956c43 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Thu, 30 Jul 2026 15:34:28 -0400 Subject: [PATCH 1/2] feature: veto timer --- assets/css/tailwind.css | 31 +++++++++++++ components/MatchOptions.vue | 31 +++++++++++++ components/draft-games/DraftClock.vue | 16 +++++-- components/match/MatchMapVeto.vue | 18 ++++++++ components/match/MatchOptionsDisplay.vue | 14 ++++++ components/match/MatchPicksDisplay.vue | 24 ++++++++++ components/match/MatchRegionVeto.vue | 18 ++++++++ components/matchmaking/Matchmaking.vue | 2 +- components/matchmaking/MatchmakingConfirm.vue | 4 +- components/tournament/TournamentStageForm.vue | 42 +++++++++++++++++ graphql/matchOptionsFields.ts | 1 + i18n/locales/en.json | 10 ++++ pages/matches/[id]/index.vue | 1 + pages/settings/application/matchmaking.vue | 46 ++++++++++++++++++- stores/DraftGamesStore.ts | 2 + tailwind.config.js | 5 -- utilities/match-options-validator.ts | 15 ++++++ utilities/setupOptions.ts | 11 +++++ 18 files changed, 279 insertions(+), 12 deletions(-) diff --git a/assets/css/tailwind.css b/assets/css/tailwind.css index af5b092d2..cc35f0bf4 100644 --- a/assets/css/tailwind.css +++ b/assets/css/tailwind.css @@ -351,6 +351,37 @@ mark { transparent 4px ); } +/* Scan sweep across the top of the queue / match-confirm panels. The band is a + repeating gradient whose period is one track width, painted on a double-width + element, so shifting exactly one period (-50%) loops with no seam: the next + band enters the left edge as the previous one leaves the right. Colour is + currentColor, so callers pick it with text-*. */ +@keyframes tac-scan-sweep { + from { + transform: translateX(-50%); + } + to { + transform: translateX(0); + } +} +.tac-scan-sweep { + width: 200%; + background-image: repeating-linear-gradient( + 90deg, + transparent 0, + transparent 12.5%, + currentColor 25%, + transparent 37.5%, + transparent 50% + ); + animation: tac-scan-sweep 2s linear infinite; +} +@media (prefers-reduced-motion: reduce) { + .tac-scan-sweep { + animation: none; + } +} + /* One-shot pulse for clip share buttons — toast is far from the click target, so the button itself flashes amber + scales briefly. Used by MatchHighlightsReel queue/featured share, HighlightCard, and diff --git a/components/MatchOptions.vue b/components/MatchOptions.vue index 2333bd925..b4841f5de 100644 --- a/components/MatchOptions.vue +++ b/components/MatchOptions.vue @@ -807,6 +807,37 @@ import SettingHeader from "~/components/match/SettingHeader.vue"; + + + {{ + $t("match.options.advanced.veto_pick_timeout.label") + }} + + + + + + + + + + + {{ $t("match.options.advanced.veto_pick_timeout.range") }} + + + + + {{ diff --git a/components/draft-games/DraftClock.vue b/components/draft-games/DraftClock.vue index 1bb168f6a..b1468cef6 100644 --- a/components/draft-games/DraftClock.vue +++ b/components/draft-games/DraftClock.vue @@ -7,11 +7,13 @@ const props = withDefaults( total?: number; accent?: string; pulse?: boolean; + compact?: boolean; }>(), { total: 30, accent: "var(--tac-amber)", pulse: false, + compact: false, }, ); @@ -67,7 +69,11 @@ const urgent = computed(() => remaining.value <= 6 && remaining.value > 0); :class="{ pulse: pulse }" :style="{ '--accent': accent }" > - + remaining.value <= 6 && remaining.value > 0);
SEC diff --git a/components/match/MatchMapVeto.vue b/components/match/MatchMapVeto.vue index dfebd4168..6fb807009 100644 --- a/components/match/MatchMapVeto.vue +++ b/components/match/MatchMapVeto.vue @@ -7,6 +7,7 @@ import MapDisplay from "~/components/MapDisplay.vue"; import MapSelector from "~/components/match/MapSelector.vue"; import { Separator } from "~/components/ui/separator"; import MatchPicksDisplay from "~/components/match/MatchPicksDisplay.vue"; +import DraftClock from "~/components/draft-games/DraftClock.vue"; @@ -320,6 +346,7 @@ export default { .string() .default(e_player_roles_enum.user), max_acceptable_latency: z.number().default(100), + veto_pick_timeout: z.number().min(0).max(600).default(60), }), }), ), @@ -331,7 +358,18 @@ export default { immediate: true, handler(newVal: Array<{ name: string; value: string | null }>) { for (const setting of newVal) { - if ( + if (setting.name === "public.veto_pick_timeout") { + // Kept out of the branch below: 0 is a meaningful value here (it + // disables the veto timer) and `|| 100` would swallow it. + const vetoPickTimeout = Number(setting.value); + + (this.form.setFieldValue as any)( + setting.name, + Number.isFinite(vetoPickTimeout) && vetoPickTimeout >= 0 + ? vetoPickTimeout + : 60, + ); + } else if ( setting.name === "public.max_acceptable_latency" || setting.name === "auto_cancel_duration" || setting.name === "live_match_timeout" @@ -503,6 +541,12 @@ export default { name: "live_match_timeout", value: String((this.form.values as any).live_match_timeout), }, + { + name: "public.veto_pick_timeout", + value: String( + (this.form.values as any).public.veto_pick_timeout, + ), + }, ], on_conflict: { constraint: settings_constraint.settings_pkey, diff --git a/stores/DraftGamesStore.ts b/stores/DraftGamesStore.ts index 0fe3f37ec..faeb6cb25 100644 --- a/stores/DraftGamesStore.ts +++ b/stores/DraftGamesStore.ts @@ -275,6 +275,7 @@ export const useDraftGamesStore = defineStore("draft-games", () => { map_veto_type: true, map_veto_picking_lineup_id: true, region_veto_picking_lineup_id: true, + veto_pick_expires_at: true, region_veto_picks: { type: true, region: true, @@ -289,6 +290,7 @@ export const useDraftGamesStore = defineStore("draft-games", () => { regions: true, coaches: true, check_in_setting: true, + veto_pick_timeout: true, type: true, map_pool: { id: true, diff --git a/tailwind.config.js b/tailwind.config.js index f3420e531..7832c39e6 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -139,10 +139,6 @@ module.exports = { from: { strokeDashoffset: "0" }, to: { strokeDashoffset: "calc(var(--flow-length) * -1px)" }, }, - "loading-bar": { - "0%": { transform: "translateX(-100%)" }, - "100%": { transform: "translateX(100%)" }, - }, "fade-in": { from: { opacity: "0", transform: "translateY(5px)" }, to: { opacity: "1", transform: "translateY(0)" }, @@ -174,7 +170,6 @@ module.exports = { "caret-blink": "caret-blink 1.25s ease-out infinite", "spin-smooth": "spin-smooth 1s cubic-bezier(0.4, 0, 0.2, 1) infinite", "topo-flow": "topo-flow linear infinite", - "loading-bar": "loading-bar 2s infinite", "fade-in": "fade-in 0.3s ease-out", "soft-pulse": "soft-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite", "ping-slow": "ping-slow 1.8s cubic-bezier(0.4, 0, 0.6, 1) infinite", diff --git a/utilities/match-options-validator.ts b/utilities/match-options-validator.ts index 0d3c5a2e6..c7889a781 100644 --- a/utilities/match-options-validator.ts +++ b/utilities/match-options-validator.ts @@ -16,6 +16,12 @@ export default function matchOptionsValidator( settings.find((setting) => setting.name === "public.default_models") ?.value === "true"; + const defaultVetoPickTimeout = Number.parseInt( + settings.find((setting) => setting.name === "public.veto_pick_timeout") + ?.value ?? "", + 10, + ); + return z.object({ mr: z.string().default("12"), map_veto: z.boolean().default(true), @@ -24,6 +30,15 @@ export default function matchOptionsValidator( lan: z.boolean().default(false), coaches: z.boolean().default(false), tv_delay: z.number().min(0).max(120).default(115), + veto_pick_timeout: z + .number() + .min(0) + .max(600) + .default( + Number.isNaN(defaultVetoPickTimeout) || defaultVetoPickTimeout < 0 + ? 60 + : defaultVetoPickTimeout, + ), round_restart_delay: z.number().min(0).max(60).nullable().optional(), halftime_pausematch: z.boolean().default(false), knife_round: z.boolean().default(true), diff --git a/utilities/setupOptions.ts b/utilities/setupOptions.ts index 0f3f004e0..4e8839951 100644 --- a/utilities/setupOptions.ts +++ b/utilities/setupOptions.ts @@ -36,6 +36,7 @@ export const setupOptions = ( map_pool_id: options.map_pool.id, regions: selectedRegions, tv_delay: options.tv_delay, + veto_pick_timeout: options.veto_pick_timeout ?? 60, round_restart_delay: options.round_restart_delay ?? null, halftime_pausematch: options.halftime_pausematch ?? false, check_in_setting: options.check_in_setting, @@ -71,6 +72,7 @@ export function setupOptionsVariables( match_mode: string; map_pool_id?: string; tv_delay: number; + veto_pick_timeout: number; round_restart_delay?: number | null; halftime_pausematch?: boolean; map_pool?: { @@ -156,6 +158,13 @@ export function setupOptionsVariables( throw new Error("tv_delay is required"); } + if ( + values.veto_pick_timeout === undefined || + values.veto_pick_timeout === null + ) { + throw new Error("veto_pick_timeout is required"); + } + if ( values.check_in_setting === undefined || values.check_in_setting === null @@ -184,6 +193,7 @@ export function setupOptionsVariables( ready_setting: values.ready_setting, tech_timeout_setting: values.tech_timeout_setting, tv_delay: values.tv_delay, + veto_pick_timeout: values.veto_pick_timeout, round_restart_delay: values.round_restart_delay ?? null, halftime_pausematch: values.halftime_pausematch ?? false, ...(useAuthStore().isRoleAbove(e_player_roles_enum.tournament_organizer) @@ -238,6 +248,7 @@ export function setupOptionsSetMutation(hasMapPoolId: boolean = true) { timeout_setting: $("timeout_setting", "e_timeout_settings_enum!"), tech_timeout_setting: $("tech_timeout_setting", "e_timeout_settings_enum!"), tv_delay: $("tv_delay", "Int!"), + veto_pick_timeout: $("veto_pick_timeout", "Int!"), round_restart_delay: $("round_restart_delay", "Int"), halftime_pausematch: $("halftime_pausematch", "Boolean!"), ...(useAuthStore().isRoleAbove(e_player_roles_enum.tournament_organizer) From 3ce23f96436f81c08519be71f71c260f8ac052e7 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Tue, 11 Aug 2026 20:38:50 -0400 Subject: [PATCH 2/2] wip --- components/MatchOptions.vue | 110 ++++++++++---- components/draft-games/DraftClock.vue | 46 +----- components/match/MatchAdminBottomBar.vue | 141 +++++++++++------- components/match/MatchMapVeto.vue | 81 +++------- components/match/MatchRegionVeto.vue | 80 +++------- components/match/VetoTurnBanner.vue | 89 +++++++++++ components/tournament/TournamentStageForm.vue | 119 +++++++++++---- composables/useCountdown.ts | 55 +++++++ composables/useVetoOverride.ts | 28 ++++ i18n/locales/en.json | 8 +- pages/gpu-nodes/index.vue | 43 ++++-- utilities/setupOptions.ts | 26 ++-- 12 files changed, 528 insertions(+), 298 deletions(-) create mode 100644 components/match/VetoTurnBanner.vue create mode 100644 composables/useCountdown.ts create mode 100644 composables/useVetoOverride.ts diff --git a/components/MatchOptions.vue b/components/MatchOptions.vue index b4841f5de..847627672 100644 --- a/components/MatchOptions.vue +++ b/components/MatchOptions.vue @@ -807,33 +807,60 @@ import SettingHeader from "~/components/match/SettingHeader.vue"; - + - {{ - $t("match.options.advanced.veto_pick_timeout.label") - }} - - - - - - - - - - - {{ $t("match.options.advanced.veto_pick_timeout.range") }} - +
+ {{ + $t("match.options.advanced.veto_pick_timeout.label") + }} + {{ + $t( + "match.options.advanced.veto_pick_timeout.description", + ) + }} +
+ + + +
+ +
+ + + + + + + + + + + {{ + $t("match.options.advanced.veto_pick_timeout.range") + }} + +
@@ -863,7 +890,9 @@ import SettingHeader from "~/components/match/SettingHeader.vue"; - {{ $t("match.options.advanced.round_restart_delay.range") }} + {{ + $t("match.options.advanced.round_restart_delay.range") + }} @@ -1206,6 +1235,7 @@ import { import { mapFields } from "~/graphql/mapGraphql"; import { useApplicationSettingsStore } from "~/stores/ApplicationSettings"; import { useAuthStore } from "~/stores/AuthStore"; +import { canSetVetoPickTimeout } from "~/utilities/setupOptions"; interface Map { id: string; @@ -1325,6 +1355,7 @@ export default { select_single_region: null as string | null, showAdvancedSettings: false, advHeight: "0px", + lastVetoPickTimeout: 0, }; }, watch: { @@ -1665,6 +1696,9 @@ export default { canSetcheckInSettings() { return useAuthStore().isRoleAbove(e_player_roles_enum.match_organizer); }, + canSetVetoPickTimeout() { + return canSetVetoPickTimeout(); + }, canSetMatchCancellation() { return useAuthStore().isRoleAbove( e_player_roles_enum.tournament_organizer, @@ -1682,8 +1716,32 @@ export default { )?.value; return val || "180"; }, + vetoPickTimeoutDefault(): number { + const val = Number.parseInt( + useApplicationSettingsStore().settings.find( + (s) => s.name === "public.veto_pick_timeout", + )?.value ?? "", + 10, + ); + return Number.isNaN(val) || val <= 0 ? 60 : val; + }, }, methods: { + toggleVetoTimer(enabled: boolean) { + if (!enabled) { + const current = this.form.values.veto_pick_timeout; + if (current > 0) { + this.lastVetoPickTimeout = current; + } + this.form.setFieldValue("veto_pick_timeout", 0); + return; + } + + this.form.setFieldValue( + "veto_pick_timeout", + this.lastVetoPickTimeout || this.vetoPickTimeoutDefault, + ); + }, animateAdvanced(open: boolean) { const wrap = this.$refs.advWrapRef as HTMLElement | undefined; if (!wrap) { diff --git a/components/draft-games/DraftClock.vue b/components/draft-games/DraftClock.vue index b1468cef6..2326b8ad6 100644 --- a/components/draft-games/DraftClock.vue +++ b/components/draft-games/DraftClock.vue @@ -1,5 +1,6 @@ diff --git a/components/match/MatchMapVeto.vue b/components/match/MatchMapVeto.vue index 6fb807009..dd590707e 100644 --- a/components/match/MatchMapVeto.vue +++ b/components/match/MatchMapVeto.vue @@ -1,13 +1,12 @@