Files
client-web/game.js

519 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');