diff --git a/components/draft-games/CreateDraftGame.vue b/components/draft-games/CreateDraftGame.vue index 09202b23..7ee8eb7c 100644 --- a/components/draft-games/CreateDraftGame.vue +++ b/components/draft-games/CreateDraftGame.vue @@ -145,6 +145,8 @@ const { result: gameModesResult } = useQuery( description: true, enabled: true, competitive_safe: true, + players_per_team: true, + allow_short_handed_start: true, supported_runtimes: [{}, true], }, ], @@ -450,7 +452,22 @@ const PER_TEAM: Record = { Premier: 5, Faceit: 5, }; -const perTeam = computed(() => PER_TEAM[matchType.value] || 5); + +const selectedGameMode = computed | undefined>(() => + draftEligibleModes.value.find( + (gameMode) => gameMode.id === form.values.game_mode_id, + ), +); + +// A custom mode sizes its own teams; the match type is what everything else +// falls back to. Mirrors resolvePlayersPerTeam on the API, which is what the +// lobby is actually created with. +const perTeam = computed( + () => + selectedGameMode.value?.players_per_team || + PER_TEAM[matchType.value] || + 5, +); // A team lobby only seats `perTeam` players a side, so anyone the host benched // in the roster picker can only reach the match through a substitute slot. diff --git a/components/draft-games/DraftModePicker.vue b/components/draft-games/DraftModePicker.vue index f0657f74..c0f10a04 100644 --- a/components/draft-games/DraftModePicker.vue +++ b/components/draft-games/DraftModePicker.vue @@ -64,7 +64,24 @@ const select = (id: string) => {
-

{{ mode.name }}

+
+

{{ mode.name }}

+ + + {{ mode.players_per_team }}v{{ mode.players_per_team }} + + + {{ $t("draft_games.create.mode_short_handed") }} + +

diff --git a/components/draft-games/DraftRoom.vue b/components/draft-games/DraftRoom.vue index ea5c25b8..10e02b07 100644 --- a/components/draft-games/DraftRoom.vue +++ b/components/draft-games/DraftRoom.vue @@ -5,6 +5,7 @@ import { ArrowUp, Swords, Play, + TriangleAlert, X, MessagesSquare, Inbox, @@ -659,6 +660,12 @@ const regionsAvailable = computed( () => appSettings.availableRegions.length > 0, ); +// Only a custom mode may run a match smaller than its declared size, and only +// when the admin who defined that mode said so. +const shortHandedAllowed = computed( + () => props.room.options?.game_mode?.allow_short_handed_start === true, +); + const startReady = computed(() => { if (props.room.mode === "Teams") { // The match lineups are exactly what the lobby assigned now, so starting @@ -682,6 +689,29 @@ const startReady = computed(() => { return accepted.value.length === props.room.capacity; }); +// What a short-handed mode will accept: a real match on both sides, and -- for a +// captains draft, whose pick order is built on capacity / 2 -- an even pool. +const forceReady = computed(() => { + if (!shortHandedAllowed.value || startReady.value) { + return false; + } + if (props.room.mode === "Teams") { + return !!props.room.team_1_id && team1Count.value > 0 && team2Count.value > 0; + } + if (props.room.mode === "Host") { + return team1Count.value > 0 && team2Count.value > 0; + } + if (isManualCaptains.value && !manualCaptainsReady.value) { + return false; + } + if (props.room.mode === "Captains") { + return accepted.value.length >= 2 && accepted.value.length % 2 === 0; + } + return accepted.value.length >= 2; +}); + +const canStart = computed(() => startReady.value || forceReady.value); + const startHint = computed(() => { if (props.room.mode === "Teams") { return props.room.team_1_id @@ -694,11 +724,35 @@ const startHint = computed(() => { if (isManualCaptains.value && !manualCaptainsReady.value) { return "draft_games.room.pick_captains"; } + if (shortHandedAllowed.value && props.room.mode === "Captains") { + return "draft_games.room.need_even"; + } return "draft_games.room.need_full"; }); +// Starting short is a decision the room cannot walk back -- everyone still on +// their way in loses the seat -- so it asks once. +const confirmingForce = ref(false); + +// Someone joining while the confirm is armed turns this back into an ordinary +// full start; leaving it armed would keep offering to start short. +watch(forceReady, (short) => { + if (!short) { + confirmingForce.value = false; + } +}); + const start = () => { - return runGuarded("start", () => useDraftGamesStore().start(props.room.id)); + if (forceReady.value && !confirmingForce.value) { + confirmingForce.value = true; + return; + } + + confirmingForce.value = false; + + return runGuarded("start", () => + useDraftGamesStore().start(props.room.id, forceReady.value), + ); }; @@ -892,7 +946,7 @@ const start = () => {

@@ -1139,7 +1203,7 @@ const start = () => { appearing under the cursor mid-assignment. -->
@@ -1241,7 +1305,7 @@ const start = () => { diff --git a/components/game-modes/GameModeForm.vue b/components/game-modes/GameModeForm.vue index 6d9c9600..b6fe8694 100644 --- a/components/game-modes/GameModeForm.vue +++ b/components/game-modes/GameModeForm.vue @@ -99,6 +99,57 @@ import { SELECT_NONE, nullableSelectField } from "~/utilities/selectNone";
+ +
+
+
+

+ {{ $t("game_modes.form.players_per_team") }} +

+

+ {{ $t("game_modes.form.players_per_team_description") }} +

+
+ + + + + + + + +
+ +
+
+

+ {{ $t("game_modes.form.allow_short_handed_start") }} +

+

+ {{ $t("game_modes.form.allow_short_handed_start_description") }} +

+
+ + + + + + + +
+
+
{ game_mode: { id: true, name: true, + players_per_team: true, + allow_short_handed_start: true, }, map_pool: { id: true, @@ -497,19 +499,19 @@ export const useDraftGamesStore = defineStore("draft-games", () => { }, }); - const start = (draftGameId: string) => + // Goes through the API rather than setting status directly: a short-handed + // start has to narrow the lobby to the players who turned up before the DB + // trigger and the draft pick pattern read its capacity. + const start = (draftGameId: string, force = false) => getGraphqlClient().mutate({ mutation: gql` - mutation StartDraftGame($draftGameId: uuid!) { - update_draft_games_by_pk( - pk_columns: { id: $draftGameId } - _set: { status: Filled } - ) { - id + mutation StartDraftGame($draftGameId: uuid!, $force: Boolean) { + startDraftGame(draftGameId: $draftGameId, force: $force) { + success } } `, - variables: { draftGameId }, + variables: { draftGameId, force }, }); const pick = (draftGameId: string, steamId: string) =>