"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { BingoCell } from "./BingoCell"; import { Badge } from "./ui/badge"; import { Button } from "./ui/button"; import { playSound, playBingo } from "@/lib/sounds"; import { cn } from "@/lib/utils"; type Item = { id: string; text: string; emoji: string; soundCategory: string; soundUrl?: string | null; gridIndex: number; }; type GridCell = { index: number; item: Item | null; marked: boolean; markedBy: string[]; markCount: number; }; type Campaign = { id: string; name: string; gridSize: number; status: string; }; type Props = { campaign: Campaign; initialGrid: GridCell[]; currentUserNickname: string; isAdmin: boolean; }; function checkBingo(grid: GridCell[], gridSize: number): number[][] { const lines: number[][] = []; const size = gridSize; for (let row = 0; row < size; row++) { const indices = Array.from({ length: size }, (_, c) => row * size + c); if (indices.every(i => grid[i]?.marked)) lines.push(indices); } for (let col = 0; col < size; col++) { const indices = Array.from({ length: size }, (_, r) => r * size + col); if (indices.every(i => grid[i]?.marked)) lines.push(indices); } const diag1 = Array.from({ length: size }, (_, i) => i * size + i); if (diag1.every(i => grid[i]?.marked)) lines.push(diag1); const diag2 = Array.from({ length: size }, (_, i) => (i + 1) * size - i - 1); if (diag2.every(i => grid[i]?.marked)) lines.push(diag2); return lines; } export function BingoCard({ campaign, initialGrid, currentUserNickname, isAdmin }: Props) { const [grid, setGrid] = useState(initialGrid); const [bingoLines, setBingoLines] = useState([]); const [marking, setMarking] = useState(null); const [chaosLevel, setChaosLevel] = useState(0); const [showBingo, setShowBingo] = useState(false); const [activityLog, setActivityLog] = useState([]); const bingoNotified = useRef(false); const pollingRef = useRef | null>(null); const fetchState = useCallback(async () => { try { const res = await fetch(`/api/game/${campaign.id}/state`); const data = await res.json(); if (data.grid) { setGrid(data.grid); const lines = checkBingo(data.grid, campaign.gridSize); setBingoLines(lines); const markedCount = data.grid.filter((c: GridCell) => c.marked).length; const totalItems = data.grid.filter((c: GridCell) => c.item).length; setChaosLevel(totalItems > 0 ? Math.min(markedCount / totalItems, 1) : 0); if (lines.length > 0 && !bingoNotified.current) { bingoNotified.current = true; setShowBingo(true); playBingo(); } if (data.activityLog) { setActivityLog(data.activityLog); } } } catch {} }, [campaign.id, campaign.gridSize]); useEffect(() => { fetchState(); pollingRef.current = setInterval(fetchState, 3000); return () => { if (pollingRef.current) clearInterval(pollingRef.current); }; }, [fetchState]); const markItem = async (itemId: string) => { if (marking) return; const item = grid.find(c => c.item?.id === itemId); if (item?.item) playSound(item.item.soundCategory, item.item.soundUrl); setMarking(itemId); try { const res = await fetch("/api/game/mark", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ campaignId: campaign.id, itemId }), }); if (!res.ok && res.status === 409) { // already marked by someone else, just refresh } await fetchState(); } catch {} setMarking(null); }; const unmarkItem = async (itemId: string) => { if (marking) return; setMarking(itemId); try { await fetch("/api/game/mark", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ campaignId: campaign.id, itemId }), }); await fetchState(); } catch {} setMarking(null); }; const markedCount = grid.filter(c => c.marked).length; const totalItems = grid.filter(c => c.item).length; const hullHealth = Math.max(0, 100 - chaosLevel * 100); const reactorTemp = 80 + chaosLevel * 120; const o2Level = Math.max(0, 100 - chaosLevel * 130); const stageLabel = chaosLevel < 0.25 ? "✅ Sub stable" : chaosLevel < 0.5 ? "⚠️ Leaking" : chaosLevel < 0.75 ? "🚨 MELTDOWN" : "☢️ SUB DESTROYED"; const chaosStageClass = chaosLevel < 0.3 ? "from-cyan-600 to-cyan-500" : chaosLevel < 0.6 ? "from-amber-600 to-orange-500" : "from-red-600 to-red-500"; return (
{/* Header */}

{campaign.name}

{markedCount}/{totalItems} cells marked

{/* Chaos + Subs compact row */}
{stageLabel} {Math.round(chaosLevel * 100)}% chaos
= 0.6 && "animate-pulse" )} style={{ width: `${chaosLevel * 100}%` }} />
Hull {Math.round(hullHealth)}%
120 ? "border-orange-800/50 text-orange-400" : "border-slate-700/30 text-slate-500")}> R{Math.round(reactorTemp)}°
O₂ {Math.round(o2Level)}%
{/* Bingo Grid */}
{grid.map((cell) => ( ))}
{/* Bingo Lines */} {bingoLines.length > 0 && ( 🏆 BINGO! {bingoLines.length} line(s) )} {/* Activity Log */}

Crew Log

{activityLog.length === 0 ? (

Awaiting chaos...

) : (
{activityLog.map((entry, i) => (

{entry}

))}
)}
{/* Bingo Celebration Modal */} {showBingo && (
setShowBingo(false)}>
e.stopPropagation()}>
🏆🔥💀

БИНГО!

Хаос победил! Суб взорван, экипаж мёртв, всем весело!

🎉💥🌊🤡
)}
); }