127 lines
5.1 KiB
JavaScript
127 lines
5.1 KiB
JavaScript
// 饥荒组队联机 Store:独立 WebSocket 连接 + 房间状态(建房/邀请码/座位/皮肤/准备)
|
||
// 对局内实时消息(starve_input/starve_state/starve_end)直接转发给游戏组件注册的回调,
|
||
// 不落 pinia 响应式状态(高频快照,避免无谓的响应式开销)
|
||
import { defineStore } from 'pinia'
|
||
import { toast } from '../api/http'
|
||
|
||
// 游戏内消息回调(模块级,非响应式):StarveGame 挂载对局时注册
|
||
let gameHandler = null
|
||
|
||
export const useStarveCoopStore = defineStore('starveCoop', {
|
||
state: () => ({
|
||
ws: null, // WebSocket 实例(独立于对战大厅的连接)
|
||
connected: false, // 连接状态
|
||
roomState: null, // 房间状态快照(座位/皮肤/准备/状态)
|
||
closedReason: '', // 房间被解散原因
|
||
pingTimer: null, // 应用层心跳
|
||
}),
|
||
getters: {
|
||
inRoom: (s) => !!s.roomState,
|
||
mySeat: (s) => s.roomState?.my_seat ?? -1,
|
||
isHost: (s) => s.roomState && s.roomState.host_id === s.roomState.seats?.[s.roomState.my_seat]?.user_id,
|
||
// 已入座的玩家列表(联机开局用)
|
||
activePlayers: (s) =>
|
||
(s.roomState?.seats || [])
|
||
.filter((seat) => seat.occupied)
|
||
.map((seat) => ({ seat: seat.index, name: seat.name, skin: seat.skin || 'wilson', userId: seat.user_id })),
|
||
},
|
||
actions: {
|
||
// 建立独立连接;带 JWT,成功后 resolve
|
||
connect() {
|
||
if (this.ws && (this.ws.readyState === 0 || this.ws.readyState === 1)) {
|
||
return Promise.resolve()
|
||
}
|
||
const token = localStorage.getItem('token')
|
||
let host = location.host
|
||
let proto = location.protocol === 'https:' ? 'wss' : 'ws'
|
||
const base = window.desktop?.serverBase
|
||
if (base) {
|
||
const u = new URL(base)
|
||
host = u.host
|
||
proto = u.protocol === 'https:' ? 'wss' : 'ws'
|
||
}
|
||
const url = `${proto}://${host}/ws?token=${encodeURIComponent(token)}`
|
||
return new Promise((resolve, reject) => {
|
||
const ws = new WebSocket(url)
|
||
this.ws = ws
|
||
ws.onopen = () => {
|
||
this.connected = true
|
||
this.pingTimer = setInterval(() => this.send('ping', {}), 30000)
|
||
resolve()
|
||
}
|
||
ws.onmessage = (ev) => this.handleMessage(JSON.parse(ev.data))
|
||
ws.onclose = () => {
|
||
// 主动 disconnect 时 this.ws 已被置 null,这里只处理"意外断线":
|
||
// 后端断线即移除座位,客户端必须同步清掉房间状态,否则 UI 仍显示在房间内
|
||
if (this.ws !== ws) return
|
||
this.connected = false
|
||
clearInterval(this.pingTimer)
|
||
if (this.roomState) {
|
||
this.roomState = null
|
||
gameHandler?.('room_closed', { reason: '连接已断开' })
|
||
toast('连接已断开,请重新建房或加入', 'info')
|
||
}
|
||
}
|
||
ws.onerror = () => reject(new Error('连接失败'))
|
||
})
|
||
},
|
||
handleMessage(msg) {
|
||
const { type, data } = msg
|
||
switch (type) {
|
||
case 'room_state':
|
||
this.roomState = data
|
||
gameHandler?.(type, data)
|
||
break
|
||
case 'starve_input':
|
||
case 'starve_state':
|
||
case 'starve_end':
|
||
// 对局内实时消息:直接交给游戏组件(主机收 input,客机收 state,全员收 end)
|
||
gameHandler?.(type, data)
|
||
break
|
||
case 'room_closed':
|
||
this.closedReason = data.reason || '房间已解散'
|
||
this.roomState = null
|
||
gameHandler?.('room_closed', data)
|
||
toast(this.closedReason, 'info')
|
||
break
|
||
case 'error':
|
||
toast(data.msg || '操作失败')
|
||
break
|
||
// chat_msg / friend_event 由主连接(battle store)负责,这里忽略避免重复处理
|
||
}
|
||
},
|
||
send(type, data = {}) {
|
||
if (this.ws?.readyState === 1) {
|
||
this.ws.send(JSON.stringify({ type, data }))
|
||
}
|
||
},
|
||
// ---- 房间操作 ----
|
||
createRoom() { this.send('create_room', { game: 'starve', mode: 'pvp' }) },
|
||
joinRoom(code) { this.send('join_room', { code: String(code || '').trim().toUpperCase() }) },
|
||
leaveRoom() {
|
||
this.send('leave_room')
|
||
this.roomState = null
|
||
},
|
||
setReady(ready) { this.send('ready', { ready }) },
|
||
setSkin(code) { this.send('skin', { skin: code }) },
|
||
startGame() { this.send('start') },
|
||
// ---- 对局内消息 ----
|
||
sendInput(data) { this.send('starve_input', data) }, // 客机 → 房主(服务端转发)
|
||
sendState(data) { this.send('starve_state', data) }, // 房主 → 其余座位(服务端广播)
|
||
sendOver() { this.send('starve_over') }, // 房主宣告全灭结束
|
||
// 游戏组件注册/注销对局消息回调
|
||
setGameHandler(cb) { gameHandler = cb },
|
||
clearGameHandler() { gameHandler = null },
|
||
// 断开连接(退出游玩页时调用)
|
||
disconnect() {
|
||
clearInterval(this.pingTimer)
|
||
const ws = this.ws
|
||
this.ws = null // 先置空再 close,onclose 回调据此识别"主动断开"并跳过断线处理
|
||
ws?.close()
|
||
this.connected = false
|
||
this.roomState = null
|
||
gameHandler = null
|
||
},
|
||
},
|
||
})
|