UNPKG

swordfight-engine

Version:

A multiplayer sword fighting game engine with character management, round-based combat, and real-time multiplayer support

385 lines (382 loc) 11.7 kB
/** * swordfight-engine v1.6.21 * @license MIT */ // src/classes/transports/MultiplayerTransport.js var MultiplayerTransport = class _MultiplayerTransport { constructor(game) { this.game = game; this.started = false; if (new.target === _MultiplayerTransport) { throw new Error("MultiplayerTransport is an abstract class and cannot be instantiated directly"); } } /** * Connect to a room/session * @param {string} _roomId - The room identifier * @returns {Promise<void>} */ async connect(_roomId) { throw new Error("connect() must be implemented by subclass"); } /** * Send a move to the opponent * @param {Object} _data - Move data { move: Object, round: number } */ sendMove(_data) { throw new Error("sendMove() must be implemented by subclass"); } /** * Register callback for receiving opponent's move * @param {Function} _callback - Callback function to handle received move */ getMove(_callback) { throw new Error("getMove() must be implemented by subclass"); } /** * Send player name to opponent * @param {Object} _data - Name data { name: string } */ sendName(_data) { throw new Error("sendName() must be implemented by subclass"); } /** * Register callback for receiving opponent's name * @param {Function} _callback - Callback function to handle received name */ getName(_callback) { throw new Error("getName() must be implemented by subclass"); } /** * Send character slug to opponent * @param {Object} _data - Character data { characterSlug: string } */ sendCharacter(_data) { throw new Error("sendCharacter() must be implemented by subclass"); } /** * Register callback for receiving opponent's character slug * @param {Function} _callback - Callback function to handle received character slug */ getCharacter(_callback) { throw new Error("getCharacter() must be implemented by subclass"); } /** * Disconnect from the session */ disconnect() { throw new Error("disconnect() must be implemented by subclass"); } /** * Get the number of connected peers * @returns {number} */ getPeerCount() { throw new Error("getPeerCount() must be implemented by subclass"); } /** * Check if room is full * @returns {boolean} */ isRoomFull() { return this.getPeerCount() >= 2; } }; // src/classes/transports/DurableObjectTransport.js var DurableObjectTransport = class extends MultiplayerTransport { constructor(game, options = {}) { super(game); this.serverUrl = options.serverUrl; if (!this.serverUrl) { throw new Error("DurableObjectTransport requires options.serverUrl (your CloudFlare Worker URL)"); } this.ws = null; this.moveCallbacks = []; this.nameCallbacks = []; this.characterCallbacks = []; this.roomId = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = options.maxReconnectAttempts || 5; this.reconnectDelay = options.reconnectDelay || 1e3; } /** * Connect to a CloudFlare Worker and join a game room * @param {string} roomId - The room identifier */ async connect(roomId) { this.roomId = roomId; return new Promise((resolve, reject) => { const wsUrl = `${this.serverUrl}?room=${encodeURIComponent(roomId)}`; if (window.logging) { console.log("Connecting to CloudFlare Worker:", wsUrl); } try { this.ws = new WebSocket(wsUrl); } catch (error) { console.error("Failed to create WebSocket:", error); reject(error); return; } const timeout = setTimeout(() => { if (this.ws.readyState !== WebSocket.OPEN) { this.ws.close(); reject(new Error("Connection timeout")); } }, 1e4); this.ws.onopen = () => { clearTimeout(timeout); if (window.logging) { console.log("Connected to CloudFlare Worker"); } const playerName = this._getPlayerName(); this.game.myCharacter.name = playerName; this.sendName({ name: playerName, characterSlug: this.game.myCharacterSlug }); resolve(); }; this.ws.onmessage = (event) => { try { const message = JSON.parse(event.data); if (window.logging) { console.log("[DurableObjectTransport] Received message:", message.type, message); } this._handleMessage(message); } catch (error) { console.error("Error parsing message:", error); } }; this.ws.onerror = (error) => { clearTimeout(timeout); console.error("WebSocket error:", error); reject(error); }; this.ws.onclose = (event) => { clearTimeout(timeout); if (window.logging) { console.log("WebSocket connection closed", event.code, event.reason); } this.started = false; if (event.code !== 1e3 && this.reconnectAttempts < this.maxReconnectAttempts) { this._attemptReconnect(); } }; }); } /** * Attempt to reconnect to the server * @private */ _attemptReconnect() { this.reconnectAttempts++; if (window.logging) { console.log(`Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts}...`); } setTimeout(() => { this.connect(this.roomId).catch((error) => { console.error("Reconnection failed:", error); if (this.reconnectAttempts >= this.maxReconnectAttempts) { if (typeof document !== "undefined") { const disconnectEvent = new CustomEvent("connectionLost"); document.dispatchEvent(disconnectEvent); } } }); }, this.reconnectDelay * this.reconnectAttempts); } /** * Handle incoming WebSocket messages * @private */ _handleMessage(message) { switch (message.type) { case "history": if (window.logging) { console.log(`Replaying ${message.messages.length} buffered messages`); } message.messages.forEach((bufferedMessage) => { this._handleMessage(bufferedMessage); }); break; case "peer-joined": if (window.logging) { console.log("Peer joined the room"); } this.started = true; if (typeof document !== "undefined") { const startEvent = new CustomEvent("start", { detail: { game: this.game } }); document.dispatchEvent(startEvent); } break; case "move": if (window.logging) { console.log("[DurableObjectTransport] Processing move, callbacks:", this.moveCallbacks.length); } this.moveCallbacks.forEach((callback) => { try { if (window.logging) { console.log("[DurableObjectTransport] Calling move callback with data:", message.data); } callback(message.data); } catch (error) { console.error("Error in move callback:", error); } }); break; case "name": if (window.logging) { console.log("[DurableObjectTransport] Processing name, callbacks:", this.nameCallbacks.length); } this.nameCallbacks.forEach((callback) => { try { if (window.logging) { console.log("[DurableObjectTransport] Calling name callback with data:", message.data); } callback(message.data); } catch (error) { console.error("Error in name callback:", error); } }); break; case "character": if (window.logging) { console.log("[DurableObjectTransport] Processing character, callbacks:", this.characterCallbacks.length); } this.characterCallbacks.forEach((callback) => { try { if (window.logging) { console.log("[DurableObjectTransport] Calling character callback with data:", message.data); } callback(message.data); } catch (error) { console.error("Error in character callback:", error); } }); break; case "room-full": console.error("Room is full"); if (typeof document !== "undefined") { const roomFullEvent = new CustomEvent("roomFull"); document.dispatchEvent(roomFullEvent); } break; case "peer-left": if (window.logging) { console.log("Peer left the room"); } this.started = false; if (typeof document !== "undefined") { const peerLeftEvent = new CustomEvent("peerLeft"); document.dispatchEvent(peerLeftEvent); } break; case "error": console.error("Server error:", message.message); break; default: console.warn("Unknown message type:", message.type); } } /** * Get player name from storage or character * @private */ _getPlayerName() { if (typeof localStorage !== "undefined" && localStorage.getItem("playerName")) { return localStorage.getItem("playerName"); } return this.game.myCharacter.name; } /** * Send a move to the opponent * @param {Object} data - Move data { move: Object, round: number } */ sendMove(data) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: "move", data })); } else { console.error("Cannot send move: WebSocket not open"); } } /** * Register callback for receiving opponent's move * @param {Function} callback - Callback function to handle received move */ getMove(callback) { if (window.logging) { console.log("DurableObjectTransport: Registering move callback. Total callbacks:", this.moveCallbacks.length + 1); } this.moveCallbacks.push(callback); } /** * Send player name to opponent * @param {Object} data - Name data { name: string } */ sendName(data) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: "name", data })); } else { console.error("Cannot send name: WebSocket not open"); } } /** * Register callback for receiving opponent's name * @param {Function} callback - Callback function to handle received name */ getName(callback) { this.nameCallbacks.push(callback); } /** * Send character slug to opponent * @param {Object} data - Character data { characterSlug: string } */ sendCharacter(data) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: "character", data })); } else { console.error("Cannot send character: WebSocket not open"); } } /** * Register callback for receiving opponent's character slug * @param {Function} callback - Callback function to handle received character slug */ getCharacter(callback) { this.characterCallbacks.push(callback); } /** * Disconnect from the session */ disconnect() { if (this.ws) { this.ws.close(1e3, "Client disconnected"); this.ws = null; } this.started = false; this.moveCallbacks = []; this.nameCallbacks = []; this.characterCallbacks = []; } /** * Get the number of connected peers * Note: Durable Objects don't expose peer count directly * We track this based on peer-joined/peer-left events * @returns {number} */ getPeerCount() { return this.started ? 1 : 0; } }; export { DurableObjectTransport };