diff --git a/app/db/tables.ts b/app/db/tables.ts index 8b4bf89a8..1ebd17ae9 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -389,6 +389,8 @@ export interface TournamentSettings { isRanked?: boolean; autoCheckInAll?: boolean; enableNoScreenToggle?: boolean; + deadlines?: "STRICT" | "DEFAULT"; + isInvitational?: boolean; /** Can teams add subs on their own while tournament is in progress? */ autonomousSubs?: boolean; /** Timestamp (SQLite format) when reg closes, if missing then means closes at start time */ diff --git a/app/features/calendar/CalendarRepository.server.ts b/app/features/calendar/CalendarRepository.server.ts index 0c33f078a..ddd8bec97 100644 --- a/app/features/calendar/CalendarRepository.server.ts +++ b/app/features/calendar/CalendarRepository.server.ts @@ -408,6 +408,8 @@ type CreateArgs = Pick< thirdPlaceMatch?: boolean; autoCheckInAll?: boolean; isRanked?: boolean; + isInvitational?: boolean; + deadlines: TournamentSettings["deadlines"]; enableNoScreenToggle?: boolean; autonomousSubs?: boolean; regClosesAt?: number; @@ -435,6 +437,8 @@ export async function create(args: CreateArgs) { teamsPerGroup: args.teamsPerGroup, thirdPlaceMatch: args.thirdPlaceMatch, isRanked: args.isRanked, + deadlines: args.deadlines, + isInvitational: args.isInvitational, enableNoScreenToggle: args.enableNoScreenToggle, autonomousSubs: args.autonomousSubs, regClosesAt: args.regClosesAt, @@ -535,6 +539,8 @@ export async function update(args: UpdateArgs) { teamsPerGroup: args.teamsPerGroup, thirdPlaceMatch: args.thirdPlaceMatch, isRanked: args.isRanked, + deadlines: args.deadlines, + isInvitational: args.isInvitational, enableNoScreenToggle: args.enableNoScreenToggle, autonomousSubs: args.autonomousSubs, regClosesAt: args.regClosesAt, diff --git a/app/features/calendar/actions/calendar.new.server.ts b/app/features/calendar/actions/calendar.new.server.ts index 8b0c40d7f..f604840ad 100644 --- a/app/features/calendar/actions/calendar.new.server.ts +++ b/app/features/calendar/actions/calendar.new.server.ts @@ -77,6 +77,8 @@ export const action: ActionFunction = async ({ request }) => { teamsPerGroup: data.teamsPerGroup ?? undefined, thirdPlaceMatch: data.thirdPlaceMatch ?? undefined, isRanked: data.isRanked ?? undefined, + isInvitational: data.isInvitational ?? false, + deadlines: data.strictDeadline ? ("STRICT" as const) : ("DEFAULT" as const), enableNoScreenToggle: data.enableNoScreenToggle ?? undefined, autoCheckInAll: data.autoCheckInAll ?? undefined, autonomousSubs: data.autonomousSubs ?? undefined, @@ -209,6 +211,8 @@ export const newCalendarEventActionSchema = z z.boolean().nullish(), ), autonomousSubs: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()), + strictDeadline: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()), + isInvitational: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()), // // tournament format related fields // diff --git a/app/features/calendar/routes/calendar.new.tsx b/app/features/calendar/routes/calendar.new.tsx index 724b37e8e..e583892fd 100644 --- a/app/features/calendar/routes/calendar.new.tsx +++ b/app/features/calendar/routes/calendar.new.tsx @@ -170,6 +170,8 @@ function EventForm() { + + ) : null} {isTournament ? : } @@ -640,6 +642,60 @@ function AutonomousSubsToggle() { ); } +function InvitationalToggle() { + const baseEvent = useBaseEvent(); + const [isInvitational, setIsInvitational] = React.useState( + baseEvent?.tournamentCtx?.settings.isInvitational ?? false, + ); + const id = React.useId(); + + return ( +
+ + + + No open registration or subs list. All teams must be added by the + organizer. + +
+ ); +} + +function StrictDeadlinesToggle() { + const baseEvent = useBaseEvent(); + const [strictDeadlines, setStrictDeadlines] = React.useState( + baseEvent?.tournamentCtx?.settings.deadlines === "STRICT" ? true : false, + ); + const id = React.useId(); + + return ( +
+ + + + Strict deadlines has 5 minutes less for the target time of each round + (25min Bo3, 35min Bo5 compared to 30min Bo3, 40min Bo5 normal). + +
+ ); +} + function RegClosesAtSelect() { const baseEvent = useBaseEvent(); const [regClosesAt, setRegClosesAt] = React.useState( diff --git a/app/features/tournament-bracket/components/Bracket/useDeadline.ts b/app/features/tournament-bracket/components/Bracket/useDeadline.ts index bba60d164..8907f8d15 100644 --- a/app/features/tournament-bracket/components/Bracket/useDeadline.ts +++ b/app/features/tournament-bracket/components/Bracket/useDeadline.ts @@ -13,10 +13,16 @@ const MINUTES = { BO7: 50, }; -const minutesToPlay = (count: number) => { - if (count === 3) return MINUTES.BO3; - if (count === 5) return MINUTES.BO5; - if (count === 7) return MINUTES.BO7; +const STRICT_MINUTES = { + BO3: 25, + BO5: 35, + BO7: 45, +}; + +const minutesToPlay = (count: number, strict: boolean) => { + if (count === 3) return strict ? STRICT_MINUTES.BO3 : MINUTES.BO3; + if (count === 5) return strict ? STRICT_MINUTES.BO5 : MINUTES.BO5; + if (count === 7) return strict ? STRICT_MINUTES.BO7 : MINUTES.BO7; logger.warn("Unknown best of count", { count }); return MINUTES.BO5; @@ -73,7 +79,10 @@ export function useDeadline(roundId: number, bestOf: number) { if (!dl) return null; - dl.setMinutes(dl.getMinutes() + minutesToPlay(bestOf)); + dl.setMinutes( + dl.getMinutes() + + minutesToPlay(bestOf, tournament.ctx.settings.deadlines === "STRICT"), + ); return dl; } catch (e) { diff --git a/app/features/tournament-bracket/core/Tournament.ts b/app/features/tournament-bracket/core/Tournament.ts index 986afe9fb..efd12eee6 100644 --- a/app/features/tournament-bracket/core/Tournament.ts +++ b/app/features/tournament-bracket/core/Tournament.ts @@ -9,10 +9,7 @@ import { assertUnreachable } from "~/utils/types"; import { isAdmin } from "~/permissions"; import { TOURNAMENT } from "~/features/tournament"; import type { TournamentData, TournamentDataTeam } from "./Tournament.server"; -import { - HACKY_isInviteOnlyEvent, - HACKY_resolvePicture, -} from "~/features/tournament/tournament-utils"; +import { HACKY_resolvePicture } from "~/features/tournament/tournament-utils"; import { rankedModesShort } from "~/modules/in-game-lists/modes"; import type { ModeShort } from "~/modules/in-game-lists"; import { @@ -547,10 +544,6 @@ export class Tournament { : TOURNAMENT.COUNTERPICK_MAPS_PER_MODE; } - get hasOpenRegistration() { - return !HACKY_isInviteOnlyEvent(this.ctx); - } - get hasStarted() { return this.brackets.some((bracket) => !bracket.preview); } @@ -717,13 +710,12 @@ export class Tournament { return true; } - // TODO: get from settings - private isInvitational() { - return this.ctx.name.includes("Finale"); + get isInvitational() { + return this.ctx.settings.isInvitational ?? false; } get subsFeatureEnabled() { - return !this.isInvitational(); + return !this.isInvitational; } get canAddNewSubPost() { @@ -738,7 +730,7 @@ export class Tournament { } get maxTeamMemberCount() { - const maxMembersBeforeStart = this.isInvitational() + const maxMembersBeforeStart = this.isInvitational ? 5 : TOURNAMENT.DEFAULT_TEAM_MAX_MEMBERS_BEFORE_START; diff --git a/app/features/tournament/routes/to.$id.join.tsx b/app/features/tournament/routes/to.$id.join.tsx index 865b26114..9503f3270 100644 --- a/app/features/tournament/routes/to.$id.join.tsx +++ b/app/features/tournament/routes/to.$id.join.tsx @@ -13,14 +13,10 @@ import { assertUnreachable } from "~/utils/types"; import { tournamentPage } from "~/utils/urls"; import { findByInviteCode } from "../queries/findTeamByInviteCode.server"; import { giveTrust } from "../queries/giveTrust.server"; -import hasTournamentStarted from "../queries/hasTournamentStarted.server"; import { joinTeam } from "../queries/joinLeaveTeam.server"; import { TOURNAMENT } from "../tournament-constants"; import { joinSchema } from "../tournament-schemas.server"; -import { - tournamentIdFromParams, - tournamentTeamMaxSize, -} from "../tournament-utils"; +import { tournamentIdFromParams } from "../tournament-utils"; import { useTournamentFriendCode, useTournament } from "./to.$id"; import { FriendCodeInput } from "~/components/FriendCodeInput"; import * as UserRepository from "~/features/user-page/UserRepository.server"; @@ -60,8 +56,7 @@ export const action: ActionFunction = async ({ request, params }) => { inviteCode, teamToJoin, userId: user.id, - tournamentHasStarted: tournament.hasStarted, - tournament: tournament.ctx, + maxTeamSize: tournament.maxTeamMemberCount, }) === "VALID", "Cannot join this team or invite code is invalid", ); @@ -105,15 +100,13 @@ export const action: ActionFunction = async ({ request, params }) => { throw redirect(tournamentPage(leanTeam.tournamentId)); }; -export const loader = ({ request, params }: LoaderFunctionArgs) => { - const tournamentId = tournamentIdFromParams(params); +export const loader = ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const inviteCode = url.searchParams.get("code"); return { teamId: inviteCode ? findByInviteCode(inviteCode)?.id : null, inviteCode, - tournamentHasStarted: hasTournamentStarted(tournamentId), }; }; @@ -128,11 +121,10 @@ export default function JoinTeamPage() { const teamToJoin = data.teamId ? tournament.teamById(data.teamId) : undefined; const captain = teamToJoin?.members.find((member) => member.isOwner); const validationStatus = validateCanJoin({ - tournament: tournament.ctx, inviteCode: data.inviteCode, teamToJoin, userId: user?.id, - tournamentHasStarted: data.tournamentHasStarted, + maxTeamSize: tournament.maxTeamMemberCount, }); const textPrompt = () => { @@ -195,14 +187,12 @@ function validateCanJoin({ inviteCode, teamToJoin, userId, - tournamentHasStarted, - tournament, + maxTeamSize, }: { inviteCode?: string | null; teamToJoin?: { members: { userId: number }[] }; userId?: number; - tournamentHasStarted: boolean; - tournament: { name: string }; + maxTeamSize: number; }) { if (typeof inviteCode !== "string") { return "MISSING_CODE"; @@ -216,10 +206,7 @@ function validateCanJoin({ if (!teamToJoin) { return "NO_TEAM_MATCHING_CODE"; } - if ( - teamToJoin.members.length >= - tournamentTeamMaxSize({ tournament, tournamentHasStarted }) - ) { + if (teamToJoin.members.length >= maxTeamSize) { return "TEAM_FULL"; } if (teamToJoin.members.some((member) => member.userId === userId)) { diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx index ef02efa78..13df6f069 100644 --- a/app/features/tournament/routes/to.$id.register.tsx +++ b/app/features/tournament/routes/to.$id.register.tsx @@ -64,7 +64,6 @@ import { upsertCounterpickMaps } from "../queries/upsertCounterpickMaps.server"; import { TOURNAMENT } from "../tournament-constants"; import { registerSchema } from "../tournament-schemas.server"; import { - HACKY_isInviteOnlyEvent, isOneModeTournamentOf, tournamentIdFromParams, } from "../tournament-utils"; @@ -118,7 +117,7 @@ export const action: ActionFunction = async ({ request, params }) => { teamId: data.teamId ?? null, }); } else { - validate(!HACKY_isInviteOnlyEvent(event), "Event is invite only"); + validate(!tournament.isInvitational, "Event is invite only"); validate( await UserRepository.currentFriendCodeByUserId(user.id), "No friend code", @@ -489,19 +488,19 @@ function RegistrationForms() { const ownTeam = tournament.ownedTeamByUser(user); const ownTeamCheckedIn = Boolean(ownTeam && ownTeam.checkIns.length > 0); - if (!user && tournament.hasOpenRegistration) { + if (!user && !tournament.isInvitational) { return ; } const showRegistrationProgress = () => { if (ownTeam) return true; - return tournament.hasOpenRegistration; + return !tournament.isInvitational; }; const showRegisterNewTeam = () => { if (ownTeam) return true; - if (!tournament.hasOpenRegistration) return false; + if (tournament.isInvitational) return false; if (!tournament.registrationOpen) return false; return !tournament.regularCheckInHasEnded; @@ -516,7 +515,11 @@ function RegistrationForms() { mapPool={data?.mapPool ?? undefined} members={ownTeam?.members} /> - ) : null} + ) : ( + + This tournament is invitational. Tournament organizer adds all teams. + + )} {showRegisterNewTeam() ? ( <> diff --git a/app/features/tournament/tournament-utils.ts b/app/features/tournament/tournament-utils.ts index 13cece2f2..ff99f7f8a 100644 --- a/app/features/tournament/tournament-utils.ts +++ b/app/features/tournament/tournament-utils.ts @@ -5,7 +5,6 @@ import type { ModeShort } from "~/modules/in-game-lists"; import { rankedModesShort } from "~/modules/in-game-lists/modes"; import { tournamentLogoUrl } from "~/utils/urls"; import type { PlayedSet } from "./core/sets.server"; -import { TOURNAMENT } from "./tournament-constants"; export function tournamentIdFromParams(params: Params) { const result = Number(params["id"]); @@ -54,7 +53,7 @@ export function isOneModeTournamentOf( export function HACKY_resolvePicture(event: { name: string }) { const normalizedEventName = event.name.toLowerCase(); - if (HACKY_isInviteOnlyEvent(event)) { + if (normalizedEventName.includes("sendouq")) { return tournamentLogoUrl("sf"); } @@ -166,7 +165,7 @@ const WHITE = "#fffcfc"; export function HACKY_resolveThemeColors(event: { name: string }) { const normalizedEventName = event.name.toLowerCase(); - if (HACKY_isInviteOnlyEvent(event)) { + if (normalizedEventName.includes("sendouq")) { return { bg: "#1e1e1e", text: WHITE }; } @@ -273,19 +272,6 @@ export function HACKY_resolveThemeColors(event: { name: string }) { return { bg: "#3430ad", text: WHITE }; } -const HACKY_isSendouQSeasonFinale = (event: { name: string }) => - event.name.includes("Finale"); - -export function HACKY_isInviteOnlyEvent(event: { name: string }) { - return HACKY_isSendouQSeasonFinale(event); -} - -export function HACKY_maxRosterSizeBeforeStart(event: { name: string }) { - if (HACKY_isSendouQSeasonFinale(event)) return 5; - - return TOURNAMENT.DEFAULT_TEAM_MAX_MEMBERS_BEFORE_START; -} - export function tournamentRoundI18nKey(round: PlayedSet["round"]) { if (round.round === "grand_finals") return `bracket.grand_finals`; if (round.round === "bracket_reset") { @@ -295,16 +281,3 @@ export function tournamentRoundI18nKey(round: PlayedSet["round"]) { return `bracket.${round.type}` as const; } - -export function tournamentTeamMaxSize({ - tournament, - tournamentHasStarted, -}: { - tournament: { name: string }; - tournamentHasStarted: boolean; -}) { - // ensuring every team can add at least one sub while the tournament is ongoing - return ( - HACKY_maxRosterSizeBeforeStart(tournament) + Number(tournamentHasStarted) - ); -}