"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 handleMark = async (itemId: string) => { if (marking) return; 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) { const item = grid.find(c => c.item?.id === itemId); if (item?.item) playSound(item.item.soundCategory, item.item.soundUrl); await fetchState(); } else if (res.status === 409) { 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 Meter */}
{stageLabel} {Math.round(chaosLevel * 100)}% chaos
= 0.6 && "animate-pulse" )} style={{ width: `${chaosLevel * 100}%` }} />
{/* Sub Status + Grid Row */}
{/* Sub Status Panel */}

Sub Status

Hull {Math.round(hullHealth)}%
Reactor 120 ? "text-orange-400" : "text-slate-400")}>{Math.round(reactorTemp)}°C
120 ? "bg-orange-500 animate-pulse" : "bg-cyan-500")} style={{ width: `${Math.min(reactorTemp / 2, 100)}%` }} />
O₂ {Math.round(o2Level)}%
{chaosLevel < 0.25 && "All systems nominal"} {chaosLevel >= 0.25 && chaosLevel < 0.5 && "⚠️ Minor breaches detected"} {chaosLevel >= 0.5 && chaosLevel < 0.75 && "🚨 EVACUATE! SUB COMPROMISED"} {chaosLevel >= 0.75 && "☢️ MELTDOWN IN PROGRESS"}
{/* Activity Log */}

Crew Log

{activityLog.length === 0 && (

Awaiting chaos...

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

{entry}

))}
{/* Bingo Grid */}
{grid.map((cell) => ( ))}
{/* Bingo Lines */} {bingoLines.length > 0 && (
🏆 BINGO! {bingoLines.length} line(s)
)} {/* Bingo Celebration Modal */} {showBingo && (
setShowBingo(false)}>
e.stopPropagation()}>
🏆🔥💀

БИНГО!

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

🎉💥🌊🤡
)} {/* Mobile Activity Log */}

Crew Log

{activityLog.length === 0 && (

Awaiting chaos...

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

{entry}

))}
{/* Legend */}
🎺 horn 🚨 alarm 🌊 flood 💥 boom 👹 monster 💀 death 🔥 chaos
); }