176 lines
4.9 KiB
TypeScript
176 lines
4.9 KiB
TypeScript
|
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
||
|
|
import type { BoardState, Player } from '../types';
|
||
|
|
|
||
|
|
const INITIAL_BOARD: BoardState = {
|
||
|
|
config: {
|
||
|
|
min_x: 0,
|
||
|
|
max_x: 64,
|
||
|
|
min_y: 0,
|
||
|
|
max_y: 64,
|
||
|
|
grid_cells_x: 64,
|
||
|
|
grid_cells_y: 64,
|
||
|
|
},
|
||
|
|
player_count: 0,
|
||
|
|
players: [],
|
||
|
|
};
|
||
|
|
|
||
|
|
export function useGameSocket() {
|
||
|
|
const [boardState, setBoardState] = useState<BoardState>(INITIAL_BOARD);
|
||
|
|
const [isConnected, setIsConnected] = useState(false);
|
||
|
|
const [selectedPlayer, setSelectedPlayer] = useState<Player | null>(null);
|
||
|
|
const wsRef = useRef<WebSocket | null>(null);
|
||
|
|
const reconnectTimeoutRef = useRef<number | null>(null);
|
||
|
|
|
||
|
|
const fetchBoard = useCallback(async () => {
|
||
|
|
try {
|
||
|
|
const res = await fetch('/api/board');
|
||
|
|
if (res.ok) {
|
||
|
|
const data: BoardState = await res.json();
|
||
|
|
setBoardState(data);
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to fetch board state:', err);
|
||
|
|
}
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const connectWebSocket = useCallback(() => {
|
||
|
|
if (wsRef.current && (wsRef.current.readyState === WebSocket.OPEN || wsRef.current.readyState === WebSocket.CONNECTING)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
|
|
const host = window.location.host;
|
||
|
|
const wsUrl = `${protocol}//${host}/ws`;
|
||
|
|
|
||
|
|
const ws = new WebSocket(wsUrl);
|
||
|
|
wsRef.current = ws;
|
||
|
|
|
||
|
|
ws.onopen = () => {
|
||
|
|
setIsConnected(true);
|
||
|
|
console.log('Connected to botWebWars WebSocket');
|
||
|
|
};
|
||
|
|
|
||
|
|
ws.onmessage = (event) => {
|
||
|
|
try {
|
||
|
|
const data = JSON.parse(event.data);
|
||
|
|
if (data.event === 'init') {
|
||
|
|
setBoardState(data.state);
|
||
|
|
} else if (data.event === 'player_joined') {
|
||
|
|
setBoardState((prev) => {
|
||
|
|
const exists = prev.players.some((p) => p.id === data.player.id);
|
||
|
|
const nextPlayers = exists
|
||
|
|
? prev.players.map((p) => (p.id === data.player.id ? data.player : p))
|
||
|
|
: [...prev.players, data.player];
|
||
|
|
return {
|
||
|
|
...prev,
|
||
|
|
player_count: data.player_count ?? nextPlayers.length,
|
||
|
|
players: nextPlayers,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
} else if (data.event === 'player_left') {
|
||
|
|
setBoardState((prev) => {
|
||
|
|
const nextPlayers = prev.players.filter((p) => p.id !== data.player_id);
|
||
|
|
return {
|
||
|
|
...prev,
|
||
|
|
player_count: data.player_count ?? nextPlayers.length,
|
||
|
|
players: nextPlayers,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
setSelectedPlayer((prev) => (prev?.id === data.player_id ? null : prev));
|
||
|
|
} else if (data.event === 'board_reset') {
|
||
|
|
setBoardState((prev) => ({
|
||
|
|
...prev,
|
||
|
|
player_count: 0,
|
||
|
|
players: [],
|
||
|
|
}));
|
||
|
|
setSelectedPlayer(null);
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Error parsing WebSocket message:', err);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
ws.onclose = () => {
|
||
|
|
setIsConnected(false);
|
||
|
|
reconnectTimeoutRef.current = window.setTimeout(() => {
|
||
|
|
connectWebSocket();
|
||
|
|
}, 2500);
|
||
|
|
};
|
||
|
|
|
||
|
|
ws.onerror = (err) => {
|
||
|
|
console.error('WebSocket encountered an error:', err);
|
||
|
|
ws.close();
|
||
|
|
};
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
fetchBoard();
|
||
|
|
connectWebSocket();
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
if (reconnectTimeoutRef.current) {
|
||
|
|
clearTimeout(reconnectTimeoutRef.current);
|
||
|
|
}
|
||
|
|
if (wsRef.current) {
|
||
|
|
wsRef.current.close();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}, [fetchBoard, connectWebSocket]);
|
||
|
|
|
||
|
|
// Keep player selection in sync if player updates
|
||
|
|
useEffect(() => {
|
||
|
|
if (selectedPlayer) {
|
||
|
|
const updated = boardState.players.find((p) => p.id === selectedPlayer.id);
|
||
|
|
if (updated) {
|
||
|
|
setSelectedPlayer(updated);
|
||
|
|
} else {
|
||
|
|
setSelectedPlayer(null);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, [boardState.players, selectedPlayer]);
|
||
|
|
|
||
|
|
const registerPlayer = async (name: string, color: string): Promise<Player> => {
|
||
|
|
const res = await fetch('/api/players', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ name, color }),
|
||
|
|
});
|
||
|
|
if (!res.ok) {
|
||
|
|
const errorData = await res.json().catch(() => ({}));
|
||
|
|
throw new Error(errorData.detail || 'Failed to register player');
|
||
|
|
}
|
||
|
|
const newPlayer: Player = await res.json();
|
||
|
|
setSelectedPlayer(newPlayer);
|
||
|
|
return newPlayer;
|
||
|
|
};
|
||
|
|
|
||
|
|
const removePlayer = async (id: string) => {
|
||
|
|
const res = await fetch(`/api/players/${id}`, {
|
||
|
|
method: 'DELETE',
|
||
|
|
});
|
||
|
|
if (!res.ok) {
|
||
|
|
throw new Error('Failed to remove player');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const resetBoard = async () => {
|
||
|
|
const res = await fetch('/api/board/reset', {
|
||
|
|
method: 'POST',
|
||
|
|
});
|
||
|
|
if (!res.ok) {
|
||
|
|
throw new Error('Failed to reset board');
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return {
|
||
|
|
boardState,
|
||
|
|
isConnected,
|
||
|
|
selectedPlayer,
|
||
|
|
setSelectedPlayer,
|
||
|
|
registerPlayer,
|
||
|
|
removePlayer,
|
||
|
|
resetBoard,
|
||
|
|
refresh: fetchBoard,
|
||
|
|
};
|
||
|
|
}
|