519 lines
21 KiB
JavaScript
519 lines
21 KiB
JavaScript
const { createApp } = Vue;
|
||
|
||
// WebSocket服务类
|
||
class WebSocketService {
|
||
constructor() {
|
||
this.connections = new Map();
|
||
this.reconnectAttempts = new Map();
|
||
this.maxReconnectAttempts = 5;
|
||
this.reconnectDelay = 1000;
|
||
}
|
||
|
||
connect(squareId, onMessage, onStatusChange) {
|
||
const wsUrl = `wss://www.hlsq.asia/ws/?token=${squareId}`;
|
||
|
||
try {
|
||
const ws = new WebSocket(wsUrl);
|
||
ws.binaryType = 'arraybuffer';
|
||
|
||
ws.onopen = () => {
|
||
console.log(`WebSocket ${squareId} connected`);
|
||
onStatusChange(true);
|
||
this.reconnectAttempts.set(squareId, 0);
|
||
|
||
// 发送进入实例消息(protobuf)
|
||
protoUtils.readyPromise.then(() => {
|
||
const enterInstanceBuffer = protoUtils.createEnterInstanceMessage(1);
|
||
if (enterInstanceBuffer) {
|
||
this.sendMessage(ws, enterInstanceBuffer);
|
||
}
|
||
});
|
||
};
|
||
|
||
ws.onclose = (e) => {
|
||
console.log(`WebSocket ${squareId} disconnected:`, e.code, e.reason);
|
||
onStatusChange(false);
|
||
this.connections.delete(squareId);
|
||
|
||
// 自动重连
|
||
this.scheduleReconnect(squareId, onMessage, onStatusChange);
|
||
};
|
||
|
||
ws.onerror = (error) => {
|
||
console.error(`WebSocket ${squareId} error:`, error);
|
||
};
|
||
|
||
ws.onmessage = (event) => {
|
||
try {
|
||
onMessage(event.data, squareId);
|
||
} catch (error) {
|
||
console.error('Error processing message:', error);
|
||
}
|
||
};
|
||
|
||
this.connections.set(squareId, ws);
|
||
} catch (error) {
|
||
console.error(`WebSocket ${squareId} init error:`, error);
|
||
}
|
||
}
|
||
|
||
sendMessage(ws, data) {
|
||
if (ws.readyState === WebSocket.OPEN) {
|
||
try {
|
||
if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
|
||
ws.send(data);
|
||
} else {
|
||
console.error('Attempted to send non-binary data through binary channel.');
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to send message:', error);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 发送动作消息
|
||
sendAction(squareId, actionId, x, y) {
|
||
const ws = this.connections.get(squareId);
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
console.warn(`Cannot send action: WebSocket ${squareId} not connected`);
|
||
return false;
|
||
}
|
||
|
||
protoUtils.readyPromise.then(() => {
|
||
const actionBuffer = protoUtils.createActionMessage(actionId, x, y);
|
||
if (actionBuffer) {
|
||
this.sendMessage(ws, actionBuffer);
|
||
console.log(`Action sent to square ${squareId}: actionId=${actionId}, x=${x}, y=${y}`);
|
||
return true;
|
||
}
|
||
});
|
||
|
||
return false;
|
||
}
|
||
|
||
// 发送方向动作消息
|
||
sendDirectionAction(squareId, directionBits) {
|
||
const ws = this.connections.get(squareId);
|
||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||
console.warn(`Cannot send direction action: WebSocket ${squareId} not connected`);
|
||
return false;
|
||
}
|
||
|
||
protoUtils.readyPromise.then(() => {
|
||
const actionBuffer = protoUtils.createDirectionActionMessage(directionBits);
|
||
if (actionBuffer) {
|
||
this.sendMessage(ws, actionBuffer);
|
||
console.log(`Direction action sent to square ${squareId}: directionBits=${directionBits}`);
|
||
return true;
|
||
}
|
||
});
|
||
|
||
return false;
|
||
}
|
||
|
||
scheduleReconnect(squareId, onMessage, onStatusChange) {
|
||
const attempts = this.reconnectAttempts.get(squareId) || 0;
|
||
|
||
if (attempts < this.maxReconnectAttempts) {
|
||
const delay = this.reconnectDelay * Math.pow(2, attempts);
|
||
this.reconnectAttempts.set(squareId, attempts + 1);
|
||
|
||
setTimeout(() => {
|
||
console.log(`Attempting to reconnect WebSocket ${squareId} (attempt ${attempts + 1})`);
|
||
this.connect(squareId, onMessage, onStatusChange);
|
||
}, delay);
|
||
} else {
|
||
console.error(`Max reconnection attempts reached for WebSocket ${squareId}`);
|
||
}
|
||
}
|
||
|
||
disconnect(squareId) {
|
||
const ws = this.connections.get(squareId);
|
||
if (ws) {
|
||
ws.close();
|
||
this.connections.delete(squareId);
|
||
}
|
||
}
|
||
|
||
disconnectAll() {
|
||
this.connections.forEach((ws, squareId) => {
|
||
this.disconnect(squareId);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 方块组件
|
||
const SquareComponent = {
|
||
props: ['squareId', 'connected', 'dots'],
|
||
template: `
|
||
<div class="square">
|
||
<div class="connection-dot" :class="{ connected: connected }"></div>
|
||
<div class="loading-indicator" v-if="!connected">
|
||
<div class="spinner"></div>
|
||
</div>
|
||
<div
|
||
v-for="(dot, index) in dots"
|
||
:key="dot.uid"
|
||
class="game-dot"
|
||
:class="{ 'player-controlled': dot.isPlayerControlled }"
|
||
:style="{ left: dot.x + 'px', top: transformY(dot.y) + 'px' }"
|
||
></div>
|
||
</div>
|
||
`,
|
||
methods: {
|
||
// 转换Y坐标,使左下角为原点
|
||
transformY(y) {
|
||
// 假设方块高度为400px(从GameConfig获取)
|
||
const height = GameConfig.game.gridSize.height;
|
||
// 将y坐标从"从下到上"转换为"从上到下"
|
||
return height - y;
|
||
}
|
||
}
|
||
};
|
||
|
||
// 主应用组件
|
||
const App = {
|
||
components: {
|
||
'square-component': SquareComponent
|
||
},
|
||
template: `
|
||
<div class="app-container">
|
||
<div class="grid-container">
|
||
<square-component
|
||
v-for="(square, index) in squares"
|
||
:key="index"
|
||
:square-id="index + 1"
|
||
:connected="square.connected"
|
||
:dots="square.dots"
|
||
@mouseenter="setActiveSquare(index + 1)"
|
||
@mouseleave="clearActiveSquare()"
|
||
></square-component>
|
||
</div>
|
||
<div class="status-bar" v-if="showStatusBar">
|
||
<div class="status-item">
|
||
连接状态: {{ connectedCount }}/{{ squares.length }}
|
||
</div>
|
||
<div class="status-item">
|
||
总圆点数: {{ totalDots }}
|
||
</div>
|
||
<div class="status-item" v-if="activeSquareId">
|
||
当前方块: {{ activeSquareId }}
|
||
</div>
|
||
<div class="status-item" v-if="directionState.active">
|
||
方向键: {{ directionStateText }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`,
|
||
data() {
|
||
return {
|
||
squares: [
|
||
{ connected: false, dots: [] },
|
||
{ connected: false, dots: [] },
|
||
{ connected: false, dots: [] },
|
||
{ connected: false, dots: [] }
|
||
],
|
||
wsService: null,
|
||
showStatusBar: true,
|
||
activeSquareId: null,
|
||
directionState: {
|
||
active: false,
|
||
w: false, // 1
|
||
s: false, // 2
|
||
a: false, // 4
|
||
d: false // 8
|
||
},
|
||
directionKeyCodes: {
|
||
'KeyW': 'w',
|
||
'KeyS': 's',
|
||
'KeyA': 'a',
|
||
'KeyD': 'd',
|
||
// 兼容箭头键
|
||
'ArrowUp': 'w',
|
||
'ArrowDown': 's',
|
||
'ArrowLeft': 'a',
|
||
'ArrowRight': 'd'
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
connectedCount() {
|
||
return this.squares.filter(square => square.connected).length;
|
||
},
|
||
totalDots() {
|
||
return this.squares.reduce((total, square) => total + square.dots.length, 0);
|
||
},
|
||
// 计算方向状态文本
|
||
directionStateText() {
|
||
const keys = [];
|
||
if (this.directionState.w) keys.push('W');
|
||
if (this.directionState.s) keys.push('S');
|
||
if (this.directionState.a) keys.push('A');
|
||
if (this.directionState.d) keys.push('D');
|
||
return keys.join('+') || '无';
|
||
},
|
||
// 计算方向位
|
||
directionBits() {
|
||
let bits = 0;
|
||
if (this.directionState.w) bits |= 1; // W = 1
|
||
if (this.directionState.s) bits |= 2; // S = 2
|
||
if (this.directionState.a) bits |= 4; // A = 4
|
||
if (this.directionState.d) bits |= 8; // D = 8
|
||
return bits;
|
||
}
|
||
},
|
||
mounted() {
|
||
this.wsService = new WebSocketService();
|
||
this.initializeConnections();
|
||
|
||
// 添加键盘事件监听
|
||
window.addEventListener('keydown', this.handleKeyDown);
|
||
window.addEventListener('keyup', this.handleKeyUp);
|
||
|
||
// 添加键盘快捷键
|
||
this.addKeyboardShortcuts();
|
||
},
|
||
beforeUnmount() {
|
||
if (this.wsService) {
|
||
this.wsService.disconnectAll();
|
||
}
|
||
|
||
// 移除键盘事件监听
|
||
window.removeEventListener('keydown', this.handleKeyDown);
|
||
window.removeEventListener('keyup', this.handleKeyUp);
|
||
},
|
||
methods: {
|
||
initializeConnections() {
|
||
this.squares.forEach((square, index) => {
|
||
const squareId = index + 1;
|
||
this.wsService.connect(
|
||
squareId,
|
||
this.handleMessage,
|
||
(connected) => this.updateConnectionStatus(index, connected)
|
||
);
|
||
});
|
||
},
|
||
|
||
updateConnectionStatus(index, connected) {
|
||
this.squares[index].connected = connected;
|
||
},
|
||
|
||
handleMessage(rawData, squareId) {
|
||
const squareIndex = squareId - 1;
|
||
|
||
try {
|
||
// 仅处理二进制 protobuf 消息
|
||
if (rawData instanceof ArrayBuffer || rawData instanceof Uint8Array) {
|
||
const arrayBuf = rawData instanceof Uint8Array ? rawData : new Uint8Array(rawData);
|
||
const outerMsg = protoUtils.encoder.decodeMessage(arrayBuf);
|
||
if (!outerMsg) return;
|
||
|
||
const msgId = outerMsg.ID;
|
||
const msgEnum = protoUtils.encoder.MessageID;
|
||
|
||
// 根据消息ID处理不同类型的消息
|
||
switch (msgId) {
|
||
case msgEnum.MESSAGE_ID_POSITION: {
|
||
// 处理位置更新消息
|
||
const S2C_Position = protoUtils.encoder.root.lookupType('S2C_Position');
|
||
const positionMsg = S2C_Position.decode(outerMsg.Payload);
|
||
|
||
// 处理所有位置信息
|
||
if (positionMsg.Info && positionMsg.Info.length > 0) {
|
||
positionMsg.Info.forEach(info => {
|
||
// 注意:坐标已经是以左下角为原点,保持原样
|
||
if (utils.validateCoordinates(info.X, info.Y)) {
|
||
// 查找是否已存在该UID的点
|
||
const existingDotIndex = this.squares[squareIndex].dots.findIndex(dot => dot.uid === info.UID);
|
||
|
||
if (existingDotIndex !== -1) {
|
||
// 已存在,更新位置
|
||
this.squares[squareIndex].dots[existingDotIndex].x = info.X;
|
||
this.squares[squareIndex].dots[existingDotIndex].y = info.Y;
|
||
this.squares[squareIndex].dots[existingDotIndex].timestamp = Date.now();
|
||
console.log(`Dot updated for square ${squareId}, UID ${info.UID}: (${info.X}, ${info.Y})`);
|
||
} else {
|
||
// 不存在,创建新点
|
||
this.squares[squareIndex].dots.push({
|
||
x: info.X,
|
||
y: info.Y,
|
||
timestamp: Date.now(),
|
||
uid: info.UID,
|
||
// 判断是否是玩家控制的点(假设UID为当前squareId的是玩家控制的)
|
||
isPlayerControlled: info.UID === squareId
|
||
});
|
||
|
||
// 限制每个方块的圆点数量,避免内存泄漏
|
||
if (this.squares[squareIndex].dots.length > GameConfig.game.maxDotsPerSquare) {
|
||
this.squares[squareIndex].dots.shift();
|
||
}
|
||
|
||
console.log(`New dot added to square ${squareId} at (${info.X}, ${info.Y}) for UID ${info.UID}`);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
break;
|
||
}
|
||
|
||
case msgEnum.MESSAGE_ID_ENTER_INSTANCE: {
|
||
// 处理进入实例响应消息
|
||
const S2C_EnterInstance = protoUtils.encoder.root.lookupType('S2C_EnterInstance');
|
||
const enterInstanceMsg = S2C_EnterInstance.decode(outerMsg.Payload);
|
||
|
||
// 如果有位置信息,添加到对应方块
|
||
if (enterInstanceMsg.Info) {
|
||
const info = enterInstanceMsg.Info;
|
||
// 注意:坐标已经是以左下角为原点,保持原样
|
||
if (utils.validateCoordinates(info.X, info.Y)) {
|
||
// 查找是否已存在该UID的点
|
||
const existingDotIndex = this.squares[squareIndex].dots.findIndex(dot => dot.uid === info.UID);
|
||
|
||
if (existingDotIndex !== -1) {
|
||
// 已存在,更新位置
|
||
this.squares[squareIndex].dots[existingDotIndex].x = info.X;
|
||
this.squares[squareIndex].dots[existingDotIndex].y = info.Y;
|
||
this.squares[squareIndex].dots[existingDotIndex].timestamp = Date.now();
|
||
this.squares[squareIndex].dots[existingDotIndex].isInitial = true;
|
||
this.squares[squareIndex].dots[existingDotIndex].isPlayerControlled = info.UID === squareId;
|
||
console.log(`Initial position updated for square ${squareId}, UID ${info.UID}: (${info.X}, ${info.Y})`);
|
||
} else {
|
||
// 不存在,创建新点
|
||
this.squares[squareIndex].dots.push({
|
||
x: info.X,
|
||
y: info.Y,
|
||
timestamp: Date.now(),
|
||
uid: info.UID,
|
||
isInitial: true, // 标记为初始位置
|
||
// 判断是否是玩家控制的点(假设UID为当前squareId的是玩家控制的)
|
||
isPlayerControlled: info.UID === squareId
|
||
});
|
||
|
||
console.log(`Initial position created for square ${squareId}: (${info.X}, ${info.Y}) UID: ${info.UID}`);
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
|
||
default:
|
||
console.warn('Unhandled message ID:', msgId);
|
||
break;
|
||
}
|
||
return; // 已处理protobuf
|
||
}
|
||
} catch (error) {
|
||
console.error('Error processing message:', error);
|
||
}
|
||
},
|
||
|
||
// 设置当前活动方块
|
||
setActiveSquare(squareId) {
|
||
if (this.squares[squareId - 1].connected) {
|
||
this.activeSquareId = squareId;
|
||
this.directionState.active = true;
|
||
console.log(`Active square set to ${squareId}`);
|
||
}
|
||
},
|
||
|
||
// 清除当前活动方块
|
||
clearActiveSquare() {
|
||
if (this.activeSquareId) {
|
||
// 在清除之前发送一个id=0的action消息
|
||
this.sendDirectionAction(this.activeSquareId, 0);
|
||
|
||
// 然后清除状态
|
||
this.activeSquareId = null;
|
||
this.directionState.active = false;
|
||
// 重置所有方向键状态
|
||
this.directionState.w = false;
|
||
this.directionState.s = false;
|
||
this.directionState.a = false;
|
||
this.directionState.d = false;
|
||
}
|
||
},
|
||
|
||
// 处理键盘按下事件
|
||
handleKeyDown(event) {
|
||
if (!this.activeSquareId) return;
|
||
|
||
const key = this.directionKeyCodes[event.code];
|
||
if (key && this.directionState.hasOwnProperty(key)) {
|
||
// 只有当状态真正变化时才发送
|
||
if (!this.directionState[key]) {
|
||
this.directionState[key] = true;
|
||
// 计算新的方向位并发送
|
||
const directionBits = this.directionBits;
|
||
this.sendDirectionAction(this.activeSquareId, directionBits);
|
||
}
|
||
event.preventDefault(); // 防止页面滚动
|
||
}
|
||
},
|
||
|
||
// 处理键盘释放事件
|
||
handleKeyUp(event) {
|
||
if (!this.activeSquareId) return;
|
||
|
||
const key = this.directionKeyCodes[event.code];
|
||
if (key && this.directionState.hasOwnProperty(key)) {
|
||
// 只有当状态真正变化时才发送
|
||
if (this.directionState[key]) {
|
||
this.directionState[key] = false;
|
||
// 计算新的方向位并发送
|
||
const directionBits = this.directionBits;
|
||
this.sendDirectionAction(this.activeSquareId, directionBits);
|
||
}
|
||
event.preventDefault();
|
||
}
|
||
},
|
||
|
||
addKeyboardShortcuts() {
|
||
document.addEventListener('keydown', (e) => {
|
||
switch (e.key) {
|
||
case 'h':
|
||
this.showStatusBar = !this.showStatusBar;
|
||
break;
|
||
case 'c':
|
||
this.clearAllDots();
|
||
break;
|
||
case 'r':
|
||
this.reconnectAll();
|
||
break;
|
||
}
|
||
});
|
||
},
|
||
|
||
clearAllDots() {
|
||
this.squares.forEach(square => {
|
||
square.dots = [];
|
||
});
|
||
console.log('All dots cleared');
|
||
},
|
||
|
||
reconnectAll() {
|
||
this.wsService.disconnectAll();
|
||
setTimeout(() => {
|
||
this.initializeConnections();
|
||
}, 1000);
|
||
console.log('Reconnecting all WebSockets...');
|
||
},
|
||
|
||
// 发送动作消息到服务器
|
||
sendAction(squareId, actionId, x, y) {
|
||
if (this.wsService) {
|
||
return this.wsService.sendAction(squareId, actionId, x, y);
|
||
}
|
||
return false;
|
||
},
|
||
|
||
// 发送方向动作消息到服务器
|
||
sendDirectionAction(squareId, directionBits) {
|
||
if (this.wsService) {
|
||
return this.wsService.sendDirectionAction(squareId, directionBits);
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
};
|
||
|
||
createApp(App).mount('#app'); |