2026-06-04 10:11:00 +03:30
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { AnimatePresence, motion } from "framer-motion";
|
2026-06-04 10:49:54 +03:30
|
|
|
import { Crown, Loader2 } from "lucide-react";
|
2026-06-04 13:13:48 +03:30
|
|
|
import { useEffect } from "react";
|
2026-06-04 10:11:00 +03:30
|
|
|
import { ScreenShell } from "@/components/online/ScreenHeader";
|
2026-06-05 10:07:51 +03:30
|
|
|
import { Avatar } from "@/components/online/Avatar";
|
2026-06-04 10:11:00 +03:30
|
|
|
import { useGameStore } from "@/lib/game-store";
|
|
|
|
|
import { useOnlineStore } from "@/lib/online-store";
|
2026-06-04 10:49:54 +03:30
|
|
|
import { useSessionStore } from "@/lib/session-store";
|
2026-06-04 10:11:00 +03:30
|
|
|
import { useUIStore } from "@/lib/ui-store";
|
|
|
|
|
import { useI18n } from "@/lib/i18n";
|
|
|
|
|
import { getService } from "@/lib/online/service";
|
|
|
|
|
|
|
|
|
|
export function MatchmakingScreen() {
|
|
|
|
|
const { t } = useI18n();
|
|
|
|
|
const mm = useOnlineStore((s) => s.matchmaking);
|
|
|
|
|
const cancelMatchmaking = useOnlineStore((s) => s.cancelMatchmaking);
|
|
|
|
|
const newOnlineMatch = useGameStore((s) => s.newOnlineMatch);
|
2026-06-04 13:13:48 +03:30
|
|
|
const enterServerMatch = useGameStore((s) => s.enterServerMatch);
|
2026-06-04 10:49:54 +03:30
|
|
|
const upgradePlan = useSessionStore((s) => s.upgradePlan);
|
2026-06-04 10:11:00 +03:30
|
|
|
const goGame = useUIStore((s) => s.goGame);
|
|
|
|
|
const go = useUIStore((s) => s.go);
|
|
|
|
|
|
|
|
|
|
const ready = mm.phase === "ready";
|
2026-06-04 10:49:54 +03:30
|
|
|
const queued = mm.phase === "queued";
|
2026-06-04 10:11:00 +03:30
|
|
|
const slots = [0, 1, 2, 3];
|
|
|
|
|
|
2026-06-04 13:13:48 +03:30
|
|
|
// Live server: the server starts the match itself — auto-enter when ready.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (mm.phase === "ready" && getService().live) {
|
|
|
|
|
enterServerMatch(getService());
|
|
|
|
|
goGame("home");
|
|
|
|
|
}
|
|
|
|
|
}, [mm.phase, enterServerMatch, goGame]);
|
|
|
|
|
|
2026-06-04 10:11:00 +03:30
|
|
|
const cancel = async () => {
|
|
|
|
|
await cancelMatchmaking();
|
|
|
|
|
go("online");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const enter = () => {
|
|
|
|
|
const players = getService().getMatchPlayers();
|
|
|
|
|
if (!players) return;
|
|
|
|
|
newOnlineMatch({
|
|
|
|
|
players: players.map((p) => ({
|
|
|
|
|
displayName: p.displayName,
|
|
|
|
|
avatar: p.avatar,
|
|
|
|
|
level: p.level,
|
|
|
|
|
})),
|
|
|
|
|
targetScore: 7,
|
|
|
|
|
stake: mm.stake,
|
|
|
|
|
ranked: mm.ranked,
|
|
|
|
|
});
|
|
|
|
|
goGame("home");
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-04 10:49:54 +03:30
|
|
|
if (queued) {
|
|
|
|
|
return (
|
|
|
|
|
<ScreenShell>
|
|
|
|
|
<div className="flex flex-col items-center justify-center min-h-[80dvh] text-center">
|
|
|
|
|
<div className="text-5xl mb-4">⏳</div>
|
|
|
|
|
<h1 className="gold-text text-2xl font-black">{t("queue.title")}</h1>
|
|
|
|
|
<p className="text-cream/60 text-sm mt-1">{t("queue.busy")}</p>
|
|
|
|
|
<div className="my-6 text-7xl font-black gold-text tabular-nums">
|
|
|
|
|
{mm.queuePosition ?? 0}
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-cream/70">{t("queue.position", { n: mm.queuePosition ?? 0 })}</p>
|
|
|
|
|
|
|
|
|
|
<div className="mt-9 flex flex-col items-center gap-3 w-full max-w-xs">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => upgradePlan()}
|
2026-06-05 10:07:51 +03:30
|
|
|
className="press-3d btn-gold w-full rounded-2xl py-3 flex items-center justify-center gap-2"
|
2026-06-04 10:49:54 +03:30
|
|
|
>
|
|
|
|
|
<Crown className="size-4" />
|
|
|
|
|
{t("queue.upgrade")}
|
|
|
|
|
</button>
|
|
|
|
|
<span className="text-[11px] text-cream/45">{t("queue.skip")}</span>
|
|
|
|
|
<button
|
|
|
|
|
onClick={cancel}
|
|
|
|
|
className="glass rounded-xl px-6 py-2.5 text-cream/70 hover:text-cream"
|
|
|
|
|
>
|
|
|
|
|
{t("mm.cancel")}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</ScreenShell>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 10:11:00 +03:30
|
|
|
return (
|
|
|
|
|
<ScreenShell>
|
|
|
|
|
<div className="flex flex-col items-center justify-center min-h-[80dvh] text-center">
|
|
|
|
|
<motion.div
|
|
|
|
|
animate={ready ? {} : { rotate: 360 }}
|
|
|
|
|
transition={{ repeat: ready ? 0 : Infinity, duration: 2, ease: "linear" }}
|
|
|
|
|
className="mb-6"
|
|
|
|
|
>
|
|
|
|
|
{ready ? (
|
|
|
|
|
<span className="text-5xl">✅</span>
|
|
|
|
|
) : (
|
|
|
|
|
<Loader2 className="size-12 text-gold-400" />
|
|
|
|
|
)}
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
|
|
|
<h1 className="gold-text text-2xl font-black">
|
|
|
|
|
{ready ? t("mm.ready") : mm.phase === "found" ? t("mm.found") : t("mm.searching")}
|
|
|
|
|
</h1>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-4 gap-3 mt-8">
|
|
|
|
|
{slots.map((i) => {
|
|
|
|
|
const p = mm.players[i];
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={i}
|
2026-06-05 10:07:51 +03:30
|
|
|
className={
|
|
|
|
|
"w-16 h-20 rounded-2xl flex flex-col items-center justify-center gap-1 transition " +
|
|
|
|
|
(p ? "glass gold-border" : "border border-dashed border-navy-700/60 bg-navy-900/30")
|
|
|
|
|
}
|
2026-06-04 10:11:00 +03:30
|
|
|
>
|
|
|
|
|
<AnimatePresence mode="wait">
|
|
|
|
|
{p ? (
|
|
|
|
|
<motion.div
|
|
|
|
|
key={p.id}
|
|
|
|
|
initial={{ scale: 0, opacity: 0 }}
|
|
|
|
|
animate={{ scale: 1, opacity: 1 }}
|
2026-06-05 10:07:51 +03:30
|
|
|
transition={{ type: "spring", stiffness: 260, damping: 18 }}
|
2026-06-04 10:11:00 +03:30
|
|
|
className="flex flex-col items-center gap-0.5"
|
|
|
|
|
>
|
2026-06-05 10:07:51 +03:30
|
|
|
<span className="grid size-9 place-items-center rounded-xl bg-navy-900 overflow-hidden">
|
|
|
|
|
<Avatar id={p.avatar} size={24} />
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-[9px] text-cream/80 max-w-14 truncate">
|
2026-06-04 10:11:00 +03:30
|
|
|
{p.displayName}
|
|
|
|
|
</span>
|
2026-06-05 10:07:51 +03:30
|
|
|
<span className="text-[8px] text-gold-400/80">
|
2026-06-04 10:11:00 +03:30
|
|
|
{t("common.level")} {p.level}
|
|
|
|
|
</span>
|
|
|
|
|
</motion.div>
|
|
|
|
|
) : (
|
|
|
|
|
<motion.span
|
|
|
|
|
key="empty"
|
|
|
|
|
className="text-cream/20 text-2xl"
|
|
|
|
|
animate={{ opacity: [0.2, 0.5, 0.2] }}
|
|
|
|
|
transition={{ repeat: Infinity, duration: 1.4 }}
|
|
|
|
|
>
|
|
|
|
|
?
|
|
|
|
|
</motion.span>
|
|
|
|
|
)}
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="mt-10 flex gap-3">
|
2026-06-05 10:07:51 +03:30
|
|
|
<button onClick={cancel} className="press-3d glass rounded-2xl px-6 py-3 text-cream/70">
|
2026-06-04 10:11:00 +03:30
|
|
|
{t("mm.cancel")}
|
|
|
|
|
</button>
|
|
|
|
|
{ready && (
|
|
|
|
|
<motion.button
|
|
|
|
|
initial={{ scale: 0.8, opacity: 0 }}
|
|
|
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
|
|
|
onClick={enter}
|
2026-06-05 10:07:51 +03:30
|
|
|
className="press-3d btn-gold rounded-2xl px-8 py-3 text-lg"
|
2026-06-04 10:11:00 +03:30
|
|
|
>
|
|
|
|
|
{t("mm.start")}
|
|
|
|
|
</motion.button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</ScreenShell>
|
|
|
|
|
);
|
|
|
|
|
}
|