"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()}>
🏆🔥💀
БИНГО!
Капитан офонарел! Суб разгерметизирован! Все мертвы!
🎉💥🌊🤪
)}
);
}