first commit

This commit is contained in:
jakciehan
2026-04-10 22:59:39 +08:00
commit cc2e7b9bb0
89 changed files with 23631 additions and 0 deletions
+528
View File
@@ -0,0 +1,528 @@
/**
* NetworkManager.js
* Manages WebSocket connection for PVP online multiplayer.
* Handles connection lifecycle, heartbeat, reconnection, and message routing.
*/
const { NET_MSG } = require('../base/GameGlobal');
class NetworkManager {
constructor() {
/** @type {WebSocket|null} */
this._ws = null;
/** @type {string} Server URL */
this._serverUrl = '';
/** @type {boolean} */
this._connected = false;
/** @type {boolean} */
this._connecting = false;
/** @type {string|null} Current room ID */
this._roomId = null;
/** @type {number} Player slot (1 or 2) */
this._playerSlot = 0;
/** @type {string} Unique player ID */
this._playerId = '';
// Heartbeat
this._heartbeatInterval = null;
this._heartbeatTimeout = null;
this._heartbeatMs = 5000;
this._heartbeatTimeoutMs = 10000;
// Reconnection
this._reconnectAttempts = 0;
this._maxReconnectAttempts = 3;
this._reconnectDelay = 2000;
this._reconnectTimer = null;
this._shouldReconnect = false;
// Message handlers
/** @type {Map<string, Array<Function>>} */
this._handlers = new Map();
// Latency tracking
this._lastPingTime = 0;
this._latency = 0;
// Generate a unique player ID
this._playerId = this._generatePlayerId();
}
/**
* Connect to the WebSocket server.
* @param {string} serverUrl - WebSocket server URL (e.g., 'wss://example.com/ws').
* @returns {Promise<boolean>} Whether connection succeeded.
*/
connect(serverUrl) {
return new Promise((resolve) => {
if (this._connected || this._connecting) {
resolve(this._connected);
return;
}
this._serverUrl = serverUrl;
this._connecting = true;
this._shouldReconnect = true;
try {
this._ws = wx.connectSocket({
url: serverUrl,
header: { 'content-type': 'application/json' },
});
this._ws.onOpen(() => {
console.log('[NetworkManager] Connected to server');
this._connected = true;
this._connecting = false;
this._reconnectAttempts = 0;
this._startHeartbeat();
this._emit('connected');
resolve(true);
});
this._ws.onMessage((res) => {
this._handleMessage(res.data);
});
this._ws.onError((err) => {
console.error('[NetworkManager] WebSocket error:', err);
this._connecting = false;
this._emit('error', err);
resolve(false);
});
this._ws.onClose((res) => {
console.log('[NetworkManager] Connection closed:', res.code, res.reason);
this._connected = false;
this._connecting = false;
this._stopHeartbeat();
this._emit('disconnected', { code: res.code, reason: res.reason });
// Auto-reconnect if needed
if (this._shouldReconnect && this._reconnectAttempts < this._maxReconnectAttempts) {
this._attemptReconnect();
}
});
} catch (e) {
console.error('[NetworkManager] Failed to create WebSocket:', e);
this._connecting = false;
resolve(false);
}
});
}
/**
* Disconnect from the server.
*/
disconnect() {
this._shouldReconnect = false;
this._stopHeartbeat();
this._clearReconnectTimer();
if (this._ws) {
try {
this._ws.close({});
} catch (e) {
// Ignore close errors
}
this._ws = null;
}
this._connected = false;
this._connecting = false;
this._roomId = null;
this._playerSlot = 0;
}
/**
* Send a message to the server.
* @param {string} type - Message type from NET_MSG.
* @param {object} [data={}] - Message payload.
*/
send(type, data = {}) {
if (!this._connected || !this._ws) {
console.warn('[NetworkManager] Cannot send, not connected');
return;
}
const message = JSON.stringify({
type,
data,
playerId: this._playerId,
roomId: this._roomId,
timestamp: Date.now(),
});
try {
this._ws.send({ data: message });
} catch (e) {
console.error('[NetworkManager] Send error:', e);
}
}
/**
* Create a new room on the server.
*/
createRoom() {
this.send(NET_MSG.CREATE_ROOM, {
playerId: this._playerId,
});
}
/**
* Join an existing room.
* @param {string} roomId - Room ID to join.
*/
joinRoom(roomId) {
this.send(NET_MSG.JOIN_ROOM, {
playerId: this._playerId,
roomId: roomId,
});
}
/**
* Send player input to the server.
* @param {object} input - { direction, firing, x, y }
*/
sendInput(input) {
this.send(NET_MSG.PLAYER_INPUT, input);
}
/**
* Send player state for synchronization.
* @param {object} state - { x, y, direction, hp, alive }
*/
sendState(state) {
this.send(NET_MSG.PLAYER_STATE, state);
}
/**
* Send bullet fire event.
* @param {object} bulletData - { x, y, direction }
*/
sendBulletFire(bulletData) {
this.send(NET_MSG.BULLET_FIRE, bulletData);
}
// ============================================================
// 3v3 Team Methods
// ============================================================
/**
* Create a new team for 3v3 mode.
*/
createTeam() {
this.send(NET_MSG.CREATE_TEAM, {
playerId: this._playerId,
});
}
/**
* Join an existing team by teamId.
* @param {string} teamId - Team ID to join.
*/
joinTeam(teamId) {
this.send(NET_MSG.JOIN_TEAM, {
playerId: this._playerId,
teamId,
});
}
/**
* Leave the current team.
*/
leaveTeam() {
this.send(NET_MSG.LEAVE_TEAM, {
playerId: this._playerId,
});
}
/**
* Toggle ready state in team room.
* @param {boolean} ready - Whether the player is ready.
*/
teamReady(ready) {
this.send(NET_MSG.TEAM_READY, {
playerId: this._playerId,
ready,
});
}
/**
* Start matchmaking (leader only).
*/
startMatch() {
this.send(NET_MSG.MATCH_START, {
playerId: this._playerId,
});
}
/**
* Cancel matchmaking (leader only).
*/
cancelMatch() {
this.send(NET_MSG.MATCH_CANCEL, {
playerId: this._playerId,
});
}
/**
* Kick a player from the team (leader only).
* @param {string} targetPlayerId - Player ID to kick.
*/
kickPlayer(targetPlayerId) {
this.send(NET_MSG.TEAM_KICK, {
playerId: this._playerId,
targetPlayerId,
});
}
/**
* Disband the team (leader only).
*/
disbandTeam() {
this.send(NET_MSG.TEAM_DISBAND, {
playerId: this._playerId,
});
}
/**
* Start solo matchmaking for 3v3.
*/
soloMatch() {
this.send(NET_MSG.SOLO_MATCH, {
playerId: this._playerId,
});
}
/**
* Attempt to reconnect to an ongoing team game.
* @param {string} teamId - Team room ID.
*/
reconnectToTeam(teamId) {
this.send(NET_MSG.RECONNECT, {
teamId,
playerId: this._playerId,
});
}
/**
* Register a handler for a message type.
* @param {string} type - Message type.
* @param {Function} handler - Callback function(data).
* @returns {Function} Unsubscribe function.
*/
on(type, handler) {
if (!this._handlers.has(type)) {
this._handlers.set(type, []);
}
this._handlers.get(type).push(handler);
return () => {
const list = this._handlers.get(type);
if (list) {
const idx = list.indexOf(handler);
if (idx !== -1) list.splice(idx, 1);
}
};
}
/**
* Remove a handler for a message type.
* @param {string} type
* @param {Function} handler
*/
off(type, handler) {
const list = this._handlers.get(type);
if (list) {
const idx = list.indexOf(handler);
if (idx !== -1) list.splice(idx, 1);
}
}
/**
* Remove all handlers.
*/
clearHandlers() {
this._handlers.clear();
}
// ============================================================
// Private Methods
// ============================================================
/**
* Handle incoming WebSocket message.
* @private
*/
_handleMessage(rawData) {
try {
const msg = JSON.parse(rawData);
const { type, data } = msg;
// Handle system messages
if (type === NET_MSG.PONG) {
this._latency = Date.now() - this._lastPingTime;
this._resetHeartbeatTimeout();
return;
}
if (type === NET_MSG.ROOM_CREATED) {
this._roomId = data.roomId;
this._playerSlot = 1; // Creator is player 1
} else if (type === NET_MSG.ROOM_JOINED) {
this._roomId = data.roomId;
this._playerSlot = data.playerSlot || 2;
}
// Emit to registered handlers
this._emit(type, data);
} catch (e) {
console.error('[NetworkManager] Failed to parse message:', e, rawData);
}
}
/**
* Emit an event to registered handlers.
* @private
*/
_emit(type, data) {
const list = this._handlers.get(type);
if (list) {
for (const handler of list) {
try {
handler(data);
} catch (e) {
console.error(`[NetworkManager] Handler error for "${type}":`, e);
}
}
}
}
/**
* Start heartbeat ping/pong.
* @private
*/
_startHeartbeat() {
this._stopHeartbeat();
this._heartbeatInterval = setInterval(() => {
if (this._connected) {
this._lastPingTime = Date.now();
this.send(NET_MSG.PING);
this._startHeartbeatTimeout();
}
}, this._heartbeatMs);
}
/**
* Stop heartbeat.
* @private
*/
_stopHeartbeat() {
if (this._heartbeatInterval) {
clearInterval(this._heartbeatInterval);
this._heartbeatInterval = null;
}
this._resetHeartbeatTimeout();
}
/**
* Start heartbeat timeout (disconnect if no pong received).
* @private
*/
_startHeartbeatTimeout() {
this._resetHeartbeatTimeout();
this._heartbeatTimeout = setTimeout(() => {
console.warn('[NetworkManager] Heartbeat timeout, disconnecting');
this.disconnect();
}, this._heartbeatTimeoutMs);
}
/**
* Reset heartbeat timeout.
* @private
*/
_resetHeartbeatTimeout() {
if (this._heartbeatTimeout) {
clearTimeout(this._heartbeatTimeout);
this._heartbeatTimeout = null;
}
}
/**
* Attempt to reconnect to the server.
* @private
*/
_attemptReconnect() {
this._clearReconnectTimer();
this._reconnectAttempts++;
console.log(`[NetworkManager] Reconnect attempt ${this._reconnectAttempts}/${this._maxReconnectAttempts}`);
this._emit('reconnecting', { attempt: this._reconnectAttempts });
this._reconnectTimer = setTimeout(() => {
this.connect(this._serverUrl);
}, this._reconnectDelay * this._reconnectAttempts);
}
/**
* Clear reconnect timer.
* @private
*/
_clearReconnectTimer() {
if (this._reconnectTimer) {
clearTimeout(this._reconnectTimer);
this._reconnectTimer = null;
}
}
/**
* Generate a unique player ID.
* @private
* @returns {string}
*/
_generatePlayerId() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let id = 'p_';
for (let i = 0; i < 8; i++) {
id += chars.charAt(Math.floor(Math.random() * chars.length));
}
return id + '_' + Date.now().toString(36);
}
// ============================================================
// Getters
// ============================================================
/** Whether currently connected. */
get connected() {
return this._connected;
}
/** Current room ID. */
get roomId() {
return this._roomId;
}
/** Player slot (1 or 2). */
get playerSlot() {
return this._playerSlot;
}
/** Player unique ID. */
get playerId() {
return this._playerId;
}
/** Current latency in ms. */
get latency() {
return this._latency;
}
/** Whether currently connecting. */
get connecting() {
return this._connecting;
}
}
module.exports = NetworkManager;