feat(panel): add useGameRoom hook for per-room game state

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
syntaxbullet
2026-04-02 13:28:42 +02:00
parent 069c0b93ef
commit 4b3f6590cc

View File

@@ -0,0 +1,115 @@
import { useEffect, useState, useCallback } from "react";
import { useWebSocket } from "./useWebSocket";
interface PlayerInfo {
discordId: string;
username: string;
}
interface GameRoomState {
gameState: unknown;
players: PlayerInfo[];
spectators: PlayerInfo[];
roomStatus: "connecting" | "waiting" | "playing" | "finished" | "not_found";
isSpectator: boolean;
gameOver: { winner: string | null; reason: string } | null;
error: string | null;
}
export function useGameRoom(roomId: string, userId: string) {
const { send, subscribe, connected } = useWebSocket();
const [state, setState] = useState<GameRoomState>({
gameState: null,
players: [],
spectators: [],
roomStatus: "connecting",
isSpectator: false,
gameOver: null,
error: null,
});
useEffect(() => {
if (!connected) return;
send({ type: "JOIN_ROOM", roomId, as: "player" });
const unsubscribe = subscribe((msg: any) => {
if (msg.roomId && msg.roomId !== roomId) return;
switch (msg.type) {
case "GAME_STATE":
setState(prev => ({ ...prev, gameState: msg.state, roomStatus: "playing" }));
break;
case "GAME_STARTED":
setState(prev => ({ ...prev, gameState: msg.state, roomStatus: "playing" }));
break;
case "GAME_UPDATE":
setState(prev => ({ ...prev, gameState: msg.state }));
break;
case "PLAYER_JOINED":
setState(prev => {
if (msg.as === "spectator") {
const isMe = msg.player.discordId === userId;
return {
...prev,
spectators: [...prev.spectators.filter(s => s.discordId !== msg.player.discordId), msg.player],
isSpectator: isMe ? true : prev.isSpectator,
roomStatus: prev.roomStatus === "connecting" ? "waiting" : prev.roomStatus,
};
}
return {
...prev,
players: [...prev.players.filter(p => p.discordId !== msg.player.discordId), msg.player],
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 "GAME_ENDED":
setState(prev => ({
...prev,
roomStatus: "finished",
gameOver: { winner: msg.winner, reason: msg.reason },
}));
break;
case "ERROR":
if (msg.message === "Game already started" || msg.message === "Room is full") {
send({ type: "JOIN_ROOM", roomId, as: "spectator" });
} else if (msg.message === "Room not found") {
setState(prev => ({ ...prev, roomStatus: "not_found" }));
} else {
setState(prev => ({ ...prev, error: msg.message }));
}
break;
}
});
return () => {
send({ type: "LEAVE_ROOM", roomId });
unsubscribe();
};
}, [roomId, connected, userId, send, subscribe]);
const sendAction = useCallback((action: unknown) => {
send({ type: "GAME_ACTION", roomId, action });
setState(prev => ({ ...prev, error: null }));
}, [roomId, send]);
const leaveRoom = useCallback(() => {
send({ type: "LEAVE_ROOM", roomId });
}, [roomId, send]);
return { ...state, sendAction, leaveRoom };
}