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
JavaScript
/**
* 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
};