Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 3m40s
117 lines
3.5 KiB
TypeScript
117 lines
3.5 KiB
TypeScript
import { z } from "zod";
|
|
import type { RoomView } from "./multiplayer";
|
|
|
|
const safeName = z
|
|
.string()
|
|
.trim()
|
|
.min(3)
|
|
.max(80)
|
|
.refine(
|
|
(value) =>
|
|
!/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(value),
|
|
"Names cannot contain hidden controls",
|
|
);
|
|
export const tournamentSettings = z
|
|
.object({
|
|
name: safeName,
|
|
datasetId: z.string().uuid(),
|
|
difficulty: z.union([
|
|
z.enum(["easy", "normal", "hard", "void"]),
|
|
z.string().uuid(),
|
|
]),
|
|
playerLimit: z.number().int().min(2).max(32),
|
|
roundCount: z.number().int().min(1).max(10),
|
|
roundTimeSeconds: z.number().int().min(10).max(3000),
|
|
timerMode: z.enum(["shared", "perMini"]).default("shared"),
|
|
startsAt: z.string().datetime({ offset: true }),
|
|
announceDiscord: z.boolean(),
|
|
})
|
|
.strict()
|
|
.refine(
|
|
(value) =>
|
|
value.difficulty === "void"
|
|
? value.timerMode === "perMini" && value.roundTimeSeconds === 10
|
|
: value.roundTimeSeconds >= 15 && value.roundTimeSeconds % 15 === 0,
|
|
"Void requires a separate 10-second timer; other tournament timers use 15-second steps",
|
|
)
|
|
.refine(
|
|
(value) => value.timerMode !== "perMini" || value.roundTimeSeconds <= 600,
|
|
"Mini-round timers allow at most 600 seconds",
|
|
);
|
|
export const tournamentEdit = z
|
|
.object({
|
|
startsAt: z.string().datetime({ offset: true }),
|
|
playerLimit: z.number().int().min(2).max(32),
|
|
announceDiscord: z.boolean(),
|
|
})
|
|
.strict();
|
|
export type TournamentRecord = {
|
|
id: string;
|
|
name: string;
|
|
owner_id: string | null;
|
|
starts_at: string;
|
|
server_timezone: string;
|
|
player_limit: number;
|
|
round_count: number;
|
|
round_time_seconds: number;
|
|
timer_mode: "shared" | "perMini";
|
|
status: "scheduled" | "running" | "finished" | "cancelled";
|
|
announce_discord: boolean;
|
|
finished_at: string | null;
|
|
winner_id: string | null;
|
|
};
|
|
export type TournamentView = {
|
|
id: string;
|
|
name: string;
|
|
ownerId: string | null;
|
|
datasetId: string;
|
|
startsAt: string;
|
|
serverTimezone: string;
|
|
playerLimit: number;
|
|
roundCount: number;
|
|
roundTimeSeconds: number;
|
|
timerMode: "shared" | "perMini";
|
|
roundDeadline?: string | null;
|
|
announceDiscord: boolean;
|
|
status: TournamentRecord["status"];
|
|
currentRound: number;
|
|
finishedAt: string | null;
|
|
winnerId: string | null;
|
|
you: string | null;
|
|
roomId: string | null;
|
|
currentRoom?: RoomView | null;
|
|
announcementState?:
|
|
| "pending"
|
|
| "sending"
|
|
| "sent"
|
|
| "skipped"
|
|
| "failed"
|
|
| null;
|
|
serverNow: number;
|
|
players: { accountId: string; name: string; total: number; rank: number }[];
|
|
};
|
|
|
|
/** Interpret a datetime-local value in the advertised server zone, never the browser zone. */
|
|
export function serverLocalToIso(value: string, zone: string) {
|
|
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(value))
|
|
throw new Error("Choose a valid start time");
|
|
const target = Date.parse(value + ":00Z");
|
|
if (!Number.isFinite(target)) throw new Error("Choose a valid start time");
|
|
const formatter = new Intl.DateTimeFormat("sv-SE", {
|
|
timeZone: zone,
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hourCycle: "h23",
|
|
});
|
|
let instant = target;
|
|
for (let i = 0; i < 4; i++) {
|
|
const shown = formatter.format(new Date(instant)).replace(" ", "T");
|
|
if (shown === value) return new Date(instant).toISOString();
|
|
instant += target - Date.parse(shown + ":00Z");
|
|
}
|
|
throw new Error("That local time does not exist in the server timezone");
|
|
}
|