"use client";
import { AnimatePresence, motion } from "framer-motion";
import { Crown, Flag, LogOut, SmilePlus, Volume2, VolumeX, Zap } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useGameStore, viewerRot } from "@/lib/game-store";
import { useSoundStore } from "@/lib/sound-store";
import { Avatar } from "@/components/online/Avatar";
import { legalMoves } from "@/lib/hokm/engine";
import { sortHand } from "@/lib/hokm/deck";
import {
Card,
Seat,
Suit,
SUITS,
SUIT_IS_RED,
SUIT_SYMBOL,
teamOf,
} from "@/lib/hokm/types";
import { useI18n } from "@/lib/i18n";
import { useSessionStore } from "@/lib/session-store";
import { cardBackById, cardFrontById, ownedReactions, ownedStickers, titleById, turnMsForStake } from "@/lib/online/gamification";
import { getService } from "@/lib/online/service";
import { cn } from "@/lib/cn";
import { PlayingCard } from "./PlayingCard";
import { Sticker } from "./online/Sticker";
import { MatchPlayersList } from "./online/MatchPlayersList";
function useCountdown(deadline: number | null) {
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
if (deadline == null) return;
const id = setInterval(() => setNow(Date.now()), 250);
return () => clearInterval(id);
}, [deadline]);
if (deadline == null) return null;
return Math.max(0, Math.ceil((deadline - now) / 1000));
}
function useCardSkins() {
const frontId = useSessionStore((s) => s.profile?.cardFront ?? "classic");
const backId = useSessionStore((s) => s.profile?.cardBack ?? "classic");
const f = cardFrontById(frontId);
const b = cardBackById(backId);
return {
front: { bg1: f.bg1, bg2: f.bg2, border: f.border },
back: { c1: b.c1, c2: b.c2, accent: b.accent, pattern: b.pattern, motif: b.motif },
};
}
export function GameTable({
onExit,
onForfeit,
}: { onExit?: () => void; onForfeit?: () => void } = {}) {
const game = useGameStore((s) => s.game);
const reset = useGameStore((s) => s.reset);
const mode = useGameStore((s) => s.mode);
const { t } = useI18n();
const [askFf, setAskFf] = useState(false);
const sfx = useSoundStore((s) => s.sfx);
const music = useSoundStore((s) => s.music);
const toggleAll = useSoundStore((s) => s.toggleAll);
const muted = !sfx && !music;
const exit = onExit ?? reset;
const vw = useViewportWidth();
// Pull the played-card pile inward on narrow screens so it clears the side stacks.
const trickScale = vw < 400 ? 0.82 : 1;
// Smaller played cards on phones so the center pile stays clear of the side seats.
const trickCardSize: "sm" | "md" = vw < 480 ? "sm" : "md";
const { phase, players, hakem, trump, turn, currentTrick } = game;
const legalMovesList = useMemo(
() => (phase === "playing" && turn === 0 ? legalMoves(game, 0) : []),
[phase, turn, game]
);
const legalIds = new Set(legalMovesList.map((c) => c.id));
// Keyboard shortcuts (desktop): 1โ9 / 0 play the Nth playable card in hand
// order, Space/Enter play the first playable card, M mutes, F forfeits,
// Esc/Q quits. A floating hint lists them.
const playHuman = useGameStore((s) => s.playHuman);
const chooseTrump = useGameStore((s) => s.chooseTrump);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
const el = e.target as HTMLElement | null;
if (el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable)) return;
const k = e.key.toLowerCase();
// Hakem choosing trump: 1โ4 pick a suit.
if (phase === "choosing-trump" && players[hakem!]?.isHuman) {
const idx = "1234".indexOf(e.key);
if (idx >= 0) { e.preventDefault(); chooseTrump(SUITS[idx]); return; }
}
if (phase === "playing" && turn === 0) {
const playable = sortHand(game.players[0].hand).filter((c) => legalIds.has(c.id));
if (k === " " || k === "enter") {
if (playable[0]) { e.preventDefault(); playHuman(playable[0]); }
return;
}
// 1-9 then 0 โ 10th
const digit = e.key === "0" ? 9 : "123456789".indexOf(e.key);
if (digit >= 0 && playable[digit]) { e.preventDefault(); playHuman(playable[digit]); return; }
}
if (k === "m") { e.preventDefault(); toggleAll(); }
else if (k === "f" && onForfeit) { e.preventDefault(); setAskFf(true); }
else if (k === "escape" || k === "q") { e.preventDefault(); exit(); }
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [phase, turn, hakem, game.players, legalMovesList]);
return (
{/* Top HUD */}
{trump && }
{onForfeit && (
)}
{/* forfeit confirm (requester) */}
{askFf && (
๐ณ๏ธ
{t("forfeit.title")}
{t("forfeit.ask")}
{t("forfeit.rule")}
)}
{/* Felt table โ portrait proportions (tall, centered between HUD and hand) */}
{/* max-w-full so the felt never exceeds its padded container โ otherwise an
overflowing flex item under justify-center pins to the (RTL) start and
the trick area, centered on the felt, drifts off-center. */}
{/* opponent + partner seats */}
{/* opponents' face-down hands */}
{/* center trick area (offsets scale down on narrow screens) */}
{/* Your hand */}
{/* Turn indicator + timer โ stacked so they never overlap */}
)}
)}
{/* button โ raised above the hand so it doesn't overlap the cards on mobile */}
>
);
}
/* ------------------------------ Overlays ------------------------------ */
function Backdrop({ children }: { children: React.ReactNode }) {
return (
{/* min-h-full + centering: centers short panels, scrolls tall ones (no clip). */}
{children}
);
}
function HakemOverlay() {
const game = useGameStore((s) => s.game);
const { t } = useI18n();
const { front } = useCardSkins();
const hakemName = game.hakem != null ? game.players[game.hakem].name : "";
return (