Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions components/MatchOptions.vue
Original file line numberDiff line numberDiff line change
Expand Up@@ -995,10 +995,10 @@ import { SELECT_NONE, nullableSelectField } from "~/utilities/selectNone";
</FormField>

<p
v-if="selectedGameMode && !selectedGameMode.competitive_safe"
v-if="selectedGameMode"
class="text-sm text-muted-foreground"
>
{{ $t("match.options.game_mode.not_competitive_safe") }}
{{ $t("match.options.game_mode.unranked_note") }}
</p>
</div>
</Card>
Expand DownExpand Up@@ -1524,7 +1524,6 @@ export default {
id: true,
name: true,
enabled: true,
competitive_safe: true,
supported_runtimes: [{}, true],
},
],
Expand DownExpand Up@@ -1556,6 +1555,9 @@ export default {
enabled: {
_eq: true,
},
deleted_at: {
_is_null: true,
},
},
},
mapFields,
Expand DownExpand Up@@ -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<Record<string, any>> {
return (this.gameModes ?? []).filter((mode: Record<string, any>) => {
if (!mode.enabled) {
Expand Down
3 changes: 3 additions & 0 deletions components/charts/LastTenWinsAndLosses.vue
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,6 +106,9 @@ export default {
type: {
_eq: e_map_pool_types_enum.Competitive,
},
deleted_at: {
_is_null: true,
},
},
order_by: [
{
Expand Down
143 changes: 109 additions & 34 deletions components/draft-games/CreateDraftGame.vue
Original file line numberDiff line numberDiff line change
Expand Up@@ -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";
Expand DownExpand Up@@ -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<Array<Record<string, any>>>(() =>
((gameModesResult.value as any)?.game_modes ?? []).filter(
(gameMode: Record<string, any>) =>
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)) {
Expand All@@ -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",
Expand DownExpand Up@@ -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<string | null>(null);
const settingsSnapshot = () =>
JSON.stringify({
Expand DownExpand Up@@ -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;
}

Expand DownExpand Up@@ -639,29 +716,27 @@ const submit = form.handleSubmit(async (values: any) => {
@keydown.capture="interacted = true"
>
<div class="flex items-center gap-2">
<template v-for="(label, index) in steps" :key="label">
<template v-for="({ id, label }, index) in visibleSteps" :key="id">
<button
type="button"
:disabled="stepDisabled(index + 1)"
:title="
stepDisabled(index + 1) ? $t('draft_games.create.no_formats') : ''
"
:disabled="stepDisabled(id)"
:title="stepDisabled(id) ? $t('draft_games.create.no_formats') : ''"
class="step-pill flex flex-1 basis-0 items-center justify-center gap-2 rounded-md border px-3 py-2 text-center transition-colors disabled:cursor-not-allowed disabled:opacity-40"
:class="
stepDisabled(index + 1)
stepDisabled(id)
? 'border-dashed border-border text-muted-foreground/40'
: step === index + 1
: step === id
? 'border-[hsl(var(--tac-amber))] bg-[hsl(var(--tac-amber)/0.1)] text-foreground'
: step > index + 1
: step > id
? 'border-[hsl(var(--tac-amber)/0.4)] text-muted-foreground'
: 'border-border text-muted-foreground/60'
"
@click="goToStep(index + 1)"
@click="goToStep(id)"
>
<span
class="grid h-5 w-5 shrink-0 place-items-center rounded-full border text-[0.6rem] font-bold"
:class="
step >= index + 1 && !stepDisabled(index + 1)
step >= id && !stepDisabled(id)
? 'border-[hsl(var(--tac-amber))] text-[hsl(var(--tac-amber))]'
: 'border-border'
"
Expand All@@ -675,7 +750,7 @@ const submit = form.handleSubmit(async (values: any) => {
</span>
</button>
<div
v-if="index < steps.length - 1"
v-if="index < visibleSteps.length - 1"
class="h-px w-4 shrink-0 bg-border sm:w-8"
></div>
</template>
Expand All@@ -687,7 +762,7 @@ const submit = form.handleSubmit(async (values: any) => {
state. -->
<HeightMorph :state="step" class="step-stack">
<Transition name="step">
<div v-show="step === 3" class="space-y-5">
<div v-show="step === STEP_FORMAT" class="space-y-5">
<section v-if="!isDuel">
<AnimatedFilters
v-model="mode"
Expand DownExpand Up@@ -897,7 +972,7 @@ const submit = form.handleSubmit(async (values: any) => {
</Transition>

<Transition name="step">
<div v-show="step === 2" class="space-y-5">
<div v-show="step === STEP_MODE" class="space-y-5">
<section>
<div :class="tacticalSectionLabelClasses">
<span :class="tacticalSectionTickClasses"></span>
Expand All@@ -908,7 +983,7 @@ const submit = form.handleSubmit(async (values: any) => {
{{ $t("draft_games.create.mode_description") }}
</p>

<DraftModePicker :form="form" />
<DraftModePicker :form="form" :modes="draftEligibleModes" />
</section>
</div>
</Transition>
Expand Down
91 changes: 17 additions & 74 deletions components/draft-games/DraftModePicker.vue
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
<script setup lang="ts">
import { Badge } from "~/components/ui/badge";
import { Check, Puzzle, ShieldAlert } from "lucide-vue-next";
import { computed } from "vue";
import { Check, Puzzle } from "lucide-vue-next";

const props = defineProps<{
form: any;
modes: Array<Record<string, any>>;
}>();

const selected = computed<string>(() => props.form.values.game_mode_id ?? "");

const select = (id: string) => {
props.form.setFieldValue("game_mode_id", id);
};
</script>

<template>
Expand DownExpand Up@@ -53,81 +64,13 @@ import { Check, Puzzle, ShieldAlert } from "lucide-vue-next";
</div>

<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<span class="truncate font-medium">{{ mode.name }}</span>
<Badge
v-if="!mode.competitive_safe"
variant="outline"
class="shrink-0 gap-1 text-[0.6rem]"
>
<ShieldAlert class="h-3 w-3" />
{{ $t("draft_games.bar.unranked") }}
</Badge>
</div>
<p class="truncate text-xs text-muted-foreground">
<p class="font-medium">{{ mode.name }}</p>
<!-- The description is the whole pitch for picking a mode; it wraps
rather than truncating so a long one is still readable. -->
<p class="text-xs leading-snug text-muted-foreground">
{{ mode.description }}
</p>
</div>
</button>

<p v-if="modes.length === 0" class="text-sm text-muted-foreground">
{{ $t("draft_games.create.mode_empty") }}
</p>
</div>
</template>

<script lang="ts">
import { generateQuery } from "~/graphql/graphqlGen";

export default {
props: {
form: {
type: Object,
required: true,
},
},
data() {
return {
gameModes: [] as Array<Record<string, any>>,
};
},
apollo: {
gameModes: {
fetchPolicy: "cache-first",
query: generateQuery({
game_modes: [
{},
{
id: true,
name: true,
description: true,
enabled: true,
competitive_safe: true,
},
],
}),
update(data: { game_modes: Array<Record<string, any>> }) {
return data.game_modes;
},
skip() {
return !useApplicationSettingsStore().gamePluginsEnabled;
},
},
},
methods: {
select(id: string) {
this.form.setFieldValue("game_mode_id", id);
},
},
computed: {
selected(): string {
return this.form.values.game_mode_id ?? "";
},
modes(): Array<Record<string, any>> {
return (this.gameModes ?? []).filter(
(mode: Record<string, any>) => mode.enabled,
);
},
},
};
</script>
Loading
Loading