Files
aurorabot/panel/src/lib/useGameRoom.ts
syntaxbullet cb056e010f
Some checks failed
CI / Deploy / test (push) Failing after 48s
CI / Deploy / deploy (push) Has been skipped
Redesign game lobby and room creation flow
- Split chess and blackjack setup into guided creation steps
- Add chess time control presets and reusable lobby room metrics
- Improve room filtering, ordering, and live connection state
2026-04-10 11:34:12 +02:00

219 lines
8.4 KiB
TypeScript

import { useEffect, useState, useCallback, useRef } from "react";
import { useWebSocket } from "./useWebSocket";
import { useNavigate } from "react-router-dom";
interface PlayerInfo {
discordId: string;
username: string;
}
interface RoundResult {
settlements: Record<string, { wager: number; payout: number; net: number }>;
}
interface GameRoomState {
gameState: unknown;
players: PlayerInfo[];
spectators: PlayerInfo[];
roomStatus: "connecting" | "waiting" | "playing" | "finished" | "not_found";
isSpectator: boolean;
gameOver: { winner: string | null; reason: string; payout?: { amount: number; refunded?: boolean } } | null;
roundResult: RoundResult | null;
error: string | null;
sessionReplaced: boolean;
roomOptions: { betAmount?: number; timeControl?: string };
}
function createInitialRoomState(): GameRoomState {
return {
gameState: null,
players: [],
spectators: [],
roomStatus: "connecting",
isSpectator: false,
gameOver: null,
roundResult: null,
error: null,
sessionReplaced: false,
roomOptions: {},
};
}
export function useGameRoom(roomId: string, userId: string, role?: string, preferAs: "player" | "spectator" = "player") {
const { send, subscribe, connected } = useWebSocket();
const navigate = useNavigate();
const navigateRef = useRef(navigate);
const errorTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
navigateRef.current = navigate;
}, [navigate]);
useEffect(() => () => {
if (errorTimerRef.current !== null) {
clearTimeout(errorTimerRef.current);
}
}, []);
const [state, setState] = useState<GameRoomState>(() => createInitialRoomState());
useEffect(() => {
if (!connected) return;
setState(createInitialRoomState());
send({ type: "JOIN_ROOM", roomId, preferAs, role: role ?? "player" });
const unsubscribe = subscribe((msg: any) => {
if (msg.roomId && msg.roomId !== roomId) return;
switch (msg.type) {
case "JOIN_RESULT":
setState(prev => ({
...prev,
isSpectator: msg.joinedAs === "spectator",
roomStatus: msg.roomStatus,
players: msg.players ?? prev.players,
spectators: msg.spectators ?? prev.spectators,
gameState: msg.state !== undefined ? msg.state : prev.gameState,
roomOptions: msg.roomOptions ?? prev.roomOptions,
}));
break;
case "GAME_STATE":
// Authoritative player view — sent directly to this player
setState(prev => {
// Clear round result when a new betting phase starts
const phase = (msg.state as any)?.phase;
const roundResult = phase === "betting" ? null : prev.roundResult;
return {
...prev,
gameState: msg.state,
roundResult,
roomStatus: prev.roomStatus === "finished" ? "finished" : "playing",
};
});
break;
case "GAME_STARTED":
// Broadcast with spectator view — only use for state if we're a spectator
// (players get their own GAME_STATE via direct send)
setState(prev => ({
...prev,
gameState: prev.isSpectator ? msg.state : prev.gameState,
roomStatus: "playing",
}));
break;
case "GAME_UPDATE":
// Broadcast with spectator view — only update state for spectators
setState(prev => {
if (!prev.isSpectator) return prev;
const phase = (msg.state as any)?.phase;
return {
...prev,
gameState: msg.state,
roundResult: phase === "betting" ? null : prev.roundResult,
};
});
break;
case "PLAYER_JOINED":
setState(prev => {
if (msg.joinedAs === "spectator") {
const isMe = msg.player.discordId === userId;
return {
...prev,
spectators: [...prev.spectators.filter(s => s.discordId !== msg.player.discordId), msg.player],
isSpectator: isMe || prev.isSpectator,
roomStatus: prev.roomStatus === "connecting" ? "waiting" : prev.roomStatus,
};
}
const isMe = msg.player.discordId === userId;
return {
...prev,
players: [...prev.players.filter(p => p.discordId !== msg.player.discordId), msg.player],
isSpectator: isMe ? false : prev.isSpectator,
roomStatus: prev.roomStatus === "connecting" ? "waiting" : prev.roomStatus,
};
});
break;
case "PLAYER_LEFT":
setState(prev => ({
...prev,
players: prev.players.filter(p => p.discordId !== msg.playerId),
spectators: prev.spectators.filter(s => s.discordId !== msg.playerId),
}));
break;
case "ROUND_SETTLED":
setState(prev => ({
...prev,
roundResult: { settlements: msg.settlements },
}));
break;
case "GAME_ENDED":
setState(prev => ({
...prev,
roomStatus: "finished",
gameOver: { winner: msg.winner, reason: msg.reason, payout: msg.payout },
}));
break;
case "SESSION_REPLACED":
setState(prev => ({ ...prev, sessionReplaced: true }));
break;
case "ERROR":
if (msg.message === "Room not found") {
setState(prev => ({ ...prev, roomStatus: "not_found" }));
setTimeout(() => navigateRef.current("/games"), 2000);
} else {
setState(prev => ({ ...prev, error: msg.message }));
if (errorTimerRef.current !== null) {
clearTimeout(errorTimerRef.current);
}
errorTimerRef.current = setTimeout(() => {
setState(prev => ({ ...prev, error: null }));
}, 5000);
}
break;
}
});
return () => {
send({ type: "LEAVE_ROOM", roomId });
unsubscribe();
};
}, [connected, preferAs, role, roomId, send, subscribe, userId]);
const sendAction = useCallback((action: unknown) => {
const sent = send({ type: "GAME_ACTION", roomId, action });
if (!sent) {
setState(prev => ({ ...prev, error: "Not connected — action not sent." }));
return;
}
setState(prev => ({ ...prev, error: null }));
}, [roomId, send]);
const leaveRoom = useCallback(() => {
send({ type: "LEAVE_ROOM", roomId });
}, [roomId, send]);
const rejoin = useCallback(() => {
send({ type: "JOIN_ROOM", roomId, preferAs, role: role ?? "player" });
setState(prev => ({ ...prev, sessionReplaced: false }));
}, [roomId, preferAs, role, send]);
const fillRoom = useCallback(() => {
send({ type: "FILL_ROOM", roomId });
}, [roomId, send]);
const startGame = useCallback(() => {
send({ type: "START_GAME", roomId });
}, [roomId, send]);
return { ...state, sendAction, leaveRoom, rejoin, fillRoom, startGame };
}