From f969e86ea0ef79159d82c1fb8fd0d01e97f8242b Mon Sep 17 00:00:00 2001
From: Kalle <38327916+Sendouc@users.noreply.github.com>
Date: Thu, 1 Feb 2024 20:49:51 +0200
Subject: [PATCH] Better UX when trying to pick a tiebreaker map + misc things
---
app/db/seed/index.ts | 47 ++++++++------
.../build-analyzer/routes/analyzer.tsx | 1 +
app/features/sendouq/routes/q.rules.tsx | 4 ++
.../tournament-bracket/core/Bracket.ts | 24 ++++----
.../routes/to.$id.brackets.tsx | 7 ++-
.../tournament/routes/to.$id.admin.tsx | 1 -
.../tournament/routes/to.$id.register.tsx | 61 +++++++++++++------
.../user-page/UserRepository.server.ts | 10 +--
.../user-page/components/UserResultsTable.tsx | 8 ++-
.../routes/u.$identifier.seasons.tsx | 2 +-
e2e/tournament-bracket.spec.ts | 12 ++++
public/locales/en/tournament.json | 1 +
12 files changed, 118 insertions(+), 60 deletions(-)
diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts
index b22a8f06f..ebe1bc5e0 100644
--- a/app/db/seed/index.ts
+++ b/app/db/seed/index.ts
@@ -68,6 +68,7 @@ import {
NZAP_TEST_ID,
} from "./constants";
import placements from "./placements.json";
+import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps";
const calendarEventWithToToolsRegOpen = () =>
calendarEventWithToTools("PICNIC", true);
@@ -961,26 +962,31 @@ const tiebreakerPicks = new MapPool([
{ mode: "CB", stageId: 4 },
]);
function calendarEventWithToToolsTieBreakerMapPool() {
- for (const { mode, stageId } of tiebreakerPicks.stageModePairs) {
- sql
- .prepare(
- `
- insert into "MapPoolMap" (
- "tieBreakerCalendarEventId",
- "stageId",
- "mode"
- ) values (
- $tieBreakerCalendarEventId,
- $stageId,
- $mode
+ for (const tieBreakerCalendarEventId of [
+ TO_TOOLS_CALENDAR_EVENT_ID, // PICNIC
+ TO_TOOLS_CALENDAR_EVENT_ID + 2, // Paddling Pool
+ ]) {
+ for (const { mode, stageId } of tiebreakerPicks.stageModePairs) {
+ sql
+ .prepare(
+ `
+ insert into "MapPoolMap" (
+ "tieBreakerCalendarEventId",
+ "stageId",
+ "mode"
+ ) values (
+ $tieBreakerCalendarEventId,
+ $stageId,
+ $mode
+ )
+ `,
)
- `,
- )
- .run({
- tieBreakerCalendarEventId: TO_TOOLS_CALENDAR_EVENT_ID,
- stageId,
- mode,
- });
+ .run({
+ tieBreakerCalendarEventId,
+ stageId,
+ mode,
+ });
+ }
}
}
@@ -1112,6 +1118,9 @@ function calendarEventWithToToolsTeams(
for (const pair of shuffledPairs) {
if (event === "ITZ" && pair.mode !== "SZ") continue;
+ if (BANNED_MAPS[pair.mode].includes(pair.stageId)) {
+ continue;
+ }
if (pair.mode === "SZ" && SZ >= (event === "ITZ" ? 6 : 2)) continue;
if (pair.mode === "TC" && TC >= 2) continue;
diff --git a/app/features/build-analyzer/routes/analyzer.tsx b/app/features/build-analyzer/routes/analyzer.tsx
index f29eaa344..b5823c71a 100644
--- a/app/features/build-analyzer/routes/analyzer.tsx
+++ b/app/features/build-analyzer/routes/analyzer.tsx
@@ -992,6 +992,7 @@ function StatChart({
options={chartOptions as any}
headerSuffix={t("analyzer:abilityPoints.short")}
valueSuffix={valueSuffix}
+ xAxis="linear"
/>
);
}
diff --git a/app/features/sendouq/routes/q.rules.tsx b/app/features/sendouq/routes/q.rules.tsx
index d40bacf3b..a891822dc 100644
--- a/app/features/sendouq/routes/q.rules.tsx
+++ b/app/features/sendouq/routes/q.rules.tsx
@@ -47,6 +47,10 @@ export default function SendouqRules() {
Match can be canceled if both group owners agree. If the groups
don't agree then the match should be played out.
+
+ Only exception is if a group asks to play without Splattercolor
+ Screen and the other group in the match doesn't want to. In this
+ situation either group is free to cancel the match.
Room hosting
diff --git a/app/features/tournament-bracket/core/Bracket.ts b/app/features/tournament-bracket/core/Bracket.ts
index 5b1225924..31ca02ba0 100644
--- a/app/features/tournament-bracket/core/Bracket.ts
+++ b/app/features/tournament-bracket/core/Bracket.ts
@@ -688,17 +688,19 @@ class RoundRobinBracket extends Bracket {
let lastPlacement = 0;
let currentPlacement = 1;
let teamsEncountered = 0;
- return sorted.map((team) => {
- if (team.placement !== lastPlacement) {
- lastPlacement = team.placement;
- currentPlacement = teamsEncountered + 1;
- }
- teamsEncountered++;
- return {
- ...team,
- placement: currentPlacement,
- };
- });
+ return this.standingsWithoutNonParticipants(
+ sorted.map((team) => {
+ if (team.placement !== lastPlacement) {
+ lastPlacement = team.placement;
+ currentPlacement = teamsEncountered + 1;
+ }
+ teamsEncountered++;
+ return {
+ ...team,
+ placement: currentPlacement,
+ };
+ }),
+ );
}
get type(): TournamentBracketProgression[number]["type"] {
diff --git a/app/features/tournament-bracket/routes/to.$id.brackets.tsx b/app/features/tournament-bracket/routes/to.$id.brackets.tsx
index 24a35856b..bf8d917da 100644
--- a/app/features/tournament-bracket/routes/to.$id.brackets.tsx
+++ b/app/features/tournament-bracket/routes/to.$id.brackets.tsx
@@ -426,7 +426,7 @@ export default function TournamentBracketsPage() {
) : null}
{tournament.ctx.isFinalized || tournament.canFinalize(user) ? (
-
+
) : null}
diff --git a/app/features/tournament/routes/to.$id.admin.tsx b/app/features/tournament/routes/to.$id.admin.tsx
index ec41a637b..69505b940 100644
--- a/app/features/tournament/routes/to.$id.admin.tsx
+++ b/app/features/tournament/routes/to.$id.admin.tsx
@@ -230,7 +230,6 @@ export const action: ActionFunction = async ({ request, params }) => {
return null;
};
-// xxx: download participants of certain bracket (not checked in probably most relevant)
// TODO: translations
export default function TournamentAdminPage() {
const { t } = useTranslation(["calendar"]);
diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx
index e04747537..cd15f6145 100644
--- a/app/features/tournament/routes/to.$id.register.tsx
+++ b/app/features/tournament/routes/to.$id.register.tsx
@@ -29,6 +29,7 @@ import { MapPool } from "~/features/map-list-generator/core/map-pool";
import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps";
import * as TeamRepository from "~/features/team/TeamRepository.server";
import { findMapPoolByTeamId } from "~/features/tournament-bracket";
+import type { TournamentData } from "~/features/tournament-bracket/core/Tournament.server";
import {
tournamentFromDB,
type TournamentDataTeam,
@@ -155,8 +156,11 @@ export const action: ActionFunction = async ({ request, params }) => {
const mapPool = new MapPool(data.mapPool);
validate(ownTeam);
validate(
- validateCounterPickMapPool(mapPool, isOneModeTournamentOf(event)) ===
- "VALID",
+ validateCounterPickMapPool(
+ mapPool,
+ isOneModeTournamentOf(event),
+ tournament.ctx.tieBreakerMapPool,
+ ) === "VALID",
);
upsertCounterpickMaps({
@@ -557,7 +561,6 @@ function TeamInfo({
canUnregister: boolean;
}) {
const { t } = useTranslation(["tournament", "common"]);
- const id = React.useId();
const fetcher = useFetcher();
return (
@@ -599,12 +602,12 @@ function TeamInfo({
-
+
{t("tournament:pre.info.noHost")}
@@ -909,21 +912,26 @@ function CounterPickMapPoolPicker() {
}`}
>
- {stageIds
- .filter((id) => id !== tiebreakerStageId)
- .map((stageId) => {
- const isBanned =
- BANNED_MAPS[mode].includes(stageId);
+ {stageIds.map((stageId) => {
+ const isBanned =
+ BANNED_MAPS[mode].includes(stageId);
- return (
-
- {t(`game-misc:STAGE_${stageId}`)}{" "}
- {isBanned
+ const isTiebreaker =
+ stageId === tiebreakerStageId;
+
+ return (
+
+ {t(`game-misc:STAGE_${stageId}`)}{" "}
+ {isTiebreaker
+ ? `(${t(
+ "tournament:pre.pool.tiebreaker.short",
+ )})`
+ : isBanned
? `(${t("tournament:pre.pool.banned")})`
: ""}
-
- );
- })}
+
+ );
+ })}
);
@@ -934,6 +942,7 @@ function CounterPickMapPoolPicker() {
{validateCounterPickMapPool(
counterPickMapPool,
isOneModeTournamentOf,
+ tournament.ctx.tieBreakerMapPool,
) === "VALID" ? (
)}
@@ -967,7 +977,8 @@ function MapPoolValidationStatusMessage({
if (
status !== "TOO_MUCH_STAGE_REPEAT" &&
status !== "STAGE_REPEAT_IN_SAME_MODE" &&
- status !== "INCLUDES_BANNED"
+ status !== "INCLUDES_BANNED" &&
+ status !== "INCLUDES_TIEBREAKER"
)
return null;
@@ -987,11 +998,13 @@ type CounterPickValidationStatus =
| "VALID"
| "TOO_MUCH_STAGE_REPEAT"
| "STAGE_REPEAT_IN_SAME_MODE"
- | "INCLUDES_BANNED";
+ | "INCLUDES_BANNED"
+ | "INCLUDES_TIEBREAKER";
function validateCounterPickMapPool(
mapPool: MapPool,
isOneModeOnlyTournamentFor: ModeShort | null,
+ tieBreakerMapPool: TournamentData["ctx"]["tieBreakerMapPool"],
): CounterPickValidationStatus {
const stageCounts = new Map
();
for (const stageId of mapPool.stages) {
@@ -1024,6 +1037,16 @@ function validateCounterPickMapPool(
return "INCLUDES_BANNED";
}
+ if (
+ mapPool.stageModePairs.some((pair) =>
+ tieBreakerMapPool.some(
+ (stage) => stage.mode === pair.mode && stage.stageId === pair.stageId,
+ ),
+ )
+ ) {
+ return "INCLUDES_TIEBREAKER";
+ }
+
if (
!isOneModeOnlyTournamentFor &&
(mapPool.parsed.SZ.length !== TOURNAMENT.COUNTERPICK_MAPS_PER_MODE ||
diff --git a/app/features/user-page/UserRepository.server.ts b/app/features/user-page/UserRepository.server.ts
index 097de066d..28044b658 100644
--- a/app/features/user-page/UserRepository.server.ts
+++ b/app/features/user-page/UserRepository.server.ts
@@ -211,18 +211,18 @@ export function findResultsByUserId(userId: number) {
"TournamentResult.isHighlight",
jsonArrayFrom(
eb
- .selectFrom("TournamentTeamMember")
- .innerJoin("User", "User.id", "TournamentTeamMember.userId")
+ .selectFrom("TournamentResult as TournamentResult2")
+ .innerJoin("User", "User.id", "TournamentResult2.userId")
.select([
...COMMON_USER_FIELDS,
sql`null`.as("name"),
])
.whereRef(
- "TournamentTeamMember.tournamentTeamId",
+ "TournamentResult2.tournamentTeamId",
"=",
- "TournamentTeam.id",
+ "TournamentResult.tournamentTeamId",
)
- .where("TournamentTeamMember.userId", "!=", userId),
+ .where("TournamentResult2.userId", "!=", userId),
).as("mates"),
])
.where("TournamentResult.userId", "=", userId),
diff --git a/app/features/user-page/components/UserResultsTable.tsx b/app/features/user-page/components/UserResultsTable.tsx
index f502c918d..3187ac708 100644
--- a/app/features/user-page/components/UserResultsTable.tsx
+++ b/app/features/user-page/components/UserResultsTable.tsx
@@ -44,7 +44,7 @@ export function UserResultsTable({
- {results.map((result) => {
+ {results.map((result, i) => {
// We are trying to construct a reasonable label for the checkbox
// which shouldn't contain the whole information of the table row as
// that can be also accessed when needed.
@@ -98,6 +98,7 @@ export function UserResultsTable({
to={tournamentBracketsPage({
tournamentId: result.tournamentId,
})}
+ data-testid="tournament-name-cell"
>
{result.eventName}
@@ -116,7 +117,10 @@ export function UserResultsTable({
)}
-
+
{result.mates.map((player) => (
;
+ return ;
}
const MIN_DEGREE = 5;
diff --git a/e2e/tournament-bracket.spec.ts b/e2e/tournament-bracket.spec.ts
index f90ed49ef..9bfe2a8d5 100644
--- a/e2e/tournament-bracket.spec.ts
+++ b/e2e/tournament-bracket.spec.ts
@@ -363,7 +363,19 @@ test.describe("Tournament bracket", () => {
await expect(page.getByTestId("standing-1")).toBeVisible();
await isNotVisible(page.getByTestId("standing-3"));
+ // not possible to reopen finals match anymore
await navigateToMatch(page, 14);
await isNotVisible(page.getByTestId("reopen-match-button"));
+ await backToBracket(page);
+
+ // added result to user profile
+ await page.getByTestId("standing-player").first().click();
+ await page.getByText("Results").click();
+ await expect(
+ page.getByTestId("tournament-name-cell").first(),
+ ).toContainText("Paddling Pool 253");
+ await expect(
+ page.locator('[data-testid="mates-cell-placement-0"] li'),
+ ).toHaveCount(3);
});
});
diff --git a/public/locales/en/tournament.json b/public/locales/en/tournament.json
index 9a7e1c1e6..1af77532a 100644
--- a/public/locales/en/tournament.json
+++ b/public/locales/en/tournament.json
@@ -39,6 +39,7 @@
"pre.pool.tiebreaker": "Tiebreaker: {{stage}}",
"pre.pool.pick": "Pick {{number}}",
"pre.pool.banned": "Banned",
+ "pre.pool.tiebreaker.short": "Tiebreaker",
"pre.sub.prompt": "No team in mind for this event? You can also join the list of subs.",