"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; imageUrl?: 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; } function StatusGauge({ label, value, unit, low, high, icon, stage }: { label: string; value: number; unit?: string; low?: boolean; high?: boolean; icon: string; stage?: string; }) { const pct = Math.min(Math.max(value, 0), 10000) / 100; return (
{icon}
{label} {unit ? `${Math.round(value)}${unit}` : Math.round(value)}
); } 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) {} 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; // HUD params const reactorTemp = 2000 + chaosLevel * 8000; const hullHealth = Math.max(0, 100 - chaosLevel * 100); const floodLevel = Math.min(100, chaosLevel * 100); const captainSanity = Math.max(0, 100 - chaosLevel * 100); const sanityStage = captainSanity >= 75 ? "calm" : captainSanity >= 50 ? "nervous" : captainSanity >= 25 ? "panic" : "lost"; const sanityLabel = sanityStage === "calm" ? "Держится ещё" : sanityStage === "nervous" ? "Кукуха свистит" : sanityStage === "panic" ? "Паника!" : "Кукуха уехала пиздаааа!"; const sanityIcon = sanityStage === "calm" ? "😐" : sanityStage === "nervous" ? "😰" : sanityStage === "panic" ? "😱" : "🤪"; const stageLabel = chaosLevel < 0.25 ? "Sub stable" : chaosLevel < 0.5 ? "⚠️ Leaking" : chaosLevel < 0.75 ? "🚨 MELTDOWN" : "☢️ DESTROYED"; return (
{/* Header */}

{campaign.name}

{markedCount}/{totalItems} cells — {stageLabel}

{/* HUD Panel */}
9000} high={reactorTemp > 5000 && reactorTemp <= 9000} /> 50} />
{sanityStage === "calm" && "Капитан спокоен. Пока."} {sanityStage === "nervous" && "Капитан дёргается. Это нормально."} {sanityStage === "panic" && "КЭП НА НЕРВАХ! НЕ ТРОГАЙТЕ ЕГО!"} {sanityStage === "lost" && "КУКУХА УЕХАЛА! Капитан ушёл в отрыв!"}
{/* 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 Modal */} {showBingo && (
setShowBingo(false)}>
e.stopPropagation()}>
🏆🔥💀

БИНГО!

Капитан офонарел! Суб разгерметизирован! Все мертвы!

🎉💥🌊🤪
)}
); }