fix: update match configuration to bestOf 5 and scoringType to score_v2

This commit is contained in:
2026-02-28 06:38:05 +01:00
parent e99444b157
commit fb9140a03f
555 changed files with 124715 additions and 1 deletions

View File

@@ -0,0 +1,129 @@
import { d as db, u as user } from "../../../../../chunks/index2.js";
import { a as cancelMatch, g as getMatchFull, p as pickMap, s as submitRoll } from "../../../../../chunks/engine.js";
import { c as closeLobby, f as forceStartGame, p as playPickedMap, g as getLobby } from "../../../../../chunks/orchestrator.js";
import { redirect, error } from "@sveltejs/kit";
import { g as getBeatmap } from "../../../../../chunks/api.js";
import { eq } from "drizzle-orm";
const load = async ({ params, locals }) => {
if (!locals.user) redirect(302, "/");
let m;
try {
m = await getMatchFull(params.id);
} catch {
error(404, "Match not found");
}
const beatmapCache = {};
if (m.mappool?.slots) {
await Promise.allSettled(
m.mappool.slots.map(async (slot) => {
try {
const bm = await getBeatmap(slot.beatmapId);
beatmapCache[slot.beatmapId] = {
title: bm.beatmapset.title,
artist: bm.beatmapset.artist,
version: bm.version,
starRating: bm.difficulty_rating,
bpm: bm.bpm,
totalLength: bm.total_length,
coverUrl: bm.beatmapset.covers["card@2x"],
listCoverUrl: bm.beatmapset.covers["list@2x"]
};
} catch {
}
})
);
}
return { match: m, beatmapCache, userId: locals.user.id };
};
const actions = {
reinvite: async ({ params, locals }) => {
if (!locals.user) redirect(302, "/");
const lobby = getLobby(params.id);
if (!lobby) return { error: "No active IRC lobby for this match" };
const m = await getMatchFull(params.id);
const invited = [];
for (const p of m.participants) {
for (const pl of p.players) {
const u = await db.query.user.findFirst({ where: eq(user.id, pl.userId) });
if (u?.name) {
await lobby.invite(u.name);
invited.push(u.name);
}
}
}
return { reinvited: invited };
},
roll: async ({ params, locals }) => {
if (!locals.user) redirect(302, "/");
const m = await getMatchFull(params.id);
let myParticipant = m.participants.find(
(p) => p.players.some((pl) => pl.userId === locals.user.id)
);
if (!myParticipant) {
return { error: "You are not in this match" };
}
if (myParticipant.rollValue !== null) {
const unrolled = m.participants.find((p) => p.rollValue === null);
if (unrolled) {
myParticipant = unrolled;
} else {
return { error: "All participants have already rolled" };
}
}
const value = Math.floor(Math.random() * 100) + 1;
await submitRoll(params.id, myParticipant.id, value);
const lobby = getLobby(params.id);
if (lobby) {
lobby.chat(`${myParticipant.team?.name ?? "Player"} rolled ${value} (via web)`).catch(() => {
});
const updated = await getMatchFull(params.id);
if (updated.state === "PICKING") {
const sorted = [...updated.participants].sort(
(a, b) => (a.pickOrder ?? 99) - (b.pickOrder ?? 99)
);
lobby.chat(
`Rolls complete! ${sorted[0]?.team.name} picks first. Use !pick <slot> (e.g. !pick NM1) or pick in web UI.`
).catch(() => {
});
}
}
return { rolled: value, participantId: myParticipant.id };
},
pick: async ({ params, request, locals }) => {
if (!locals.user) redirect(302, "/");
const form = await request.formData();
const slotId = form.get("slotId")?.toString();
if (!slotId) return { error: "No map selected" };
const m = await getMatchFull(params.id);
const sorted = [...m.participants].sort(
(a, b) => (a.pickOrder ?? 99) - (b.pickOrder ?? 99)
);
const expectedIdx = m.games.length % sorted.length;
const expectedPicker = sorted[expectedIdx];
if (!expectedPicker) return { error: "Cannot determine picker" };
try {
const game = await pickMap(params.id, expectedPicker.id, slotId);
playPickedMap(params.id, game.id).catch(
(err) => console.error("[Match] IRC play failed:", err.message)
);
return { picked: true, gameId: game.id };
} catch (e) {
return { error: e.message };
}
},
forceStart: async ({ params, locals }) => {
if (!locals.user) redirect(302, "/");
await forceStartGame(params.id);
return { started: true };
},
cancel: async ({ params, locals }) => {
if (!locals.user) redirect(302, "/");
await cancelMatch(params.id);
await closeLobby(params.id);
redirect(303, "/matches");
}
};
export {
actions,
load
};

View File

@@ -0,0 +1,323 @@
import { e as escape_html, b as attr, s as stringify, c as attr_class, a as ensure_array_like, d as derived } from "../../../../../chunks/index3.js";
import "@sveltejs/kit/internal";
import "../../../../../chunks/url.js";
import "../../../../../chunks/utils.js";
import "@sveltejs/kit/internal/server";
import "../../../../../chunks/root.js";
import "../../../../../chunks/exports.js";
import "../../../../../chunks/state.svelte.js";
function _page($$renderer, $$props) {
$$renderer.component(($$renderer2) => {
let { data } = $$props;
let m = derived(() => data.match);
let p1 = derived(() => m().participants[0]);
let p2 = derived(() => m().participants[1]);
const config = derived(() => m().config);
const winsNeeded = derived(() => Math.ceil(config().bestOf / 2));
const playedSlotIds = derived(() => new Set(m().games.map((g) => g.mappoolSlotId)));
const sortedByPick = derived(() => [...m().participants].sort((a, b) => (a.pickOrder ?? 99) - (b.pickOrder ?? 99)));
const expectedPickerIdx = derived(() => m().games.length % sortedByPick().length);
const expectedPicker = derived(() => sortedByPick()[expectedPickerIdx()]);
const canRoll = derived(() => m().state === "ROLLING" && m().participants.some((p) => p.rollValue === null));
const catColors = {
NM: "bg-blue-500/20 text-blue-400 border-blue-500/30",
HD: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30",
HR: "bg-red-500/20 text-red-400 border-red-500/30",
DT: "bg-purple-500/20 text-purple-400 border-purple-500/30",
FM: "bg-green-500/20 text-green-400 border-green-500/30",
TB: "bg-pink-500/20 text-pink-400 border-pink-500/30"
};
const groupedSlots = derived(() => () => {
if (!m().mappool?.slots) return {};
const groups = {};
for (const slot of m().mappool.slots) {
if (!groups[slot.category]) groups[slot.category] = [];
groups[slot.category].push(slot);
}
for (const cat of Object.keys(groups)) {
groups[cat].sort((a, b) => a.orderInCategory - b.orderInCategory);
}
return groups;
});
let rolling = false;
let picking = false;
let forceStarting = false;
function formatLength(seconds) {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, "0")}`;
}
function stateLabel(s) {
return {
CREATED: "Waiting",
LOBBY: "In Lobby",
ROLLING: "Rolling",
PICKING: "Pick Phase",
PLAYING: "Playing",
FINISHED: "Finished",
CANCELLED: "Cancelled"
}[s] ?? s;
}
function stateBorder(s) {
return {
ROLLING: "border-yellow-500/30",
PICKING: "border-blue-500/30",
PLAYING: "border-green-500/30",
FINISHED: "border-accent/30"
}[s] ?? "border-border";
}
$$renderer2.push(`<div class="mx-auto max-w-5xl"><div class="flex items-center gap-3"><a href="/matches" class="text-text-secondary transition-colors hover:text-text-primary">←</a> <div class="flex-1"><h1 class="text-xl font-700 tracking-tight">${escape_html(m().name || "Match")}</h1> <p class="mt-0.5 text-xs text-text-secondary">Best of ${escape_html(config().bestOf)} · First to ${escape_html(winsNeeded())} `);
if (m().osuLobbyId) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`· <a${attr("href", `https://osu.ppy.sh/mp/${stringify(m().osuLobbyId)}`)} target="_blank" class="text-accent hover:underline">osu! mp/${escape_html(m().osuLobbyId)}</a>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--></p></div> <span${attr_class(`rounded border px-2.5 py-1 text-xs font-600 ${stringify(stateBorder(m().state))}`)}>${escape_html(stateLabel(m().state))}</span> `);
if (!["FINISHED", "CANCELLED"].includes(m().state)) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<form method="post" action="?/cancel"><button type="submit" class="rounded-md border border-red-500/30 px-3 py-1.5 text-xs text-red-400 hover:bg-red-500/10">Cancel</button></form>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--></div> `);
if (p1() && p2()) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<div class="mt-6 flex items-center gap-4"><div${attr_class(`flex flex-1 items-center gap-4 rounded-lg border p-4 ${stringify(m().winnerId === p1().teamId ? "border-green-500/40 bg-green-500/5" : "border-border bg-surface-800")}`)}>`);
if (p1().team.avatarUrl) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<img${attr("src", p1().team.avatarUrl)} alt="" class="h-10 w-10 rounded-full"/>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> <div class="flex-1"><p class="text-sm font-600">${escape_html(p1().team.name)}</p> `);
if (p1().rollValue != null) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<p class="text-xs text-text-secondary">Roll: ${escape_html(p1().rollValue)}${escape_html(p1().pickOrder === 1 ? " ★" : "")}</p>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--></div> <span${attr_class(`text-3xl font-800 tabular-nums ${stringify(p1().score >= winsNeeded() ? "text-green-400" : "text-text-primary")}`)}>${escape_html(p1().score)}</span></div> <span class="text-lg font-700 text-text-secondary">vs</span> <div${attr_class(`flex flex-1 items-center gap-4 rounded-lg border p-4 ${stringify(m().winnerId === p2().teamId ? "border-green-500/40 bg-green-500/5" : "border-border bg-surface-800")}`)}><span${attr_class(`text-3xl font-800 tabular-nums ${stringify(p2().score >= winsNeeded() ? "text-green-400" : "text-text-primary")}`)}>${escape_html(p2().score)}</span> <div class="flex-1 text-right"><p class="text-sm font-600">${escape_html(p2().team.name)}</p> `);
if (p2().rollValue != null) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<p class="text-xs text-text-secondary">${escape_html(p2().pickOrder === 1 ? "★ " : "")}Roll: ${escape_html(p2().rollValue)}</p>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--></div> `);
if (p2().team.avatarUrl) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<img${attr("src", p2().team.avatarUrl)} alt="" class="h-10 w-10 rounded-full"/>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--></div></div>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> `);
if (["LOBBY", "ROLLING", "PICKING", "PLAYING"].includes(m().state)) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<div class="mt-3 flex justify-center"><form method="post" action="?/reinvite"><button type="submit" class="rounded-md border border-border px-3 py-1.5 text-xs text-text-secondary transition-colors hover:border-accent/40 hover:text-accent">📨 Re-invite players to lobby</button></form></div>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> `);
if (m().state === "ROLLING") {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"><div class="w-full max-w-md rounded-xl border border-yellow-500/30 bg-surface-800 p-8 text-center shadow-2xl"><h2 class="text-lg font-700">🎲 Roll Phase</h2> <p class="mt-2 text-sm text-text-secondary">Highest roll picks first</p> <div class="mt-6 flex justify-center gap-6"><!--[-->`);
const each_array = ensure_array_like(m().participants);
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
let p = each_array[$$index];
$$renderer2.push(`<div class="rounded-lg border border-border bg-surface-700 px-6 py-4"><p class="text-xs text-text-secondary">${escape_html(p.team.name)}</p> `);
if (p.rollValue != null) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<p class="mt-1 text-2xl font-800 text-yellow-400">${escape_html(p.rollValue)}</p>`);
} else {
$$renderer2.push("<!--[!-->");
$$renderer2.push(`<p class="mt-1 text-2xl font-800 text-text-secondary/30">—</p>`);
}
$$renderer2.push(`<!--]--></div>`);
}
$$renderer2.push(`<!--]--></div> `);
if (canRoll()) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<form method="post" action="?/roll"><button type="submit"${attr("disabled", rolling, true)} class="mt-6 rounded-md bg-accent px-8 py-3 text-sm font-600 text-surface-900 transition-colors hover:bg-accent-hover disabled:opacity-50">${escape_html("🎲 Roll!")}</button></form> <p class="mt-3 text-xs text-text-secondary">or type <code class="rounded bg-surface-700 px-1.5 py-0.5 font-mono text-accent">!roll</code> in osu! chat</p>`);
} else {
$$renderer2.push("<!--[!-->");
$$renderer2.push(`<p class="mt-6 text-sm text-text-secondary animate-pulse">Waiting for all rolls...</p>`);
}
$$renderer2.push(`<!--]--></div></div>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> `);
if (m().state === "PICKING") {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"><div class="w-full max-w-3xl max-h-[80vh] overflow-y-auto rounded-xl border border-blue-500/30 bg-surface-800 p-6 shadow-2xl"><div class="text-center"><h2 class="text-lg font-700">🎯 Pick a Map</h2> <p class="mt-1 text-sm"><span class="font-600 text-accent">${escape_html(expectedPicker()?.team.name)}</span>'s turn to pick</p> <p class="mt-1 text-xs text-text-secondary">or type <code class="rounded bg-surface-700 px-1.5 py-0.5 font-mono text-accent">!pick NM1</code> in osu! chat</p></div> `);
{
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> <div class="mt-6 flex flex-col gap-4"><!--[-->`);
const each_array_1 = ensure_array_like(Object.entries(groupedSlots()()));
for (let $$index_2 = 0, $$length = each_array_1.length; $$index_2 < $$length; $$index_2++) {
let [category, slots] = each_array_1[$$index_2];
$$renderer2.push(`<div><span${attr_class(`mb-2 inline-block rounded border px-2 py-0.5 text-xs font-600 ${stringify(catColors[category] ?? "border-border")}`)}>${escape_html(category)}</span> <div class="grid grid-cols-1 gap-2 sm:grid-cols-2"><!--[-->`);
const each_array_2 = ensure_array_like(slots);
for (let i = 0, $$length2 = each_array_2.length; i < $$length2; i++) {
let slot = each_array_2[i];
const bm = data.beatmapCache[slot.beatmapId];
const isPlayed = playedSlotIds().has(slot.id);
$$renderer2.push(`<form method="post" action="?/pick"><input type="hidden" name="slotId"${attr("value", slot.id)}/> <button type="submit"${attr("disabled", isPlayed || picking, true)}${attr_class(`flex w-full items-center gap-3 rounded-lg border p-2 text-left transition-all ${stringify(isPlayed ? "border-border/50 bg-surface-900 opacity-30 cursor-not-allowed" : "border-border bg-surface-700 hover:border-accent hover:bg-surface-600 cursor-pointer")}`)}>`);
if (bm?.listCoverUrl) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<img${attr("src", bm.listCoverUrl)} alt="" class="h-10 w-20 rounded object-cover"/>`);
} else {
$$renderer2.push("<!--[!-->");
$$renderer2.push(`<div class="flex h-10 w-20 items-center justify-center rounded bg-surface-600 text-xs text-text-secondary">?</div>`);
}
$$renderer2.push(`<!--]--> <div class="min-w-0 flex-1">`);
if (bm) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<p class="truncate text-xs font-500">${escape_html(bm.artist)} - ${escape_html(bm.title)}</p> <p class="text-xs text-text-secondary">[${escape_html(bm.version)}] · ★${escape_html(bm.starRating.toFixed(1))} · ${escape_html(bm.bpm)}bpm</p>`);
} else {
$$renderer2.push("<!--[!-->");
$$renderer2.push(`<p class="text-xs text-text-secondary">#${escape_html(slot.beatmapId)}</p>`);
}
$$renderer2.push(`<!--]--></div> <span class="text-xs font-600 text-text-secondary">${escape_html(category)}${escape_html(slot.orderInCategory)}</span> `);
if (isPlayed) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<span class="text-xs text-text-secondary">✓</span>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--></button></form>`);
}
$$renderer2.push(`<!--]--></div></div>`);
}
$$renderer2.push(`<!--]--></div></div></div>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> `);
if (m().state === "PLAYING") {
$$renderer2.push("<!--[-->");
const currentGame = m().games[m().games.length - 1];
const bm = currentGame ? data.beatmapCache[currentGame.slot?.beatmapId] : null;
$$renderer2.push(`<div class="mt-6 rounded-lg border border-green-500/30 bg-green-500/5 p-5 text-center"><div class="flex items-center justify-center gap-2"><div class="relative h-3 w-3"><div class="absolute inset-0 animate-ping rounded-full bg-green-400 opacity-75"></div> <div class="relative h-3 w-3 rounded-full bg-green-400"></div></div> <span class="text-sm font-600 text-green-400">Now Playing</span></div> `);
if (bm) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<p class="mt-2 text-sm">${escape_html(bm.artist)} - ${escape_html(bm.title)} <span class="text-text-secondary">[${escape_html(bm.version)}]</span></p> <p class="mt-1 text-xs text-text-secondary">★${escape_html(bm.starRating.toFixed(1))} · ${escape_html(bm.bpm)}bpm · ${escape_html(formatLength(bm.totalLength))}</p>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> <p class="mt-3 text-xs text-text-secondary animate-pulse">Waiting for players to ready up &amp; play in osu!...</p> <form method="post" action="?/forceStart"><button type="submit"${attr("disabled", forceStarting, true)} class="mt-4 rounded-md border border-yellow-500/30 px-4 py-2 text-xs font-600 text-yellow-400 transition-colors hover:bg-yellow-500/10 disabled:opacity-50">${escape_html("Force Start (skip ready)")}</button></form></div>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> `);
if (m().state === "FINISHED") {
$$renderer2.push("<!--[-->");
const winner = m().participants.find((p) => p.teamId === m().winnerId);
$$renderer2.push(`<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"><div class="w-full max-w-md rounded-xl border border-accent/30 bg-surface-800 p-8 text-center shadow-2xl"><div class="text-4xl">🏆</div> <h2 class="mt-4 text-2xl font-800"><span class="text-accent">${escape_html(winner?.team.name)}</span> wins!</h2> <p class="mt-2 text-lg font-700 tabular-nums text-text-secondary">${escape_html(p1()?.score)} – ${escape_html(p2()?.score)}</p> <div class="mt-6 flex flex-col gap-1"><!--[-->`);
const each_array_3 = ensure_array_like(m().games);
for (let $$index_3 = 0, $$length = each_array_3.length; $$index_3 < $$length; $$index_3++) {
let game = each_array_3[$$index_3];
const bm = data.beatmapCache[game.slot?.beatmapId];
const winnerP = m().participants.find((p) => p.id === game.winnerParticipantId);
$$renderer2.push(`<div class="flex items-center gap-2 rounded bg-surface-700 px-3 py-1.5 text-xs"><span${attr_class(`font-600 ${stringify(catColors[game.slot?.category]?.split(" ")[1] ?? "text-text-secondary")}`)}>${escape_html(game.slot?.category)}${escape_html(game.slot?.orderInCategory)}</span> <span class="flex-1 truncate text-text-secondary">${escape_html(bm ? `${bm.artist} - ${bm.title}` : `#${game.slot?.beatmapId}`)}</span> <span${attr_class(`font-600 ${stringify(winnerP?.id === p1()?.id ? "text-blue-400" : "text-red-400")}`)}>${escape_html(winnerP?.team.name)}</span></div>`);
}
$$renderer2.push(`<!--]--></div> <a href="/" class="mt-6 inline-block rounded-md bg-accent px-6 py-2.5 text-sm font-600 text-surface-900 transition-colors hover:bg-accent-hover">Back to Dashboard</a></div></div>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> `);
if (m().games.length > 0) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<div class="mt-6"><h2 class="text-sm font-600">Games Played</h2> <div class="mt-3 flex flex-col gap-2"><!--[-->`);
const each_array_4 = ensure_array_like(m().games);
for (let $$index_4 = 0, $$length = each_array_4.length; $$index_4 < $$length; $$index_4++) {
let game = each_array_4[$$index_4];
const bm = data.beatmapCache[game.slot?.beatmapId];
const pickerTeam = m().participants.find((p) => p.id === game.pickedByParticipantId)?.team;
const winnerTeam = m().participants.find((p) => p.id === game.winnerParticipantId)?.team;
$$renderer2.push(`<div class="flex items-center gap-3 rounded-lg border border-border bg-surface-800 p-3"><span class="w-6 text-center text-xs font-600 text-text-secondary">${escape_html(game.gameNumber)}</span> `);
if (bm?.listCoverUrl) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<img${attr("src", bm.listCoverUrl)} alt="" class="h-8 w-16 rounded object-cover"/>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> <div class="min-w-0 flex-1">`);
if (bm) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<p class="truncate text-sm font-500">${escape_html(bm.artist)} - ${escape_html(bm.title)} <span class="text-text-secondary">[${escape_html(bm.version)}]</span></p>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> <div class="flex items-center gap-2 text-xs text-text-secondary"><span${attr_class(`rounded border px-1.5 py-0.5 ${stringify(catColors[game.slot?.category] ?? "border-border")}`)}>${escape_html(game.slot?.category)}${escape_html(game.slot?.orderInCategory)}</span> `);
if (pickerTeam) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<span>Picked by ${escape_html(pickerTeam.name)}</span>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--></div></div> `);
if (game.state === "FINISHED" && p1() && p2()) {
$$renderer2.push("<!--[-->");
const p1Score = game.scores.filter((s) => s.player?.participantId === p1().id).reduce((sum, s) => sum + s.score, 0);
const p2Score = game.scores.filter((s) => s.player?.participantId === p2().id).reduce((sum, s) => sum + s.score, 0);
$$renderer2.push(`<div class="flex items-center gap-2 font-mono text-sm tabular-nums"><span${attr_class(p1Score > p2Score ? "font-700 text-green-400" : "text-text-secondary")}>${escape_html(p1Score.toLocaleString())}</span> <span class="text-text-secondary">-</span> <span${attr_class(p2Score > p1Score ? "font-700 text-green-400" : "text-text-secondary")}>${escape_html(p2Score.toLocaleString())}</span></div>`);
} else if (game.state === "PLAYING") {
$$renderer2.push("<!--[1-->");
$$renderer2.push(`<span class="text-xs text-green-400 animate-pulse">Live</span>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> `);
if (winnerTeam) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<span class="text-xs text-green-400">✓ ${escape_html(winnerTeam.name)}</span>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--></div>`);
}
$$renderer2.push(`<!--]--></div></div>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> `);
if (m().state === "CREATED") {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<div class="mt-6 rounded-lg border border-border bg-surface-800 p-6 text-center"><p class="text-sm text-text-secondary animate-pulse">Creating osu! lobby...</p></div>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> `);
if (m().state === "LOBBY") {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<div class="mt-6 rounded-lg border border-yellow-500/30 bg-yellow-500/5 p-6 text-center"><p class="text-sm text-text-secondary">Lobby created. Waiting for players to join...</p> `);
if (m().osuLobbyId) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<p class="mt-2 text-xs text-text-secondary"><a${attr("href", `https://osu.ppy.sh/mp/${stringify(m().osuLobbyId)}`)} target="_blank" class="text-accent hover:underline">osu! mp/${escape_html(m().osuLobbyId)}</a></p>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--></div>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> `);
if (m().state === "CANCELLED") {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<div class="mt-6 rounded-lg border border-red-500/30 bg-red-500/5 p-6 text-center"><p class="text-sm text-red-400">This match was cancelled.</p></div>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--></div>`);
});
}
export {
_page as default
};

View File

@@ -0,0 +1,58 @@
import { d as db, b as match } from "../../../../chunks/index2.js";
import { redirect } from "@sveltejs/kit";
import { desc } from "drizzle-orm";
import { c as createMatch } from "../../../../chunks/engine.js";
import { i as initMatchLobby } from "../../../../chunks/orchestrator.js";
const load = async ({ locals }) => {
if (!locals.user) redirect(302, "/");
const matches = await db.query.match.findMany({
with: { participants: { with: { team: true } } },
orderBy: desc(match.createdAt),
limit: 50
});
const teams = await db.query.team.findMany({
orderBy: (t, { asc }) => [asc(t.name)]
});
const mappools = await db.query.mappool.findMany({
with: { slots: true },
orderBy: (m, { desc: desc2 }) => [desc2(m.createdAt)]
});
return { matches, teams, mappools };
};
const actions = {
createMatch: async ({ request, locals }) => {
if (!locals.user) redirect(302, "/");
const form = await request.formData();
const name = form.get("name")?.toString()?.trim() || "Custom Match";
const team1Id = form.get("team1")?.toString();
const team2Id = form.get("team2")?.toString();
const mappoolId = form.get("mappool")?.toString();
const bestOf = parseInt(form.get("bestOf")?.toString() ?? "7");
console.log("[Matches] CREATE:", { name, team1Id, team2Id, mappoolId, bestOf });
if (!team1Id || !team2Id || !mappoolId) {
return { error: "All fields are required" };
}
let result;
try {
result = await createMatch({
name,
config: { bestOf, teamSize: 1, scoringType: "score" },
mappoolId,
teams: [team1Id, team2Id],
createdBy: locals.user.id
});
console.log("[Matches] Created:", result.id);
} catch (e) {
console.error("[Matches] Error:", e);
return { error: e.message };
}
initMatchLobby(result.id).catch((err) => {
console.error("[Matches] IRC lobby failed:", err.message);
});
redirect(303, `/matches/${result.id}`);
}
};
export {
actions,
load
};

View File

@@ -0,0 +1,164 @@
import { e as escape_html, a as ensure_array_like, b as attr, s as stringify, c as attr_class, d as derived } from "../../../../chunks/index3.js";
import "@sveltejs/kit/internal";
import "../../../../chunks/url.js";
import "../../../../chunks/utils.js";
import "@sveltejs/kit/internal/server";
import "../../../../chunks/root.js";
import "../../../../chunks/exports.js";
import "../../../../chunks/state.svelte.js";
function _page($$renderer, $$props) {
$$renderer.component(($$renderer2) => {
let { data } = $$props;
const stateConfig = {
CREATED: { label: "Created", color: "text-text-secondary border-border" },
LOBBY: {
label: "In Lobby",
color: "text-yellow-400 border-yellow-500/30"
},
ROLLING: {
label: "Rolling",
color: "text-yellow-400 border-yellow-500/30",
dot: "bg-yellow-400"
},
PICKING: {
label: "Picking",
color: "text-blue-400 border-blue-500/30",
dot: "bg-blue-400"
},
PLAYING: {
label: "Live",
color: "text-green-400 border-green-500/30",
dot: "bg-green-400"
},
FINISHED: {
label: "Finished",
color: "text-text-secondary border-border"
},
CANCELLED: { label: "Cancelled", color: "text-red-400 border-red-500/30" }
};
const liveStates = ["LOBBY", "ROLLING", "PICKING", "PLAYING"];
const liveMatches = derived(() => data.matches.filter((m) => liveStates.includes(m.state)));
const recentMatches = derived(() => data.matches.filter((m) => !liveStates.includes(m.state)));
function teamDisplay(m) {
const p1 = m.participants[0];
const p2 = m.participants[1];
return { p1, p2 };
}
function timeAgo(date) {
const d = new Date(date);
const diff = Date.now() - d.getTime();
const mins = Math.floor(diff / 6e4);
if (mins < 1) return "just now";
if (mins < 60) return `${mins}m ago`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
$$renderer2.push(`<div class="mx-auto max-w-5xl"><div class="flex items-center justify-between"><div><h1 class="text-2xl font-700 tracking-tight">Matches</h1> <p class="mt-1 text-sm text-text-secondary">${escape_html(data.matches.length)} match${escape_html(data.matches.length !== 1 ? "es" : "")} `);
if (liveMatches().length > 0) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`· <span class="text-green-400">${escape_html(liveMatches().length)} live</span>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--></p></div> <button class="rounded-md bg-accent px-4 py-2 text-sm font-600 text-surface-900 transition-colors hover:bg-accent-hover">${escape_html("New Match")}</button></div> `);
{
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> `);
if (liveMatches().length > 0) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<div class="mt-6"><h2 class="flex items-center gap-2 text-sm font-600"><div class="relative h-2 w-2"><div class="absolute inset-0 animate-ping rounded-full bg-green-400 opacity-75"></div> <div class="relative h-2 w-2 rounded-full bg-green-400"></div></div> Live Now</h2> <div class="mt-3 flex flex-col gap-2"><!--[-->`);
const each_array_4 = ensure_array_like(liveMatches());
for (let $$index_4 = 0, $$length = each_array_4.length; $$index_4 < $$length; $$index_4++) {
let m = each_array_4[$$index_4];
const { p1, p2 } = teamDisplay(m);
const sc = stateConfig[m.state];
$$renderer2.push(`<a${attr("href", `/matches/${stringify(m.id)}`)} class="group flex items-center gap-4 rounded-lg border border-green-500/20 bg-surface-800 p-4 transition-all hover:border-green-500/40 hover:bg-surface-700"><div class="flex flex-1 items-center gap-3">`);
if (p1?.team.avatarUrl) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<img${attr("src", p1.team.avatarUrl)} alt="" class="h-8 w-8 rounded-full"/>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> <span class="text-sm font-600">${escape_html(p1?.team.name ?? "?")}</span></div> <div class="flex items-center gap-3"><span class="text-xl font-800 tabular-nums">${escape_html(p1?.score ?? 0)}</span> <span class="text-xs font-600 text-text-secondary">vs</span> <span class="text-xl font-800 tabular-nums">${escape_html(p2?.score ?? 0)}</span></div> <div class="flex flex-1 items-center justify-end gap-3"><span class="text-sm font-600">${escape_html(p2?.team.name ?? "?")}</span> `);
if (p2?.team.avatarUrl) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<img${attr("src", p2.team.avatarUrl)} alt="" class="h-8 w-8 rounded-full"/>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--></div> <span${attr_class(`flex items-center gap-1.5 rounded border px-2 py-0.5 text-xs font-500 ${stringify(sc?.color ?? "border-border")}`)}>`);
if (sc?.dot) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<span${attr_class(`h-1.5 w-1.5 rounded-full ${stringify(sc.dot)}`)}></span>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> ${escape_html(sc?.label ?? m.state)}</span></a>`);
}
$$renderer2.push(`<!--]--></div></div>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> <div class="mt-6">`);
if (liveMatches().length > 0) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<h2 class="text-sm font-600 text-text-secondary">Past Matches</h2>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> <div class="mt-3 flex flex-col gap-2">`);
const each_array_5 = ensure_array_like(recentMatches());
if (each_array_5.length !== 0) {
$$renderer2.push("<!--[-->");
for (let $$index_5 = 0, $$length = each_array_5.length; $$index_5 < $$length; $$index_5++) {
let m = each_array_5[$$index_5];
const { p1, p2 } = teamDisplay(m);
const config = m.config;
const sc = stateConfig[m.state];
$$renderer2.push(`<a${attr("href", `/matches/${stringify(m.id)}`)} class="group flex items-center gap-4 rounded-lg border border-border bg-surface-800 p-3 transition-colors hover:border-accent/30 hover:bg-surface-700"><div class="min-w-0 flex-1"><div class="flex items-center gap-2"><span class="text-sm font-600">${escape_html(p1?.team.name ?? "?")}</span> `);
if (m.state === "FINISHED") {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<span${attr_class(`text-xs font-700 tabular-nums ${stringify((p1?.score ?? 0) > (p2?.score ?? 0) ? "text-green-400" : "text-text-secondary")}`)}>${escape_html(p1?.score ?? 0)}</span> <span class="text-xs text-text-secondary">-</span> <span${attr_class(`text-xs font-700 tabular-nums ${stringify((p2?.score ?? 0) > (p1?.score ?? 0) ? "text-green-400" : "text-text-secondary")}`)}>${escape_html(p2?.score ?? 0)}</span>`);
} else {
$$renderer2.push("<!--[!-->");
$$renderer2.push(`<span class="text-xs text-text-secondary">vs</span>`);
}
$$renderer2.push(`<!--]--> <span class="text-sm font-600">${escape_html(p2?.team.name ?? "?")}</span></div> <p class="mt-0.5 text-xs text-text-secondary">${escape_html(m.name ? `${m.name} · ` : "")}BO${escape_html(config.bestOf)} `);
if (m.finishedAt) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`· ${escape_html(timeAgo(m.finishedAt))}`);
} else if (m.createdAt) {
$$renderer2.push("<!--[1-->");
$$renderer2.push(`· ${escape_html(timeAgo(m.createdAt))}`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--></p></div> `);
if (m.state === "FINISHED" && m.winnerId) {
$$renderer2.push("<!--[-->");
const winner = m.participants.find((p) => p.teamId === m.winnerId);
$$renderer2.push(`<span class="text-xs font-500 text-green-400">🏆 ${escape_html(winner?.team.name)}</span>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]--> <span${attr_class(`rounded border px-2 py-0.5 text-xs font-500 ${stringify(sc?.color ?? "border-border")}`)}>${escape_html(sc?.label ?? m.state)}</span></a>`);
}
} else {
$$renderer2.push("<!--[!-->");
if (liveMatches().length === 0) {
$$renderer2.push("<!--[-->");
$$renderer2.push(`<div class="rounded-lg border border-dashed border-border py-12 text-center"><p class="text-sm text-text-secondary">No matches yet. Create one to get started.</p></div>`);
} else {
$$renderer2.push("<!--[!-->");
}
$$renderer2.push(`<!--]-->`);
}
$$renderer2.push(`<!--]--></div></div></div>`);
});
}
export {
_page as default
};