Team map list (#2935)
Some checks are pending
E2E Tests / e2e (push) Waiting to run
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run

This commit is contained in:
Kalle
2026-04-03 17:51:26 +03:00
committed by GitHub
parent 366481708c
commit 911077c09c
56 changed files with 481 additions and 59 deletions

View File

@@ -178,7 +178,7 @@ const basicSeeds = (variation?: SeedVariation | null) => [
adminUserWeaponPool,
adminUserWidgets,
userProfiles,
userMapModePreferences,
variation === "TEAM_MAP_PREFS" ? undefined : userMapModePreferences,
userQWeaponPool,
seedingSkills,
lastMonthsVoting,
@@ -230,7 +230,7 @@ const basicSeeds = (variation?: SeedVariation | null) => [
arts,
commissionsOpen,
playedMatches,
variation === "NO_SQ_GROUPS" ? undefined : groups,
variation === "NO_SQ_GROUPS" ? undefined : () => groups(variation),
friendCodes,
lfgPosts,
variation === "NO_SCRIMS" ? undefined : scrimPosts,
@@ -2099,6 +2099,20 @@ const detailedTeam = (seedVariation?: SeedVariation | null) => () => {
)
.run();
}
const teamPreferences: UserMapModePreferences = {
modes: modesShort.map((mode) => ({ mode, preference: "PREFER" as const })),
pool: modesShort.map((mode) => ({
mode,
stages: [...SENDOUQ_DEFAULT_MAPS[mode]],
})),
};
sql
.prepare(
/*sql*/ `update "AllTeam" set "mapModePreferences" = ? where "id" = 1`,
)
.run(JSON.stringify(teamPreferences));
};
function otherTeams() {
@@ -2456,7 +2470,11 @@ function commissionsOpen() {
}
const SENDOU_IN_FULL_GROUP = true;
async function groups() {
async function groups(variation?: SeedVariation | null) {
if (variation === "TEAM_MAP_PREFS") {
return teamMapPrefsGroups();
}
const users = userIdsInAscendingOrderById()
.slice(0, 100)
.filter((id) => id !== ADMIN_ID && id !== NZAP_TEST_ID);
@@ -2498,6 +2516,65 @@ async function groups() {
}
}
async function teamMapPrefsGroups() {
const arMemberIds = (
sql
.prepare(
/*sql*/ `select "userId" from "AllTeamMember" where "teamId" = 1 and "leftAt" is null and "userId" != ?`,
)
.all(ADMIN_ID) as any[]
).map((row: any) => row.userId as number);
const arMemberSet = new Set(arMemberIds);
const users = userIdsInAscendingOrderById()
.slice(0, 100)
.filter(
(id) => id !== ADMIN_ID && id !== NZAP_TEST_ID && !arMemberSet.has(id),
);
const nzapGroup = await SQGroupRepository.createGroup({
status: "ACTIVE",
userId: NZAP_TEST_ID,
});
for (let j = 0; j < 3; j++) {
sql
.prepare(
/* sql */ `
insert into "GroupMember" ("groupId", "userId", "role")
values (@groupId, @userId, @role)
`,
)
.run({
groupId: nzapGroup.id,
userId: users.pop()!,
role: "REGULAR",
});
}
const adminGroup = await SQGroupRepository.createGroup({
status: "ACTIVE",
userId: ADMIN_ID,
});
for (const memberId of arMemberIds) {
sql
.prepare(
/* sql */ `
insert into "GroupMember" ("groupId", "userId", "role")
values (@groupId, @userId, @role)
`,
)
.run({
groupId: adminGroup.id,
userId: memberId,
role: "REGULAR",
});
}
sql
.prepare(/*sql*/ `update "Group" set "teamId" = 1 where "id" = ?`)
.run(adminGroup.id);
}
const randomMapList = (
groupAlpha: number,
groupBravo: number,
@@ -3111,7 +3188,7 @@ async function friendships(variation?: SeedVariation | null) {
.run({ userOneId, userTwoId });
}
if (variation === "NO_SQ_GROUPS") return;
if (variation === "NO_SQ_GROUPS" || variation === "TEAM_MAP_PREFS") return;
for (const friendId of SENDOU_FRIEND_IDS_IN_LOOKING_GROUPS) {
const group = await SQGroupRepository.createGroup({

View File

@@ -86,6 +86,7 @@ export interface Team {
inviteCode: string;
name: string;
bsky: string | null;
mapModePreferences: JSONColumnTypeNullable<UserMapModePreferences>;
/** Team's tag, typically used in-game in front of users' names to indicate they are a member of the team. */
tag: string | null;
}
@@ -289,7 +290,11 @@ export type ParsedMemento = {
>;
/** mapPreferences of season 2 */
mapPreferences?: Array<{ userId: number; preference?: Preference }[]>;
pools: Array<{ userId: number; pool: UserMapModePreferences["pool"] }>;
pools: Array<{
userId: number;
pool: UserMapModePreferences["pool"];
teamName?: string;
}>;
};
export interface GroupMatch {

View File

@@ -6,5 +6,6 @@ export const SEED_VARIATIONS = [
"NZAP_IN_TEAM",
"NO_SCRIMS",
"NO_SQ_GROUPS",
"TEAM_MAP_PREFS",
"FINALIZED_BRACKET",
] as const;

View File

@@ -347,14 +347,19 @@ export function compareMatchToReportedScores({
return "SAME";
}
type GroupPreference = {
userId: number;
preferences: UserMapModePreferences;
teamName?: string | null;
};
type CreateMatchMementoArgs = {
own: {
group: SQUncensoredGroup;
preferences: { userId: number; preferences: UserMapModePreferences }[];
preferences: GroupPreference[];
};
their: {
group: SQUncensoredGroup;
preferences: { userId: number; preferences: UserMapModePreferences }[];
preferences: GroupPreference[];
};
mapList: TournamentMapListMap[];
};
@@ -440,6 +445,7 @@ function poolsMemento(args: CreateMatchMementoArgs): ParsedMemento["pools"] {
return {
userId: p.userId,
pool,
...(p.teamName ? { teamName: p.teamName } : {}),
};
});
}

View File

@@ -1293,17 +1293,49 @@ function MapListMapPickInfo({
return result;
};
const mapPreferences = data.match.memento?.mapPreferences?.[i];
const showPopover = () => {
// legacy preference system (season 2)
if (mapPreferences && mapPreferences.length > 0) return true;
const sourceTeams = () => {
if (!data.match.memento?.pools) return [];
if (map.source === "DEFAULT") return true;
const pickerGroups = [data.match.groupAlpha, data.match.groupBravo].filter(
(g) => map.source === "BOTH" || String(g.id) === map.source,
);
return sourcePoolMemberIds().length > 0;
const teams: Array<{ name: string; avatarUrl: string | null }> = [];
for (const pickerGroup of pickerGroups) {
for (const poolEntry of data.match.memento.pools) {
if (!poolEntry.teamName) continue;
if (!pickerGroup.members.some((m) => m.id === poolEntry.userId)) {
continue;
}
const modePool = poolEntry.pool.find((p) => p.mode === map.mode);
if (
modePool?.stages.includes(map.stageId) &&
!teams.some((t) => t.name === poolEntry.teamName)
) {
teams.push({
name: poolEntry.teamName,
avatarUrl:
pickerGroup.team?.name === poolEntry.teamName
? pickerGroup.team.avatarUrl
: null,
});
}
}
}
return teams;
};
if (showPopover()) {
const mapPreferences = data.match.memento?.mapPreferences?.[i];
const teams = sourceTeams();
const poolMemberIds = sourcePoolMemberIds();
const showPopover =
(mapPreferences && mapPreferences.length > 0) ||
map.source === "DEFAULT" ||
poolMemberIds.length > 0;
if (showPopover) {
return (
<SendouPopover
popoverClassName="text-main-forced"
@@ -1321,9 +1353,25 @@ function MapListMapPickInfo({
<div className="text-sm text-center text-lighter">
{t("tournament:pickInfo.default.explanation")}
</div>
) : sourcePoolMemberIds().length > 0 ? (
) : teams.length > 0 ? (
<div className="stack sm">
{sourcePoolMemberIds().map((userId) => {
{teams.map((team) => (
<div
key={team.name}
className="stack sm horizontal items-center xs"
>
<Avatar
size="xxs"
url={team.avatarUrl}
identiconInput={team.name}
/>
{team.name}
</div>
))}
</div>
) : poolMemberIds.length > 0 ? (
<div className="stack sm">
{poolMemberIds.map((userId) => {
const user = userIdToUser(userId);
return (
<div

View File

@@ -32,10 +32,6 @@ export async function updateUserMapModePreferences({
userId: number;
mapModePreferences: UserMapModePreferences;
}) {
const modesExcluded = modesShort.filter(
(mode) => !mapModePreferences.pool.some((mp) => mp.mode === mode),
);
const currentPreferences = (
await db
.selectFrom("User")
@@ -44,22 +40,55 @@ export async function updateUserMapModePreferences({
.executeTakeFirstOrThrow()
).mapModePreferences;
for (const mode of modesExcluded) {
const previousModePreference = currentPreferences?.pool.filter(
(mp) => mp.mode === mode,
);
if (previousModePreference && previousModePreference.length > 0) {
mapModePreferences.pool.push(...previousModePreference);
}
}
const mergedPool = mergeExcludedModePreferences(
mapModePreferences.pool,
currentPreferences?.pool,
);
return db
.updateTable("User")
.set({ mapModePreferences: JSON.stringify(mapModePreferences) })
.set({
mapModePreferences: JSON.stringify({
...mapModePreferences,
pool: mergedPool,
}),
})
.where("id", "=", userId)
.execute();
}
export async function updateTeamMapModePreferences({
teamId,
mapModePreferences,
}: {
teamId: number;
mapModePreferences: UserMapModePreferences;
}) {
const currentPreferences = (
await db
.selectFrom("AllTeam")
.select("mapModePreferences")
.where("id", "=", teamId)
.executeTakeFirstOrThrow()
).mapModePreferences;
const mergedPool = mergeExcludedModePreferences(
mapModePreferences.pool,
currentPreferences?.pool,
);
return db
.updateTable("AllTeam")
.set({
mapModePreferences: JSON.stringify({
...mapModePreferences,
pool: mergedPool,
}),
})
.where("id", "=", teamId)
.execute();
}
export function updateVoiceChat(args: {
userId: number;
vc: Tables["User"]["vc"];
@@ -112,11 +141,21 @@ export function updateNoScreen({
.execute();
}
export function currentTeamByUserId(userId: number) {
return db
.selectFrom("TeamMember")
.innerJoin("Team", "Team.id", "TeamMember.teamId")
.select(["Team.name"])
.where("TeamMember.userId", "=", userId)
.executeTakeFirst();
/**
* Preserves existing preferences for modes not included in the new submission.
* So if they later want to play this mode again, the system remembers their maps.
*/
function mergeExcludedModePreferences(
newPool: UserMapModePreferences["pool"],
currentPool: UserMapModePreferences["pool"] | undefined,
) {
const modesExcluded = modesShort.filter(
(mode) => !newPool.some((mp) => mp.mode === mode),
);
const preservedPreferences = modesExcluded.flatMap(
(mode) => currentPool?.filter((mp) => mp.mode === mode) ?? [],
);
return [...newPool, ...preservedPreferences];
}

View File

@@ -1,5 +1,6 @@
import { requireUser } from "~/features/auth/core/user.server";
import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server";
import * as TeamRepository from "~/features/team/TeamRepository.server";
import { parseRequestPayload } from "~/utils/remix.server";
import { assertUnreachable } from "~/utils/types";
import { settingsActionSchema } from "../q-settings-schemas.server";
@@ -13,10 +14,25 @@ export const action = async ({ request }: { request: Request }) => {
switch (data._action) {
case "UPDATE_MAP_MODE_PREFERENCES": {
await QSettingsRepository.updateUserMapModePreferences({
mapModePreferences: data.mapModePreferences,
userId: user.id,
});
if (typeof data.teamId === "number") {
const allTeams = await TeamRepository.findAllMemberOfByUserId(user.id);
const canManage = allTeams.some(
(t) => t.id === data.teamId && (t.isOwner || t.isManager),
);
if (!canManage) {
throw new Response(null, { status: 403 });
}
await QSettingsRepository.updateTeamMapModePreferences({
mapModePreferences: data.mapModePreferences,
teamId: data.teamId,
});
} else {
await QSettingsRepository.updateUserMapModePreferences({
mapModePreferences: data.mapModePreferences,
userId: user.id,
});
}
break;
}
case "UPDATE_VC": {

View File

@@ -1,11 +1,15 @@
import { requireUser } from "~/features/auth/core/user.server";
import * as QSettingsRepository from "~/features/sendouq-settings/QSettingsRepository.server";
import * as TeamRepository from "~/features/team/TeamRepository.server";
export const loader = async () => {
const user = requireUser();
const allTeams = await TeamRepository.findAllMemberOfByUserId(user.id);
const manageableTeams = allTeams.filter((t) => t.isOwner || t.isManager);
return {
settings: await QSettingsRepository.settingsByUserId(user.id),
team: await QSettingsRepository.currentTeamByUserId(user.id),
manageableTeams,
};
};

View File

@@ -1,5 +1,5 @@
import { z } from "zod";
import { _action, modeShort, safeJSONParse, stageId } from "~/utils/zod";
import { _action, id, modeShort, safeJSONParse, stageId } from "~/utils/zod";
import { AMOUNT_OF_MAPS_IN_POOL_PER_MODE } from "./q-settings-constants";
import {
updateVoiceChatSchema,
@@ -10,6 +10,10 @@ const preference = z.enum(["AVOID", "PREFER"]).optional();
export const settingsActionSchema = z.union([
z.object({
_action: _action("UPDATE_MAP_MODE_PREFERENCES"),
teamId: z.preprocess(
(val) => (val === "" || val === undefined ? undefined : Number(val)),
id.optional(),
),
mapModePreferences: z.preprocess(
safeJSONParse,
z

View File

@@ -5,10 +5,13 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import type { MetaFunction } from "react-router";
import { useFetcher, useLoaderData } from "react-router";
import { Avatar } from "~/components/Avatar";
import { SendouSelect, SendouSelectItem } from "~/components/elements/Select";
import { ModeImage } from "~/components/Image";
import { Main } from "~/components/Main";
import { SubmitButton } from "~/components/SubmitButton";
import type { Preference, UserMapModePreferences } from "~/db/tables";
import { useUser } from "~/features/auth/core/user";
import {
soundCodeToLocalStorageKey,
soundVolume,
@@ -18,6 +21,7 @@ import { SendouForm } from "~/form/SendouForm";
import { useHydrated } from "~/hooks/useHydrated";
import { modesShort } from "~/modules/in-game-lists/modes";
import type { ModeShort } from "~/modules/in-game-lists/types";
import type { SerializeFrom } from "~/utils/remix";
import { metaTags } from "~/utils/remix";
import type { SendouRouteHandle } from "~/utils/remix.server";
import {
@@ -79,29 +83,52 @@ export default function SendouQSettingsPage() {
);
}
const PERSONAL_KEY = "personal";
type ManageableTeam = SerializeFrom<typeof loader>["manageableTeams"][number];
function preferencesFromRaw(
raw: UserMapModePreferences | null,
): UserMapModePreferences {
if (!raw) return { pool: [], modes: [] };
return {
modes: raw.modes,
pool: raw.pool.map((p) => ({
mode: p.mode,
stages: p.stages.filter((s) => !BANNED_MAPS[p.mode].includes(s)),
})),
};
}
function MapPicker() {
const { t } = useTranslation(["q", "common"]);
const data = useLoaderData<typeof loader>();
const fetcher = useFetcher();
const [preferences, setPreferences] = React.useState<UserMapModePreferences>(
() => {
if (!data.settings.mapModePreferences) {
return {
pool: [],
modes: [],
};
}
const hasTeams = data.manageableTeams.length > 0;
return {
modes: data.settings.mapModePreferences.modes,
pool: data.settings.mapModePreferences.pool.map((p) => ({
mode: p.mode,
stages: p.stages.filter((s) => !BANNED_MAPS[p.mode].includes(s)),
})),
};
},
const [selectedKey, setSelectedKey] = useState<string>(PERSONAL_KEY);
const selectedTeamId =
selectedKey === PERSONAL_KEY ? undefined : Number(selectedKey);
const preferencesForSelection = (key: string) => {
if (key === PERSONAL_KEY) {
return preferencesFromRaw(data.settings.mapModePreferences);
}
const team = data.manageableTeams.find((t) => String(t.id) === key);
return preferencesFromRaw(team?.mapModePreferences ?? null);
};
const [preferences, setPreferences] = useState<UserMapModePreferences>(() =>
preferencesForSelection(PERSONAL_KEY),
);
const handleSelectionChange = (key: string) => {
setSelectedKey(key);
setPreferences(preferencesForSelection(key));
};
const handleModePreferenceChange = ({
mode,
preference,
@@ -142,6 +169,14 @@ function MapPicker() {
return true;
};
const selectItems = [
{ id: PERSONAL_KEY, name: t("q:settings.maps.personal") },
...data.manageableTeams.map((team) => ({
id: String(team.id),
name: team.name,
})),
];
return (
<details>
<summary className={clsx(styles.summary, "half-width")}>
@@ -164,7 +199,34 @@ function MapPicker() {
}),
})}
/>
{selectedTeamId ? (
<input type="hidden" name="teamId" value={selectedTeamId} />
) : null}
<div className="stack lg">
{hasTeams ? (
<div className="half-width">
<SendouSelect
value={selectedKey}
onChange={(key) => handleSelectionChange(String(key))}
aria-label={t("q:settings.maps.preferencesFor")}
items={selectItems}
bottomText={t("q:settings.maps.teamExplanation")}
>
{(item) => (
<SendouSelectItem
key={item.id}
id={item.id}
textValue={item.name}
>
<MapPickerSelectOption
item={item}
teams={data.manageableTeams}
/>
</SendouSelectItem>
)}
</SendouSelect>
</div>
) : null}
<div className="stack items-center">
{modesShort.map((modeShort) => {
const preference = preferences.modes.find(
@@ -241,6 +303,35 @@ function MapPicker() {
);
}
function MapPickerSelectOption({
item,
teams,
}: {
item: { id: string; name: string };
teams: ManageableTeam[];
}) {
const user = useUser();
if (item.id === PERSONAL_KEY) {
return (
<div className="stack horizontal xs items-center">
<Avatar user={user} size="xxxs" />
{item.name}
</div>
);
}
const team = teams.find((t) => String(t.id) === item.id);
if (!team) return item.name;
return (
<div className="stack horizontal xs items-center">
<Avatar size="xxxs" url={team.logoUrl} identiconInput={team.name} />
{team.name}
</div>
);
}
function VoiceChat() {
const { t } = useTranslation(["q"]);
const data = useLoaderData<typeof loader>();

View File

@@ -12,7 +12,31 @@ import { userIsBanned } from "../ban/core/banned.server";
import { FULL_GROUP_SIZE } from "./q-constants";
import { SendouQError } from "./q-utils.server";
export function mapModePreferencesByGroupId(groupId: number) {
export async function mapModePreferencesByGroupId(groupId: number) {
const group = await db
.selectFrom("Group")
.leftJoin("AllTeam", "AllTeam.id", "Group.teamId")
.select([
"AllTeam.mapModePreferences as teamMapModePreferences",
"AllTeam.name as teamName",
])
.where("Group.id", "=", groupId)
.executeTakeFirst();
if (group?.teamMapModePreferences) {
const members = await db
.selectFrom("GroupMember")
.select("GroupMember.userId")
.where("GroupMember.groupId", "=", groupId)
.execute();
return members.map((m) => ({
userId: m.userId,
preferences: group.teamMapModePreferences as UserMapModePreferences,
teamName: group.teamName,
}));
}
return db
.selectFrom("GroupMember")
.innerJoin("User", "User.id", "GroupMember.userId")

View File

@@ -69,7 +69,10 @@ export function findAllMemberOfByUserId(userId: number) {
"Team.id",
"Team.customUrl",
"Team.name",
"Team.mapModePreferences",
"TeamMemberWithSecondary.role",
"TeamMemberWithSecondary.isOwner",
"TeamMemberWithSecondary.isManager",
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"logoUrl",
),

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -249,4 +249,39 @@ test.describe("SendouQ", () => {
combinedGroup.getByTestId("sendouq-group-card-member"),
).toHaveCount(2);
});
test("Team map preferences shown on match page", async ({ page }) => {
await seed(page, "TEAM_MAP_PREFS");
await impersonate(page, ADMIN_ID);
await navigate({ page, url: SENDOUQ_LOOKING_PAGE });
const groupCards = page.getByTestId("sendouq-group-card");
const count = await groupCards.count();
for (let i = 1; i < count; i++) {
const groupCard = groupCards.nth(i);
const challengeButton = groupCard
.locator('button[type="submit"]')
.first();
await challengeButton.click();
}
await impersonate(page, NZAP_TEST_ID);
await navigate({ page, url: SENDOUQ_LOOKING_PAGE });
const acceptButton = page
.getByRole("button", { name: "Start match" })
.first();
await expect(acceptButton).toBeVisible();
await acceptButton.click();
await expect(page).toHaveURL(/\/q\/match\/\d+/);
const popoverButton = page.getByRole("button", { name: /votes/ }).first();
await expect(popoverButton).toBeVisible();
await popoverButton.click();
const popover = page.getByRole("dialog");
await expect(popover.getByText("Alliance Rogue")).toBeVisible();
});
});

View File

@@ -51,6 +51,9 @@
"front.preview": "",
"front.preview.explanation": "",
"settings.maps.header": "",
"settings.maps.personal": "",
"settings.maps.preferencesFor": "",
"settings.maps.teamExplanation": "",
"settings.maps.avoid": "",
"settings.maps.prefer": "",
"settings.maps.neutral": "",

View File

@@ -54,6 +54,7 @@
"pickInfo.default.explanation": "",
"pickInfo.votes_one": "",
"pickInfo.votes_other": "",
"pickInfo.teamMapList": "",
"pickInfo.counterpick": "Modvalg",
"pickInfo.roll": "",
"generator.error": "Ændringer, som du har lavet, blev ikke gemt, da turneringen nu er begyndt. ",

View File

@@ -51,6 +51,9 @@
"front.preview": "",
"front.preview.explanation": "",
"settings.maps.header": "",
"settings.maps.personal": "",
"settings.maps.preferencesFor": "",
"settings.maps.teamExplanation": "",
"settings.maps.avoid": "",
"settings.maps.prefer": "",
"settings.maps.neutral": "",

View File

@@ -54,6 +54,7 @@
"pickInfo.default.explanation": "",
"pickInfo.votes_one": "",
"pickInfo.votes_other": "",
"pickInfo.teamMapList": "",
"pickInfo.counterpick": "Counterpick",
"pickInfo.roll": "",
"generator.error": "Die Änderungen wurden nicht gespeichert, weil das Turnier bereits begonnen hat",

View File

@@ -51,6 +51,9 @@
"front.preview": "Preview groups in the queue without joining",
"front.preview.explanation": "This feature is only available to Supporter tier (or above) patrons of sendou.ink",
"settings.maps.header": "Stages and modes",
"settings.maps.personal": "Personal",
"settings.maps.preferencesFor": "Preferences for",
"settings.maps.teamExplanation": "Team map list is used for all members when queueing as a team",
"settings.maps.avoid": "Avoid",
"settings.maps.prefer": "Prefer",
"settings.maps.neutral": "Neutral",

View File

@@ -54,6 +54,7 @@
"pickInfo.default.explanation": "A suitable map was not available from the participants' pools. This map was selected from all popular maps instead.",
"pickInfo.votes_one": "{{count}} vote",
"pickInfo.votes_other": "{{count}} votes",
"pickInfo.teamMapList": "{{teamName}} map list",
"pickInfo.counterpick": "Counterpick",
"pickInfo.roll": "Random",
"generator.error": "Changes you made weren't saved since the tournament started",

View File

@@ -51,6 +51,9 @@
"front.preview": "Ver avance de grupos en fila sin unirte",
"front.preview.explanation": "Esta función solo es disponible para Patrons a nivel Supporter (o más alto) de sendou.ink",
"settings.maps.header": "Mapas y modos",
"settings.maps.personal": "",
"settings.maps.preferencesFor": "",
"settings.maps.teamExplanation": "",
"settings.maps.avoid": "Evitar",
"settings.maps.prefer": "Preferir",
"settings.maps.neutral": "Neutral",

View File

@@ -55,6 +55,7 @@
"pickInfo.votes_one": "{{count}} voto",
"pickInfo.votes_many": "{{count}} votos",
"pickInfo.votes_other": "{{count}} votos",
"pickInfo.teamMapList": "",
"pickInfo.counterpick": "Contraselección",
"pickInfo.roll": "",
"generator.error": "Los cambios no fueron guardados ya que el torneo comenzó",

View File

@@ -51,6 +51,9 @@
"front.preview": "Ver avance de grupos en fila sin unirte",
"front.preview.explanation": "Esta función solo es disponible para Patrons a nivel Supporter (o más alto) de sendou.ink",
"settings.maps.header": "Escenarios y Estilos",
"settings.maps.personal": "",
"settings.maps.preferencesFor": "",
"settings.maps.teamExplanation": "",
"settings.maps.avoid": "Evitar",
"settings.maps.prefer": "Preferir",
"settings.maps.neutral": "Neutral",

View File

@@ -55,6 +55,7 @@
"pickInfo.votes_one": "",
"pickInfo.votes_many": "",
"pickInfo.votes_other": "",
"pickInfo.teamMapList": "",
"pickInfo.counterpick": "Contraselección",
"pickInfo.roll": "",
"generator.error": "Cambios no fueron guardados ya que el torneo comenzó",

View File

@@ -51,6 +51,9 @@
"front.preview": "",
"front.preview.explanation": "",
"settings.maps.header": "",
"settings.maps.personal": "",
"settings.maps.preferencesFor": "",
"settings.maps.teamExplanation": "",
"settings.maps.avoid": "",
"settings.maps.prefer": "",
"settings.maps.neutral": "",

View File

@@ -55,6 +55,7 @@
"pickInfo.votes_one": "",
"pickInfo.votes_many": "",
"pickInfo.votes_other": "",
"pickInfo.teamMapList": "",
"pickInfo.counterpick": "",
"pickInfo.roll": "",
"generator.error": "Les modifications apportées n'ont pas été enregistrées depuis le début du tournoi",

View File

@@ -51,6 +51,9 @@
"front.preview": "Regarder qui est dans la queue sans la rejoindre",
"front.preview.explanation": "Cette fonctionnalité n'est disponible que pour les utilisateurs de niveau Supporter (ou supérieur) sur le patreon de sendou.ink.",
"settings.maps.header": "Stages et modes",
"settings.maps.personal": "",
"settings.maps.preferencesFor": "",
"settings.maps.teamExplanation": "",
"settings.maps.avoid": "Détester",
"settings.maps.prefer": "Préférer",
"settings.maps.neutral": "Neutre",

View File

@@ -55,6 +55,7 @@
"pickInfo.votes_one": "",
"pickInfo.votes_many": "",
"pickInfo.votes_other": "",
"pickInfo.teamMapList": "",
"pickInfo.counterpick": "Counterpick",
"pickInfo.roll": "",
"generator.error": "Les modifications apportées n'ont pas été enregistrées depuis le début du tournoi",

View File

@@ -51,6 +51,9 @@
"front.preview": "",
"front.preview.explanation": "",
"settings.maps.header": "",
"settings.maps.personal": "",
"settings.maps.preferencesFor": "",
"settings.maps.teamExplanation": "",
"settings.maps.avoid": "",
"settings.maps.prefer": "",
"settings.maps.neutral": "",

View File

@@ -55,6 +55,7 @@
"pickInfo.votes_one": "",
"pickInfo.votes_two": "",
"pickInfo.votes_other": "",
"pickInfo.teamMapList": "",
"pickInfo.counterpick": "",
"pickInfo.roll": "",
"generator.error": "שינויים שביצעת לא נשמרו מאז תחילת הטורניר",

View File

@@ -51,6 +51,9 @@
"front.preview": "Visualizza gruppi nella coda prima di unirti",
"front.preview.explanation": "Questa funzione è disponibile solo per iscritti al Patreon di sendou.ink di tier Supporter o più",
"settings.maps.header": "Mappe e modalità",
"settings.maps.personal": "",
"settings.maps.preferencesFor": "",
"settings.maps.teamExplanation": "",
"settings.maps.avoid": "Evita",
"settings.maps.prefer": "Preferisci",
"settings.maps.neutral": "Neutrale",

View File

@@ -55,6 +55,7 @@
"pickInfo.votes_one": "",
"pickInfo.votes_many": "",
"pickInfo.votes_other": "",
"pickInfo.teamMapList": "",
"pickInfo.counterpick": "Counterpick",
"pickInfo.roll": "",
"generator.error": "I cambiamenti che hai fatto non sono stati salvati visto che il torneo è già cominciato.",

View File

@@ -51,6 +51,9 @@
"front.preview": "ジョインせず列にいるグループを見る",
"front.preview.explanation": "この機能はサポーター(あるいはそれ以上)の支援者のみ使えます。",
"settings.maps.header": "ステージ、モード",
"settings.maps.personal": "",
"settings.maps.preferencesFor": "",
"settings.maps.teamExplanation": "",
"settings.maps.avoid": "避ける",
"settings.maps.prefer": "好み",
"settings.maps.neutral": "普通",

View File

@@ -52,6 +52,7 @@
"pickInfo.both": "両者選択",
"pickInfo.default": "",
"pickInfo.default.explanation": "",
"pickInfo.teamMapList": "",
"pickInfo.counterpick": "順番に選ぶ",
"pickInfo.roll": "",
"generator.error": "トーナメントが開始済みなので変更は保存されませんでした。",

View File

@@ -51,6 +51,9 @@
"front.preview": "",
"front.preview.explanation": "",
"settings.maps.header": "",
"settings.maps.personal": "",
"settings.maps.preferencesFor": "",
"settings.maps.teamExplanation": "",
"settings.maps.avoid": "",
"settings.maps.prefer": "",
"settings.maps.neutral": "",

View File

@@ -52,6 +52,7 @@
"pickInfo.both": "",
"pickInfo.default": "",
"pickInfo.default.explanation": "",
"pickInfo.teamMapList": "",
"pickInfo.counterpick": "",
"pickInfo.roll": "",
"generator.error": "",

View File

@@ -51,6 +51,9 @@
"front.preview": "",
"front.preview.explanation": "",
"settings.maps.header": "",
"settings.maps.personal": "",
"settings.maps.preferencesFor": "",
"settings.maps.teamExplanation": "",
"settings.maps.avoid": "",
"settings.maps.prefer": "",
"settings.maps.neutral": "",

View File

@@ -54,6 +54,7 @@
"pickInfo.default.explanation": "",
"pickInfo.votes_one": "",
"pickInfo.votes_other": "",
"pickInfo.teamMapList": "",
"pickInfo.counterpick": "",
"pickInfo.roll": "",
"generator.error": "",

View File

@@ -51,6 +51,9 @@
"front.preview": "",
"front.preview.explanation": "",
"settings.maps.header": "",
"settings.maps.personal": "",
"settings.maps.preferencesFor": "",
"settings.maps.teamExplanation": "",
"settings.maps.avoid": "",
"settings.maps.prefer": "",
"settings.maps.neutral": "",

View File

@@ -56,6 +56,7 @@
"pickInfo.votes_few": "",
"pickInfo.votes_many": "",
"pickInfo.votes_other": "",
"pickInfo.teamMapList": "",
"pickInfo.counterpick": "",
"pickInfo.roll": "",
"generator.error": "Zmiany nie zostały zapisane z powodu rozpoczęcia turnieju",

View File

@@ -51,6 +51,9 @@
"front.preview": "Pré-visualizar grupos na fila sem entrar",
"front.preview.explanation": "Essa função está disponível apenas para patronos (ou patronesses) da tier Supporter (ou acima) do sendou.ink",
"settings.maps.header": "Mapas e modos",
"settings.maps.personal": "",
"settings.maps.preferencesFor": "",
"settings.maps.teamExplanation": "",
"settings.maps.avoid": "Evitar",
"settings.maps.prefer": "Prefere",
"settings.maps.neutral": "Neutro",

View File

@@ -55,6 +55,7 @@
"pickInfo.votes_one": "",
"pickInfo.votes_many": "",
"pickInfo.votes_other": "",
"pickInfo.teamMapList": "",
"pickInfo.counterpick": "Counterpick",
"pickInfo.roll": "",
"generator.error": "Mudanças que você fez não foram salvas uma vez que o torneio já começou",

View File

@@ -51,6 +51,9 @@
"front.preview": "Предпросмотр груп в очереди без присоединения",
"front.preview.explanation": "Данная функция доступна только меценатам sendou.imk уровня Supporter (или выше)",
"settings.maps.header": "Арены и режимы",
"settings.maps.personal": "",
"settings.maps.preferencesFor": "",
"settings.maps.teamExplanation": "",
"settings.maps.avoid": "Избегаю",
"settings.maps.prefer": "Предпочитаю",
"settings.maps.neutral": "Нейтрально",

View File

@@ -56,6 +56,7 @@
"pickInfo.votes_few": "",
"pickInfo.votes_many": "",
"pickInfo.votes_other": "",
"pickInfo.teamMapList": "",
"pickInfo.counterpick": "Контрвыбор",
"pickInfo.roll": "",
"generator.error": "Ваши изменения не были сохранены так как турнир начался",

View File

@@ -51,6 +51,9 @@
"front.preview": "预览当前匹配中的队伍",
"front.preview.explanation": "该功能仅对sendou.ink的patron支持者开放",
"settings.maps.header": "地图和模式",
"settings.maps.personal": "",
"settings.maps.preferencesFor": "",
"settings.maps.teamExplanation": "",
"settings.maps.avoid": "避开",
"settings.maps.prefer": "偏好",
"settings.maps.neutral": "中立",

View File

@@ -52,6 +52,7 @@
"pickInfo.both": "双方都选择",
"pickInfo.default": "",
"pickInfo.default.explanation": "",
"pickInfo.teamMapList": "",
"pickInfo.counterpick": "反选",
"pickInfo.roll": "",
"generator.error": "由于比赛已经开始,您所更改的内容无法保存",

View File

@@ -0,0 +1,5 @@
export function up(db) {
db.prepare(
/* sql */ `alter table "AllTeam" add column "mapModePreferences" text`,
).run();
}