UNPKG

swordfight-engine

Version:

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

471 lines (466 loc) 15.2 kB
/** * swordfight-engine v1.6.21 * @license MIT */ var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/classes/Moves.js var Moves = class { constructor(character, result = []) { /** * getMoveObject */ __publicField(this, "getMoveObject", (id) => { return this.moves.find((move) => move.id === id); }); this.character = character; this.result = result; this.moves = this.character.moves; this.filteredMoves = this.filteredMoves(); } /** * @method getMoves * * @description * Get the moves */ getMoves() { return this.character.moves; } /** * filteredMoves */ filteredMoves() { return this.moves.filter((move) => { if (this.result.allowOnly && !this.result.allowOnly.includes(move.name) && !this.result.allowOnly.includes(move.tag)) { return false; } if (move.requiresWeapon) { const availableWeapons = this.character.weapons || []; if (typeof move.requiresWeapon === "string") { const requiredWeapon = availableWeapons.find((w) => w.name === move.requiresWeapon); if (!requiredWeapon) { return false; } if (move.requiresAmmo) { const hasAmmo = requiredWeapon.ammo === null || requiredWeapon.ammo > 0; if (!hasAmmo) { return false; } } } else { if (availableWeapons.length === 0) { return false; } } } if (!this.character.shield && move.requiresShield) { return false; } const hasWeapons = this.character.weapons && this.character.weapons.length > 0; const hasDroppedWeapons = this.character.droppedWeapons && this.character.droppedWeapons.length > 0; if (move.name === "Retrieve Weapon" && (hasWeapons || !hasDroppedWeapons || this.character.weaponDestroyed)) { return false; } if (move.range !== this.result.range) { return false; } if (this.result.restrict.includes(move.type)) { return false; } if (this.result.restrict.includes(move.tag)) { return false; } if (this.result.restrict.includes(move.name)) { return false; } return true; }); } }; // src/classes/BonusCalculator.js var BonusCalculator = class { /** * Calculate the bonus to apply to a move based on previous round bonus data * @param {object} move - The current move being made * @param {string} move.type - The move type (e.g., 'strong', 'defensive') * @param {string} move.tag - The move tag (e.g., 'Down Swing', 'Thrust') * @param {string} move.name - The move name (e.g., 'Smash', 'Block') * @param {array} previousRoundBonus - The bonus array from the previous round's result * @returns {number} The bonus value to apply to this move * * @example * // Previous round resulted in a bonus for 'strong' moves * const previousBonus = [{ strong: 2 }]; * const move = { type: 'strong', tag: 'Down Swing', name: 'Smash' }; * const bonus = BonusCalculator.calculateBonus(move, previousBonus); * // Returns: 2 * * @example * // Previous round resulted in multiple bonuses * const previousBonus = [{ strong: 2, 'Down Swing': 1 }]; * const move = { type: 'strong', tag: 'Down Swing', name: 'Smash' }; * const bonus = BonusCalculator.calculateBonus(move, previousBonus); * // Returns: 3 (2 from type + 1 from tag) * * @example * // No bonus applies * const previousBonus = [{ defensive: 1 }]; * const move = { type: 'strong', tag: 'Down Swing', name: 'Smash' }; * const bonus = BonusCalculator.calculateBonus(move, previousBonus); * // Returns: 0 */ static calculateBonus(move, previousRoundBonus) { let bonus = 0; if (!previousRoundBonus || !Array.isArray(previousRoundBonus) || !previousRoundBonus.length) { return 0; } if (!move || typeof move !== "object") { return 0; } previousRoundBonus.forEach((bonusObject) => { if (!bonusObject || typeof bonusObject !== "object") { return; } for (const key in bonusObject) { if (move.type === key || move.tag === key || move.name === key) { const bonusValue = parseFloat(bonusObject[key]); if (!isNaN(bonusValue)) { bonus += bonusValue; } } } }); return bonus; } /** * Get the next round bonus that will be provided by a result * @param {object} result - The round result object * @param {array} [result.bonus] - Optional bonus array to apply next round * @returns {array|number} The bonus array for next round, or 0 if none * bonus += bonusValue; } * const result = { bonus: [{ strong: 2 }] }; * const nextBonus = BonusCalculator.getNextRoundBonus(result); * // Returns: [{ strong: 2 }] * * @example * const result = { score: 5 }; // No bonus property * const nextBonus = BonusCalculator.getNextRoundBonus(result); * // Returns: 0 */ static getNextRoundBonus(result) { if (!result || !result.bonus) { return []; } return Array.isArray(result.bonus) ? result.bonus : [result.bonus]; } /** * Calculate total score including bonus * @param {number|string} baseScore - The base score from hitting * @param {number|string} moveModifier - The move's modifier * @param {number|string} bonus - The bonus to apply * @returns {number} The total score (minimum 0) * * @example * const total = BonusCalculator.calculateTotalScore(5, 2, 3); * // Returns: 10 * * @example * // Negative scores become 0 * const total = BonusCalculator.calculateTotalScore(1, -3, 0); * // Returns: 0 */ static calculateTotalScore(baseScore, moveModifier, bonus) { const score = parseFloat(baseScore); const modifier = parseFloat(moveModifier); const bonusValue = parseFloat(bonus); if (isNaN(score)) { return 0; } const mod = isNaN(modifier) ? 0 : modifier; const bon = isNaN(bonusValue) ? 0 : bonusValue; let totalScore = score + mod + bon; if (totalScore < 0) { totalScore = 0; } return totalScore; } }; // 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/ComputerTransport.js var ComputerTransport = class extends MultiplayerTransport { constructor(game, options = {}) { super(game); this.startDelay = options.startDelay || 3e3; this.moveCallbacks = []; this.nameCallbacks = []; this.selectedOpponentSlug = null; this.gameEnded = false; this.pendingMoveTimeout = null; this.preparingForRound = null; if (typeof document !== "undefined") { document.addEventListener("victory", () => { this._stopPreparing(); }); document.addEventListener("defeat", () => { this._stopPreparing(); }); document.addEventListener("setup", () => { if (!this.gameEnded && this.game.opponentsCharacter && this.game.opponentsCharacter.moves) { this._prepareMove(); } }); } } /** * Connect to the "computer opponent session" * @param {string} _roomId - Not used for computer opponent * @returns {Promise<void>} */ async connect(_roomId) { const availableCharacters = await this.game.CharacterLoader.getAvailableCharacters(); const computerCharacters = availableCharacters.filter((slug) => !slug.includes("fighter") && slug !== "human-monk"); this.selectedOpponentSlug = computerCharacters[Math.floor(Math.random() * computerCharacters.length)]; return new Promise((resolve) => { setTimeout(() => { this.started = true; if (typeof document !== "undefined") { const startEvent = new CustomEvent("start", { detail: { game: this.game } }); document.dispatchEvent(startEvent); } resolve(); setTimeout(() => { this._triggerNameCallbacks(); setTimeout(() => { if (this.game.opponentsCharacter && this.game.opponentsCharacter.moves) { this._prepareMove(); } }, 100); }, 0); }, this.startDelay); }); } /** * Trigger all registered name callbacks with opponent data * @private */ _triggerNameCallbacks() { const data = { name: "Computer", characterSlug: this.selectedOpponentSlug }; this.nameCallbacks.forEach((callback) => { try { callback(data); } catch (error) { console.error("Error in name callback:", error); } }); } /** * Register callback for receiving opponent's move * @param {Function} callback - Callback function to handle opponent's move */ getMove(callback) { if (!this.moveCallbacks.includes(callback)) { this.moveCallbacks.push(callback); } } /** * Register callback for receiving opponent's name * @param {Function} callback - Callback function to handle received name */ getName(callback) { if (!this.nameCallbacks.includes(callback)) { this.nameCallbacks.push(callback); } } /** * Prepare and deliver computer's move after thinking delay * Called automatically when a new round starts * @private */ _prepareMove() { const currentRound = this.game.roundNumber; if (this.preparingForRound === currentRound) { return; } this.preparingForRound = currentRound; if (window.logging) { console.log(`Computer preparing move for round ${currentRound}`); } const thinkingTime = currentRound === 0 ? 0 : 3e3 + Math.floor(Math.random() * 6e3); this.pendingMoveTimeout = setTimeout(() => { this.pendingMoveTimeout = null; const previousRound = this.game.roundNumber > 0 ? this.game.rounds[this.game.roundNumber - 1] : null; const result = previousRound?.opponentsRoundData?.result || { range: this.game.opponentsCharacter.moves[0].range, restrict: [], allowOnly: null }; const moves = new Moves(this.game.opponentsCharacter, result); let move = moves.filteredMoves[Math.floor(Math.random() * moves.filteredMoves.length)]; if (this.game.opponentsCharacter.droppedWeapons && this.game.opponentsCharacter.droppedWeapons.length > 0 && Math.random() < 0.333) { const retrieveMove = moves.filteredMoves.find((mv) => mv.name === "Retrieve Weapon"); if (retrieveMove) { move = retrieveMove; } } const previousRoundData = this.game.roundNumber > 0 ? this.game.rounds[this.game.roundNumber - 1] : null; const opponentPreviousBonus = previousRoundData?.opponentsRoundData?.nextRoundBonus || []; const bonusMoves = moves.filteredMoves.filter((mv) => { const bonus = BonusCalculator.calculateBonus(mv, opponentPreviousBonus); return bonus > 0; }); if (bonusMoves.length > 0 && Math.random() < 0.333) { move = bonusMoves[Math.floor(Math.random() * bonusMoves.length)]; } if (this.gameEnded) { return; } const data = { move }; this.moveCallbacks.forEach((callback) => { try { callback(data); } catch (error) { console.error("Error in move callback:", error); } }); }, thinkingTime); } /** * Send player's move (computer is already thinking independently) * @param {Object} _data - Move data { move: Object, round: number } */ sendMove(_data) { } /** * sendName * Send the name (not used in single-player, but included for consistency) */ sendName(name) { return name; } /** * sendCharacter * Send the character slug (not used in single-player, but included for consistency) */ sendCharacter(data) { return data; } /** * getCharacter * Get the character slug (not used in single-player, but included for consistency) */ getCharacter(callback) { callback({ characterSlug: this.selectedOpponentSlug }); } /** * Get the number of connected peers * @returns {number} Always returns 1 for computer opponent */ getPeerCount() { return this.started ? 1 : 0; } /** * Stop preparing moves and cancel any pending timeouts * @private */ _stopPreparing() { this.gameEnded = true; if (this.pendingMoveTimeout) { clearTimeout(this.pendingMoveTimeout); this.pendingMoveTimeout = null; } } /** * Disconnect from the computer opponent session */ disconnect() { this.started = false; this._stopPreparing(); this.moveCallbacks = []; this.nameCallbacks = []; } }; export { ComputerTransport };