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