/
docNemo
/
tactic-hack-ui
Обзор
Документация
Войти
/
docNemo
/
tactic-hack-ui
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/net.ts
132 строки
4 KB
neo
Implement web client: queue, placement, battle with dashed-line deduction, result, reconnect
19 июл 2026, 00:13
19 июл 2026, 00:13
aba0849
Код
Авторство
О чём код?
import { Client, Room } from "colyseus.js"; import { AttackPayload, Cell, CLIENT_MSG, GameError, GameUpdate, LOBBY_MSG, MatchFoundPayload, ROOM_LOBBY, SERVER_MSG, SubmitPlacementPayload, } from "tactic-hack-core"; export type ConnectionStatus = | "idle" | "queued" | "playing" | "reconnecting" | "connectionLost"; const WS_URL = import.meta.env.VITE_WS_URL ?? "ws://localhost:2567"; const TOKEN_KEY = "tactic-hack.reconnectionToken"; const RECONNECT_ATTEMPTS = 40; const RECONNECT_DELAY_MS = 3000; /** Сетевой слой: очередь в лобби, игровая комната, автопереподключение. */ export class GameConnection { private client = new Client(WS_URL); private lobby: Room | null = null; private game: Room | null = null; private intentionalLeave = false; onUpdate: (update: GameUpdate) => void = () => {}; onGameError: (error: GameError) => void = () => {}; onStatus: (status: ConnectionStatus) => void = () => {}; async findGame(): Promise<void> { if (this.lobby || this.game) return; this.lobby = await this.client.joinOrCreate(ROOM_LOBBY); this.onStatus("queued"); this.lobby.onMessage(LOBBY_MSG.Queued, () => {}); this.lobby.onMessage<MatchFoundPayload>( LOBBY_MSG.MatchFound, async (payload) => { const room = await this.client.consumeSeatReservation( payload.reservation as never, ); void this.lobby?.leave(); this.lobby = null; this.attachGame(room); }, ); } async cancelQueue(): Promise<void> { await this.lobby?.leave(); this.lobby = null; this.onStatus("idle"); } /** Попытка вернуться в партию после перезагрузки страницы. */ async resumeIfPossible(): Promise<void> { const token = sessionStorage.getItem(TOKEN_KEY); if (!token || this.game) return; this.onStatus("reconnecting"); try { this.attachGame(await this.client.reconnect(token)); } catch { sessionStorage.removeItem(TOKEN_KEY); this.onStatus("idle"); } } private attachGame(room: Room) { this.game = room; this.intentionalLeave = false; sessionStorage.setItem(TOKEN_KEY, room.reconnectionToken); this.onStatus("playing"); room.onMessage<GameUpdate>(SERVER_MSG.Update, (u) => this.onUpdate(u)); room.onMessage<GameError>(SERVER_MSG.Error, (e) => this.onGameError(e)); room.onLeave((code) => { this.game = null; if (this.intentionalLeave || code === 1000) { sessionStorage.removeItem(TOKEN_KEY); return; } void this.tryReconnect(); }); } private async tryReconnect(attempt = 0): Promise<void> { const token = sessionStorage.getItem(TOKEN_KEY); if (!token) { this.onStatus("connectionLost"); return; } this.onStatus("reconnecting"); try { this.attachGame(await this.client.reconnect(token)); } catch { if (attempt < RECONNECT_ATTEMPTS) { setTimeout(() => void this.tryReconnect(attempt + 1), RECONNECT_DELAY_MS); } else { sessionStorage.removeItem(TOKEN_KEY); this.onStatus("connectionLost"); } } } submitPlacement(ships: Cell[]): void { const payload: SubmitPlacementPayload = { ships }; this.game?.send(CLIENT_MSG.SubmitPlacement, payload); } attack(cell: Cell): void { const payload: AttackPayload = { cell }; this.game?.send(CLIENT_MSG.Attack, payload); } surrender(): void { this.game?.send(CLIENT_MSG.Surrender); } async leaveGame(): Promise<void> { this.intentionalLeave = true; sessionStorage.removeItem(TOKEN_KEY); await this.game?.leave(true); this.game = null; this.onStatus("idle"); } }