v2.0: sound library, image upload, DnD, ResourceBrowser, Barotrauma HUD, captain sanity
Deploy / build-and-deploy (push) Failing after 52s

This commit is contained in:
2026-06-15 01:07:05 +03:00
parent 9deec9d23e
commit d0cd008f6e
16 changed files with 1039 additions and 231 deletions
+77 -59
View File
@@ -13,6 +13,7 @@ type Item = {
emoji: string;
soundCategory: string;
soundUrl?: string | null;
imageUrl?: string | null;
gridIndex: number;
};
@@ -41,26 +42,49 @@ type Props = {
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 (
<div className="flex items-center gap-1.5 text-[9px] font-mono">
<span className="w-4 text-center shrink-0">{icon}</span>
<div className="flex-1 min-w-0 space-y-0.5">
<div className="flex justify-between">
<span className="text-slate-500">{label}</span>
<span className={cn(low && "text-red-400", high && "text-orange-400", !low && !high && "text-slate-400")}>
{unit ? `${Math.round(value)}${unit}` : Math.round(value)}
</span>
</div>
<div className="h-1 bg-slate-800 rounded-full overflow-hidden">
<div className={cn(
"h-full rounded-full transition-all duration-700",
low && "bg-red-500", high && "bg-orange-500",
!low && !high && "bg-cyan-500",
stage === "panic" && "animate-pulse",
stage === "lost" && "animate-pulse",
)} style={{ width: `${Math.min(pct, 100)}%` }} />
</div>
</div>
</div>
);
}
export function BingoCard({ campaign, initialGrid, currentUserNickname, isAdmin }: Props) {
const [grid, setGrid] = useState<GridCell[]>(initialGrid);
const [bingoLines, setBingoLines] = useState<number[][]>([]);
@@ -87,9 +111,7 @@ export function BingoCard({ campaign, initialGrid, currentUserNickname, isAdmin
setShowBingo(true);
playBingo();
}
if (data.activityLog) {
setActivityLog(data.activityLog);
}
if (data.activityLog) setActivityLog(data.activityLog);
}
} catch {}
}, [campaign.id, campaign.gridSize]);
@@ -111,9 +133,7 @@ export function BingoCard({ campaign, initialGrid, currentUserNickname, isAdmin
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
}
if (!res.ok && res.status === 409) {}
await fetchState();
} catch {}
setMarking(null);
@@ -136,60 +156,60 @@ export function BingoCard({ campaign, initialGrid, currentUserNickname, isAdmin
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 reactorTemp = 80 + chaosLevel * 120;
const o2Level = Math.max(0, 100 - chaosLevel * 130);
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.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";
chaosLevel < 0.75 ? "🚨 MELTDOWN" : "☢️ DESTROYED";
return (
<div className="flex flex-col items-center gap-2 w-full max-w-lg mx-auto">
<div className="flex flex-col items-center gap-3 w-full max-w-2xl mx-auto">
{/* Header */}
<div className="text-center w-full">
<h2 className="text-xl font-bold text-cyan-300 font-mono tracking-wide uppercase">
{campaign.name}
</h2>
<p className="text-xs text-slate-500 font-mono">
{markedCount}/{totalItems} cells marked
<h2 className="text-xl font-bold text-cyan-300 font-mono tracking-wide uppercase">{campaign.name}</h2>
<p className="text-[10px] text-slate-500 font-mono">
{markedCount}/{totalItems} cells {stageLabel}
</p>
</div>
{/* Chaos + Subs compact row */}
<div className="w-full flex gap-2 items-stretch">
<div className="flex-1 min-w-0 space-y-0.5">
<div className="flex justify-between text-[9px] font-mono text-slate-500">
<span>{stageLabel}</span>
<span>{Math.round(chaosLevel * 100)}% chaos</span>
</div>
<div className="w-full bg-slate-900/60 rounded-full h-2 overflow-hidden border border-slate-700/30">
<div
className={cn(
"h-full rounded-full transition-all duration-500 ease-out bg-gradient-to-r",
chaosStageClass,
chaosLevel >= 0.6 && "animate-pulse"
)}
style={{ width: `${chaosLevel * 100}%` }}
/>
</div>
{/* HUD Panel */}
<div className={cn(
"w-full border rounded-lg p-2 space-y-1.5 bg-slate-900/50 transition-all",
sanityStage === "panic" && "border-orange-700/40 animate-shake",
sanityStage === "lost" && "border-purple-700/50 animate-flicker",
sanityStage !== "panic" && sanityStage !== "lost" && "border-slate-700/30",
)}>
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5">
<StatusGauge icon="⚛️" label="Reactor" value={reactorTemp} unit="°C" low={reactorTemp > 9000} high={reactorTemp > 5000 && reactorTemp <= 9000} />
<StatusGauge icon="🛡️" label="Hull" value={hullHealth} unit="%" low={hullHealth < 30} />
<StatusGauge icon="🌊" label="Flood" value={floodLevel} unit="%" high={floodLevel > 50} />
<StatusGauge icon={sanityIcon} label="Кэп" value={captainSanity} unit="%" low={captainSanity < 30} stage={sanityStage === "panic" || sanityStage === "lost" ? sanityStage : undefined} />
</div>
<div className="hidden sm:flex gap-2 text-[9px] font-mono">
<div className={cn("px-2 py-1 rounded border", hullHealth < 30 ? "border-red-800/50 text-red-400" : "border-slate-700/30 text-slate-500")}>
Hull {Math.round(hullHealth)}%
</div>
<div className={cn("px-2 py-1 rounded border", reactorTemp > 120 ? "border-orange-800/50 text-orange-400" : "border-slate-700/30 text-slate-500")}>
R{Math.round(reactorTemp)}°
</div>
<div className={cn("px-2 py-1 rounded border", o2Level < 30 ? "border-red-800/50 text-red-400" : "border-slate-700/30 text-slate-500")}>
O {Math.round(o2Level)}%
</div>
<div className="text-[8px] font-mono text-slate-600 text-center pt-1 border-t border-slate-700/20">
{sanityStage === "calm" && "Капитан спокоен. Пока."}
{sanityStage === "nervous" && "Капитан дёргается. Это нормально."}
{sanityStage === "panic" && "КЭП НА НЕРВАХ! НЕ ТРОГАЙТЕ ЕГО!"}
{sanityStage === "lost" && "КУКУХА УЕХАЛА! Капитан ушёл в отрыв!"}
</div>
</div>
@@ -237,17 +257,15 @@ export function BingoCard({ campaign, initialGrid, currentUserNickname, isAdmin
)}
</div>
{/* Bingo Celebration Modal */}
{/* Bingo Modal */}
{showBingo && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm" onClick={() => setShowBingo(false)}>
<div className="bg-slate-900 border-2 border-amber-500/50 rounded-2xl p-8 text-center shadow-2xl shadow-amber-900/30 animate-[bounce_1s_ease-in-out_infinite] max-w-md mx-4" onClick={e => e.stopPropagation()}>
<div className="text-6xl mb-4">🏆🔥💀</div>
<h2 className="text-3xl font-bold text-amber-400 font-mono mb-2">БИНГО!</h2>
<p className="text-slate-300 mb-4">Хаос победил! Суб взорван, экипаж мёртв, всем весело!</p>
<div className="text-4xl mb-4 animate-pulse">🎉💥🌊🤡</div>
<Button variant="outline" onClick={() => setShowBingo(false)}>
Продолжить хаос
</Button>
<p className="text-slate-300 mb-4">Капитан офонарел! Суб разгерметизирован! Все мертвы!</p>
<div className="text-4xl mb-4 animate-pulse">🎉💥🌊🤪</div>
<Button variant="outline" onClick={() => setShowBingo(false)}>Продолжить хаос</Button>
</div>
</div>
)}