初次提交

This commit is contained in:
2026-01-07 11:34:02 +08:00
parent 48298c38bc
commit 327df5d61c
12 changed files with 19906 additions and 1 deletions

519
game.js Normal file
View File

@@ -0,0 +1,519 @@
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');