diff --git a/components/MatchOptions.vue b/components/MatchOptions.vue index 841781e3d..55a11723e 100644 --- a/components/MatchOptions.vue +++ b/components/MatchOptions.vue @@ -995,10 +995,10 @@ import { SELECT_NONE, nullableSelectField } from "~/utilities/selectNone";

- {{ $t("match.options.game_mode.not_competitive_safe") }} + {{ $t("match.options.game_mode.unranked_note") }}

@@ -1524,7 +1524,6 @@ export default { id: true, name: true, enabled: true, - competitive_safe: true, supported_runtimes: [{}, true], }, ], @@ -1556,6 +1555,9 @@ export default { enabled: { _eq: true, }, + deleted_at: { + _is_null: true, + }, }, }, mapFields, @@ -1942,9 +1944,9 @@ export default { return allowsGameMode(); }, // Filtered only by what the server could actually load. competitive_safe is - // not a filter here: every match type counts toward ELO, so hiding unsafe - // modes by type would hide them everywhere. It drives the warning below - // instead, and the hard guarantee lives on servers.type = 'Ranked'. + // not a filter here: it curates which modes draft lobbies offer, nothing + // more. Any mode makes the match unranked (a DB trigger stamps + // counts_toward_ranking at creation), which is what the note below says. availableGameModes(): Array> { return (this.gameModes ?? []).filter((mode: Record) => { if (!mode.enabled) { diff --git a/components/charts/LastTenWinsAndLosses.vue b/components/charts/LastTenWinsAndLosses.vue index c230f717c..bcb6da492 100644 --- a/components/charts/LastTenWinsAndLosses.vue +++ b/components/charts/LastTenWinsAndLosses.vue @@ -106,6 +106,9 @@ export default { type: { _eq: e_map_pool_types_enum.Competitive, }, + deleted_at: { + _is_null: true, + }, }, order_by: [ { diff --git a/components/draft-games/CreateDraftGame.vue b/components/draft-games/CreateDraftGame.vue index 1e790264c..09202b237 100644 --- a/components/draft-games/CreateDraftGame.vue +++ b/components/draft-games/CreateDraftGame.vue @@ -17,9 +17,15 @@ import { X, } from "lucide-vue-next"; import { e_player_roles_enum } from "~/generated/zeus"; +import { useQuery } from "@vue/apollo-composable"; import { toTypedSchema } from "~/utilities/vee-validate-zod"; import matchOptionsValidator from "~/utilities/match-options-validator"; -import { setupOptions, setupOptionsVariables } from "~/utilities/setupOptions"; +import { + canSetGameMode, + setupOptions, + setupOptionsVariables, +} from "~/utilities/setupOptions"; +import { generateQuery } from "~/graphql/graphqlGen"; import { useApplicationSettingsStore } from "~/stores/ApplicationSettings"; import { HeightMorph, HeightSwap, Fold } from "~/components/ui/transitions"; import { useDraftGamesStore } from "~/stores/DraftGamesStore"; @@ -121,16 +127,62 @@ const team2Selection = computed(() => team2Id.value ? { id: team2Id.value, name: source?.team_2?.name } : null, ); +// A draft room only offers modes flagged for draft lobbies (competitive_safe +// curates this list, nothing else -- custom modes never count toward ELO). +// Otherwise mirrors MatchOptions's "available" rule: enabled, and loadable on +// the server runtime. +const applicationSettings = useApplicationSettingsStore(); +const modesQueryEnabled = computed( + () => canSetGameMode() && applicationSettings.gamePluginsEnabled, +); +const { result: gameModesResult } = useQuery( + generateQuery({ + game_modes: [ + {}, + { + id: true, + name: true, + description: true, + enabled: true, + competitive_safe: true, + supported_runtimes: [{}, true], + }, + ], + }), + null, + () => ({ fetchPolicy: "cache-first", enabled: modesQueryEnabled.value }), +); +const draftEligibleModes = computed>>(() => + ((gameModesResult.value as any)?.game_modes ?? []).filter( + (gameMode: Record) => + gameMode.enabled && + gameMode.competitive_safe && + (gameMode.supported_runtimes ?? []).includes( + applicationSettings.gameServerPluginRuntime, + ), + ), +); +// With nothing but Competitive to pick, the step is a page with one answer -- +// drop it from the flow rather than make the host click through it. +const hasModeStep = computed(() => draftEligibleModes.value.length > 0); + +const STEP_MODE = 2; +const STEP_FORMAT = 3; const step = ref(1); const steps = [ - "draft_games.create.step_settings", - "draft_games.create.step_mode", - "draft_games.create.step_format", - "draft_games.create.step_rules", + { id: 1, label: "draft_games.create.step_settings" }, + { id: STEP_MODE, label: "draft_games.create.step_mode" }, + { id: STEP_FORMAT, label: "draft_games.create.step_format" }, + { id: 4, label: "draft_games.create.step_rules" }, ]; -// Format is the step a 1v1 has nothing to choose in; it moved from 2 to 3 when -// mode was inserted ahead of it. -const stepDisabled = (n: number) => n === 3 && isDuel.value; +const visibleSteps = computed(() => + steps.filter((s) => s.id !== STEP_MODE || hasModeStep.value), +); +// Format is the step a 1v1 has nothing to choose in. +const stepDisabled = (n: number) => n === STEP_FORMAT && isDuel.value; +const reachableSteps = computed(() => + visibleSteps.value.map((s) => s.id).filter((id) => !stepDisabled(id)), +); const goToStep = (n: number) => { if (stepDisabled(n)) { @@ -140,24 +192,26 @@ const goToStep = (n: number) => { }; const next = () => { - let target = step.value + 1; - while (target <= steps.length && stepDisabled(target)) { - target++; - } - if (target <= steps.length) { + const target = reachableSteps.value.find((id) => id > step.value); + if (target) { step.value = target; } }; const prev = () => { - let target = step.value - 1; - while (target >= 1 && stepDisabled(target)) { - target--; - } - if (target >= 1) { + const target = [...reachableSteps.value] + .reverse() + .find((id) => id < step.value); + if (target) { step.value = target; } }; +watch(hasModeStep, (visible) => { + if (!visible && step.value === STEP_MODE) { + next(); + } +}); + const modeOptions = [ { value: "Pug", @@ -309,6 +363,29 @@ const form = useForm({ }); validatorComponent.form = form; +// Once the list has settled, a rehosted/edited room carrying a mode that is no +// longer offered here (archived, pulled from draft lobbies, role lost) falls +// back to Competitive instead of silently submitting the stale id. Settled +// means the settings too: the list is filtered on the plugin runtime, which +// is a default until the settings subscription delivers. +const modesSettled = computed( + () => + applicationSettings.settingsLoaded && + (!modesQueryEnabled.value || gameModesResult.value !== undefined), +); +watch( + [modesSettled, draftEligibleModes, () => form.values.game_mode_id], + ([settled, modes, selected]) => { + if ( + settled && + selected && + !modes.some((gameMode) => gameMode.id === selected) + ) { + form.setFieldValue("game_mode_id", ""); + } + }, +); + const editBaseline = ref(null); const settingsSnapshot = () => JSON.stringify({ @@ -530,8 +607,8 @@ const submit = form.handleSubmit(async (values: any) => { title: t("common.error"), description: t("draft_games.create.team_1_required_error"), }); - // Step 3 -- where the team selector the message is about actually lives. - step.value = 3; + // Format -- where the team selector the message is about actually lives. + step.value = STEP_FORMAT; return; } @@ -639,29 +716,27 @@ const submit = form.handleSubmit(async (values: any) => { @keydown.capture="interacted = true" >
-