mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-02 18:01:29 -05:00
Better UX when trying to pick a tiebreaker map + misc things
This commit is contained in:
parent
e506a22cb8
commit
f969e86ea0
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -992,6 +992,7 @@ function StatChart({
|
|||
options={chartOptions as any}
|
||||
headerSuffix={t("analyzer:abilityPoints.short")}
|
||||
valueSuffix={valueSuffix}
|
||||
xAxis="linear"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ export default function SendouqRules() {
|
|||
<div>
|
||||
Match can be canceled if both group owners agree. If the groups
|
||||
don't agree then the match should be played out.
|
||||
<br />
|
||||
<br /> 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.
|
||||
</div>
|
||||
|
||||
<h2 className="text-lg mt-4">Room hosting</h2>
|
||||
|
|
|
|||
|
|
@ -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"] {
|
||||
|
|
|
|||
|
|
@ -426,7 +426,7 @@ export default function TournamentBracketsPage() {
|
|||
) : null}
|
||||
</div>
|
||||
{tournament.ctx.isFinalized || tournament.canFinalize(user) ? (
|
||||
<FinalStandings standings={tournament.standings} />
|
||||
<FinalStandings />
|
||||
) : null}
|
||||
<BracketNav bracketIdx={bracketIdx} setBracketIdx={setBracketIdx} />
|
||||
<div
|
||||
|
|
@ -571,11 +571,13 @@ function AddSubsPopOver() {
|
|||
);
|
||||
}
|
||||
|
||||
function FinalStandings({ standings }: { standings: Standing[] }) {
|
||||
function FinalStandings() {
|
||||
const tournament = useTournament();
|
||||
const { t } = useTranslation(["tournament"]);
|
||||
const [viewAll, setViewAll] = React.useState(false);
|
||||
|
||||
const standings = tournament.standings;
|
||||
|
||||
if (standings.length < 2) {
|
||||
console.error("Unexpectedly few standings");
|
||||
return null;
|
||||
|
|
@ -625,6 +627,7 @@ function FinalStandings({ standings }: { standings: Standing[] }) {
|
|||
to={userPage(player)}
|
||||
key={player.userId}
|
||||
className="stack items-center text-xs"
|
||||
data-testid="standing-player"
|
||||
>
|
||||
<Avatar user={player} size="xxs" />
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
<div>
|
||||
<div className="text-lighter text-sm stack horizontal sm items-center">
|
||||
<input
|
||||
id={id}
|
||||
id="no-host"
|
||||
type="checkbox"
|
||||
name="prefersNotToHost"
|
||||
defaultChecked={Boolean(prefersNotToHost)}
|
||||
/>
|
||||
<label htmlFor={id} className="mb-0">
|
||||
<label htmlFor="no-host" className="mb-0">
|
||||
{t("tournament:pre.info.noHost")}
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -909,21 +912,26 @@ function CounterPickMapPoolPicker() {
|
|||
}`}
|
||||
>
|
||||
<option value=""></option>
|
||||
{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 (
|
||||
<option key={stageId} value={stageId}>
|
||||
{t(`game-misc:STAGE_${stageId}`)}{" "}
|
||||
{isBanned
|
||||
const isTiebreaker =
|
||||
stageId === tiebreakerStageId;
|
||||
|
||||
return (
|
||||
<option key={stageId} value={stageId}>
|
||||
{t(`game-misc:STAGE_${stageId}`)}{" "}
|
||||
{isTiebreaker
|
||||
? `(${t(
|
||||
"tournament:pre.pool.tiebreaker.short",
|
||||
)})`
|
||||
: isBanned
|
||||
? `(${t("tournament:pre.pool.banned")})`
|
||||
: ""}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -934,6 +942,7 @@ function CounterPickMapPoolPicker() {
|
|||
{validateCounterPickMapPool(
|
||||
counterPickMapPool,
|
||||
isOneModeTournamentOf,
|
||||
tournament.ctx.tieBreakerMapPool,
|
||||
) === "VALID" ? (
|
||||
<SubmitButton
|
||||
_action="UPDATE_MAP_POOL"
|
||||
|
|
@ -948,6 +957,7 @@ function CounterPickMapPoolPicker() {
|
|||
status={validateCounterPickMapPool(
|
||||
counterPickMapPool,
|
||||
isOneModeTournamentOf,
|
||||
tournament.ctx.tieBreakerMapPool,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -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<StageId, number>();
|
||||
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 ||
|
||||
|
|
|
|||
|
|
@ -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<string | null>`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),
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export function UserResultsTable({
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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}
|
||||
</Link>
|
||||
|
|
@ -116,7 +117,10 @@ export function UserResultsTable({
|
|||
)}
|
||||
</td>
|
||||
<td>
|
||||
<ul className="u__results-players">
|
||||
<ul
|
||||
className="u__results-players"
|
||||
data-testid={`mates-cell-placement-${i}`}
|
||||
>
|
||||
{result.mates.map((player) => (
|
||||
<li
|
||||
key={player.name ? player.name : player.id}
|
||||
|
|
|
|||
|
|
@ -366,7 +366,7 @@ function PowerChart() {
|
|||
];
|
||||
}, [data]);
|
||||
|
||||
return <Chart options={chartOptions as any} />;
|
||||
return <Chart options={chartOptions as any} xAxis="localTime" />;
|
||||
}
|
||||
|
||||
const MIN_DEGREE = 5;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user