add matchmaking progress screen
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
export class GameConnectionError extends Error {
|
||||
constructor(code, message) {
|
||||
super(message)
|
||||
this.code = code // 'connection' | 'join_timeout' | 'closed'
|
||||
}
|
||||
}
|
||||
|
||||
// Опкоды протокола game-server
|
||||
const OP_JOIN = 1
|
||||
const OP_END = 2
|
||||
const OP_STATE = 3
|
||||
const OP_ACK = 4
|
||||
|
||||
const CLOSE_REASONS = {
|
||||
4000: 'Соединение заменено новым',
|
||||
4001: 'Ожидалось сообщение JOIN',
|
||||
4002: 'Некорректное сообщение',
|
||||
4003: 'Недействительный игровой токен',
|
||||
4004: 'Вы не участник этой партии',
|
||||
}
|
||||
|
||||
/**
|
||||
* Подключается к game-server и отправляет JOIN. Резолвится, когда партия
|
||||
* началась (получен STATE с кодом 1 — расстановка кораблей). Возвращает
|
||||
* объект с promise и функцией отмены `cancel()`.
|
||||
*
|
||||
* @param {{ gameServerUrl: string, gameToken: string, userToken: string }} session
|
||||
*/
|
||||
export function createGameConnection({ gameServerUrl, gameToken, userToken }) {
|
||||
let ws = null
|
||||
let settled = false
|
||||
let lastSeq = 0
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const fail = (err) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
cleanup()
|
||||
reject(err)
|
||||
}
|
||||
|
||||
const succeed = (connection) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
resolve(connection)
|
||||
// Сокет НЕ закрываем — соединение переиспользуется для игры
|
||||
ws.onerror = null
|
||||
ws.onclose = (event) => {
|
||||
if (connection.onClose) connection.onClose(event.code)
|
||||
}
|
||||
}
|
||||
|
||||
ws = new WebSocket(gameServerUrl)
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send(JSON.stringify([OP_JOIN, gameToken, userToken]))
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
let msg
|
||||
try {
|
||||
msg = JSON.parse(event.data)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (!Array.isArray(msg) || typeof msg[0] !== 'number') return
|
||||
|
||||
const [opcode, seq] = msg
|
||||
if (typeof seq === 'number' && seq > lastSeq) {
|
||||
lastSeq = seq
|
||||
ack(seq)
|
||||
}
|
||||
|
||||
if (opcode === OP_STATE) {
|
||||
const state = msg[2]
|
||||
if (state === 1) {
|
||||
succeed({ socket: ws, send: sendMessage })
|
||||
}
|
||||
} else if (opcode === OP_END) {
|
||||
const code = msg[2]
|
||||
const reason = msg[3] || 'Партия завершена'
|
||||
fail(new GameConnectionError(code === -1 ? 'join_timeout' : 'closed', reason))
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
if (!settled) fail(new GameConnectionError('connection', 'Сетевая ошибка'))
|
||||
}
|
||||
|
||||
ws.onclose = (event) => {
|
||||
if (!settled) {
|
||||
const message =
|
||||
CLOSE_REASONS[event.code] ||
|
||||
(event.code === 1000 ? 'Соединение закрыто' : 'Соединение прервано')
|
||||
fail(new GameConnectionError('closed', message))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function sendMessage(payload) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(payload))
|
||||
}
|
||||
}
|
||||
|
||||
function ack(seq) {
|
||||
sendMessage([OP_ACK, seq])
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (ws) {
|
||||
ws.onopen = ws.onmessage = ws.onerror = ws.onclose = null
|
||||
ws.close()
|
||||
ws = null
|
||||
}
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if (settled) return
|
||||
settled = true
|
||||
cleanup()
|
||||
}
|
||||
|
||||
return { promise, cancel }
|
||||
}
|
||||
Reference in New Issue
Block a user