Files
Foundry-VTT-Docker/resources/app/client/data/documents/combat.js

816 lines
29 KiB
JavaScript
Raw Normal View History

2025-01-04 00:34:03 +01:00
/**
* @typedef {Object} CombatHistoryData
* @property {number|null} round
* @property {number|null} turn
* @property {string|null} tokenId
* @property {string|null} combatantId
*/
/**
* The client-side Combat document which extends the common BaseCombat model.
*
* @extends foundry.documents.BaseCombat
* @mixes ClientDocumentMixin
*
* @see {@link Combats} The world-level collection of Combat documents
* @see {@link Combatant} The Combatant embedded document which exists within a Combat document
* @see {@link CombatConfig} The Combat configuration application
*/
class Combat extends ClientDocumentMixin(foundry.documents.BaseCombat) {
/**
* Track the sorted turn order of this combat encounter
* @type {Combatant[]}
*/
turns = this.turns || [];
/**
* Record the current round, turn, and tokenId to understand changes in the encounter state
* @type {CombatHistoryData}
*/
current = this._getCurrentState();
/**
* Track the previous round, turn, and tokenId to understand changes in the encounter state
* @type {CombatHistoryData}
*/
previous = undefined;
/**
* The configuration setting used to record Combat preferences
* @type {string}
*/
static CONFIG_SETTING = "combatTrackerConfig";
/* -------------------------------------------- */
/* Properties */
/* -------------------------------------------- */
/**
* Get the Combatant who has the current turn.
* @type {Combatant}
*/
get combatant() {
return this.turns[this.turn];
}
/* -------------------------------------------- */
/**
* Get the Combatant who has the next turn.
* @type {Combatant}
*/
get nextCombatant() {
if ( this.turn === this.turns.length - 1 ) return this.turns[0];
return this.turns[this.turn + 1];
}
/* -------------------------------------------- */
/**
* Return the object of settings which modify the Combat Tracker behavior
* @type {object}
*/
get settings() {
return CombatEncounters.settings;
}
/* -------------------------------------------- */
/**
* Has this combat encounter been started?
* @type {boolean}
*/
get started() {
return this.round > 0;
}
/* -------------------------------------------- */
/** @inheritdoc */
get visible() {
return true;
}
/* -------------------------------------------- */
/**
* Is this combat active in the current scene?
* @type {boolean}
*/
get isActive() {
if ( !this.scene ) return this.active;
return this.scene.isView && this.active;
}
/* -------------------------------------------- */
/* Methods */
/* -------------------------------------------- */
/**
* Set the current Combat encounter as active within the Scene.
* Deactivate all other Combat encounters within the viewed Scene and set this one as active
* @param {object} [options] Additional context to customize the update workflow
* @returns {Promise<Combat>}
*/
async activate(options) {
const updates = this.collection.reduce((arr, c) => {
if ( c.isActive ) arr.push({_id: c.id, active: false});
return arr;
}, []);
updates.push({_id: this.id, active: true});
return this.constructor.updateDocuments(updates, options);
}
/* -------------------------------------------- */
/** @override */
prepareDerivedData() {
if ( this.combatants.size && !this.turns?.length ) this.setupTurns();
}
/* -------------------------------------------- */
/**
* Get a Combatant using its Token id
* @param {string|TokenDocument} token A Token ID or a TokenDocument instance
* @returns {Combatant[]} An array of Combatants which represent the Token
*/
getCombatantsByToken(token) {
const tokenId = token instanceof TokenDocument ? token.id : token;
return this.combatants.filter(c => c.tokenId === tokenId);
}
/* -------------------------------------------- */
/**
* Get a Combatant that represents the given Actor or Actor ID.
* @param {string|Actor} actor An Actor ID or an Actor instance
* @returns {Combatant[]}
*/
getCombatantsByActor(actor) {
const isActor = actor instanceof Actor;
if ( isActor && actor.isToken ) return this.getCombatantsByToken(actor.token);
const actorId = isActor ? actor.id : actor;
return this.combatants.filter(c => c.actorId === actorId);
}
/* -------------------------------------------- */
/**
* Begin the combat encounter, advancing to round 1 and turn 1
* @returns {Promise<Combat>}
*/
async startCombat() {
this._playCombatSound("startEncounter");
const updateData = {round: 1, turn: 0};
Hooks.callAll("combatStart", this, updateData);
return this.update(updateData);
}
/* -------------------------------------------- */
/**
* Advance the combat to the next round
* @returns {Promise<Combat>}
*/
async nextRound() {
let turn = this.turn === null ? null : 0; // Preserve the fact that it's no-one's turn currently.
if ( this.settings.skipDefeated && (turn !== null) ) {
turn = this.turns.findIndex(t => !t.isDefeated);
if (turn === -1) {
ui.notifications.warn("COMBAT.NoneRemaining", {localize: true});
turn = 0;
}
}
let advanceTime = Math.max(this.turns.length - this.turn, 0) * CONFIG.time.turnTime;
advanceTime += CONFIG.time.roundTime;
let nextRound = this.round + 1;
// Update the document, passing data through a hook first
const updateData = {round: nextRound, turn};
const updateOptions = {direction: 1, worldTime: {delta: advanceTime}};
Hooks.callAll("combatRound", this, updateData, updateOptions);
return this.update(updateData, updateOptions);
}
/* -------------------------------------------- */
/**
* Rewind the combat to the previous round
* @returns {Promise<Combat>}
*/
async previousRound() {
let turn = ( this.round === 0 ) ? 0 : Math.max(this.turns.length - 1, 0);
if ( this.turn === null ) turn = null;
let round = Math.max(this.round - 1, 0);
if ( round === 0 ) turn = null;
let advanceTime = -1 * (this.turn || 0) * CONFIG.time.turnTime;
if ( round > 0 ) advanceTime -= CONFIG.time.roundTime;
// Update the document, passing data through a hook first
const updateData = {round, turn};
const updateOptions = {direction: -1, worldTime: {delta: advanceTime}};
Hooks.callAll("combatRound", this, updateData, updateOptions);
return this.update(updateData, updateOptions);
}
/* -------------------------------------------- */
/**
* Advance the combat to the next turn
* @returns {Promise<Combat>}
*/
async nextTurn() {
let turn = this.turn ?? -1;
let skip = this.settings.skipDefeated;
// Determine the next turn number
let next = null;
if ( skip ) {
for ( let [i, t] of this.turns.entries() ) {
if ( i <= turn ) continue;
if ( t.isDefeated ) continue;
next = i;
break;
}
}
else next = turn + 1;
// Maybe advance to the next round
let round = this.round;
if ( (this.round === 0) || (next === null) || (next >= this.turns.length) ) {
return this.nextRound();
}
// Update the document, passing data through a hook first
const updateData = {round, turn: next};
const updateOptions = {direction: 1, worldTime: {delta: CONFIG.time.turnTime}};
Hooks.callAll("combatTurn", this, updateData, updateOptions);
return this.update(updateData, updateOptions);
}
/* -------------------------------------------- */
/**
* Rewind the combat to the previous turn
* @returns {Promise<Combat>}
*/
async previousTurn() {
if ( (this.turn === 0) && (this.round === 0) ) return this;
else if ( (this.turn <= 0) && (this.turn !== null) ) return this.previousRound();
let previousTurn = (this.turn ?? this.turns.length) - 1;
// Update the document, passing data through a hook first
const updateData = {round: this.round, turn: previousTurn};
const updateOptions = {direction: -1, worldTime: {delta: -1 * CONFIG.time.turnTime}};
Hooks.callAll("combatTurn", this, updateData, updateOptions);
return this.update(updateData, updateOptions);
}
/* -------------------------------------------- */
/**
* Display a dialog querying the GM whether they wish to end the combat encounter and empty the tracker
* @returns {Promise<Combat>}
*/
async endCombat() {
return Dialog.confirm({
title: game.i18n.localize("COMBAT.EndTitle"),
content: `<p>${game.i18n.localize("COMBAT.EndConfirmation")}</p>`,
yes: () => this.delete()
});
}
/* -------------------------------------------- */
/**
* Toggle whether this combat is linked to the scene or globally available.
* @returns {Promise<Combat>}
*/
async toggleSceneLink() {
const scene = this.scene ? null : (game.scenes.current?.id || null);
if ( (scene !== null) && this.combatants.some(c => c.sceneId && (c.sceneId !== scene)) ) {
ui.notifications.error("COMBAT.CannotLinkToScene", {localize: true});
return this;
}
return this.update({scene});
}
/* -------------------------------------------- */
/**
* Reset all combatant initiative scores, setting the turn back to zero
* @returns {Promise<Combat>}
*/
async resetAll() {
for ( let c of this.combatants ) {
c.updateSource({initiative: null});
}
return this.update({turn: this.started ? 0 : null, combatants: this.combatants.toObject()}, {diff: false});
}
/* -------------------------------------------- */
/**
* Roll initiative for one or multiple Combatants within the Combat document
* @param {string|string[]} ids A Combatant id or Array of ids for which to roll
* @param {object} [options={}] Additional options which modify how initiative rolls are created or presented.
* @param {string|null} [options.formula] A non-default initiative formula to roll. Otherwise, the system
* default is used.
* @param {boolean} [options.updateTurn=true] Update the Combat turn after adding new initiative scores to
* keep the turn on the same Combatant.
* @param {object} [options.messageOptions={}] Additional options with which to customize created Chat Messages
* @returns {Promise<Combat>} A promise which resolves to the updated Combat document once updates are complete.
*/
async rollInitiative(ids, {formula=null, updateTurn=true, messageOptions={}}={}) {
// Structure input data
ids = typeof ids === "string" ? [ids] : ids;
const currentId = this.combatant?.id;
const chatRollMode = game.settings.get("core", "rollMode");
// Iterate over Combatants, performing an initiative roll for each
const updates = [];
const messages = [];
for ( let [i, id] of ids.entries() ) {
// Get Combatant data (non-strictly)
const combatant = this.combatants.get(id);
if ( !combatant?.isOwner ) continue;
// Produce an initiative roll for the Combatant
const roll = combatant.getInitiativeRoll(formula);
await roll.evaluate();
updates.push({_id: id, initiative: roll.total});
// Construct chat message data
let messageData = foundry.utils.mergeObject({
speaker: ChatMessage.getSpeaker({
actor: combatant.actor,
token: combatant.token,
alias: combatant.name
}),
flavor: game.i18n.format("COMBAT.RollsInitiative", {name: combatant.name}),
flags: {"core.initiativeRoll": true}
}, messageOptions);
const chatData = await roll.toMessage(messageData, {create: false});
// If the combatant is hidden, use a private roll unless an alternative rollMode was explicitly requested
chatData.rollMode = "rollMode" in messageOptions ? messageOptions.rollMode
: (combatant.hidden ? CONST.DICE_ROLL_MODES.PRIVATE : chatRollMode );
// Play 1 sound for the whole rolled set
if ( i > 0 ) chatData.sound = null;
messages.push(chatData);
}
if ( !updates.length ) return this;
// Update multiple combatants
await this.updateEmbeddedDocuments("Combatant", updates);
// Ensure the turn order remains with the same combatant
if ( updateTurn && currentId ) {
await this.update({turn: this.turns.findIndex(t => t.id === currentId)});
}
// Create multiple chat messages
await ChatMessage.implementation.create(messages);
return this;
}
/* -------------------------------------------- */
/**
* Roll initiative for all combatants which have not already rolled
* @param {object} [options={}] Additional options forwarded to the Combat.rollInitiative method
*/
async rollAll(options) {
const ids = this.combatants.reduce((ids, c) => {
if ( c.isOwner && (c.initiative === null) ) ids.push(c.id);
return ids;
}, []);
return this.rollInitiative(ids, options);
}
/* -------------------------------------------- */
/**
* Roll initiative for all non-player actors who have not already rolled
* @param {object} [options={}] Additional options forwarded to the Combat.rollInitiative method
*/
async rollNPC(options={}) {
const ids = this.combatants.reduce((ids, c) => {
if ( c.isOwner && c.isNPC && (c.initiative === null) ) ids.push(c.id);
return ids;
}, []);
return this.rollInitiative(ids, options);
}
/* -------------------------------------------- */
/**
* Assign initiative for a single Combatant within the Combat encounter.
* Update the Combat turn order to maintain the same combatant as the current turn.
* @param {string} id The combatant ID for which to set initiative
* @param {number} value A specific initiative value to set
*/
async setInitiative(id, value) {
const combatant = this.combatants.get(id, {strict: true});
await combatant.update({initiative: value});
}
/* -------------------------------------------- */
/**
* Return the Array of combatants sorted into initiative order, breaking ties alphabetically by name.
* @returns {Combatant[]}
*/
setupTurns() {
this.turns ||= [];
// Determine the turn order and the current turn
const turns = this.combatants.contents.sort(this._sortCombatants);
if ( this.turn !== null) this.turn = Math.clamp(this.turn, 0, turns.length-1);
// Update state tracking
let c = turns[this.turn];
this.current = this._getCurrentState(c);
// One-time initialization of the previous state
if ( !this.previous ) this.previous = this.current;
// Return the array of prepared turns
return this.turns = turns;
}
/* -------------------------------------------- */
/**
* Debounce changes to the composition of the Combat encounter to de-duplicate multiple concurrent Combatant changes.
* If this is the currently viewed encounter, re-render the CombatTracker application.
* @type {Function}
*/
debounceSetup = foundry.utils.debounce(() => {
this.current.round = this.round;
this.current.turn = this.turn;
this.setupTurns();
if ( ui.combat.viewed === this ) ui.combat.render();
}, 50);
/* -------------------------------------------- */
/**
* Update active effect durations for all actors present in this Combat encounter.
*/
updateCombatantActors() {
for ( const combatant of this.combatants ) combatant.actor?.render(false, {renderContext: "updateCombat"});
}
/* -------------------------------------------- */
/**
* Loads the registered Combat Theme (if any) and plays the requested type of sound.
* If multiple exist for that type, one is chosen at random.
* @param {string} announcement The announcement that should be played: "startEncounter", "nextUp", or "yourTurn".
* @protected
*/
_playCombatSound(announcement) {
if ( !CONST.COMBAT_ANNOUNCEMENTS.includes(announcement) ) {
throw new Error(`"${announcement}" is not a valid Combat announcement type`);
}
const theme = CONFIG.Combat.sounds[game.settings.get("core", "combatTheme")];
if ( !theme || theme === "none" ) return;
const sounds = theme[announcement];
if ( !sounds ) return;
const src = sounds[Math.floor(Math.random() * sounds.length)];
game.audio.play(src, {context: game.audio.interface});
}
/* -------------------------------------------- */
/**
* Define how the array of Combatants is sorted in the displayed list of the tracker.
* This method can be overridden by a system or module which needs to display combatants in an alternative order.
* The default sorting rules sort in descending order of initiative using combatant IDs for tiebreakers.
* @param {Combatant} a Some combatant
* @param {Combatant} b Some other combatant
* @protected
*/
_sortCombatants(a, b) {
const ia = Number.isNumeric(a.initiative) ? a.initiative : -Infinity;
const ib = Number.isNumeric(b.initiative) ? b.initiative : -Infinity;
return (ib - ia) || (a.id > b.id ? 1 : -1);
}
/* -------------------------------------------- */
/**
* Refresh the Token HUD under certain circumstances.
* @param {Combatant[]} documents A list of Combatant documents that were added or removed.
* @protected
*/
_refreshTokenHUD(documents) {
if ( documents.some(doc => doc.token?.object?.hasActiveHUD) ) canvas.tokens.hud.render();
}
/* -------------------------------------------- */
/* Event Handlers */
/* -------------------------------------------- */
/** @inheritDoc */
_onCreate(data, options, userId) {
super._onCreate(data, options, userId);
if ( !this.collection.viewed && this.collection.combats.includes(this) ) {
ui.combat.initialize({combat: this, render: false});
}
this._manageTurnEvents();
}
/* -------------------------------------------- */
/** @inheritDoc */
_onUpdate(changed, options, userId) {
super._onUpdate(changed, options, userId);
const priorState = foundry.utils.deepClone(this.current);
if ( !this.previous ) this.previous = priorState; // Just in case
// Determine the new turn order
if ( "combatants" in changed ) this.setupTurns(); // Update all combatants
else this.current = this._getCurrentState(); // Update turn or round
// Record the prior state and manage turn events
const stateChanged = this.#recordPreviousState(priorState);
if ( stateChanged && (options.turnEvents !== false) ) this._manageTurnEvents();
// Render applications for Actors involved in the Combat
this.updateCombatantActors();
// Render the CombatTracker sidebar
if ( (changed.active === true) && this.isActive ) ui.combat.initialize({combat: this});
else if ( "scene" in changed ) ui.combat.initialize();
// Trigger combat sound cues in the active encounter
if ( this.active && this.started && priorState.round ) {
const play = c => c && (game.user.isGM ? !c.hasPlayerOwner : c.isOwner);
if ( play(this.combatant) ) this._playCombatSound("yourTurn");
else if ( play(this.nextCombatant) ) this._playCombatSound("nextUp");
}
}
/* -------------------------------------------- */
/** @inheritDoc */
_onDelete(options, userId) {
super._onDelete(options, userId);
if ( this.collection.viewed === this ) ui.combat.initialize();
if ( userId === game.userId ) this.collection.viewed?.activate();
}
/* -------------------------------------------- */
/* Combatant Management Workflows */
/* -------------------------------------------- */
/** @inheritDoc */
_onCreateDescendantDocuments(parent, collection, documents, data, options, userId) {
super._onCreateDescendantDocuments(parent, collection, documents, data, options, userId);
this.#onModifyCombatants(parent, documents, options);
}
/* -------------------------------------------- */
/** @inheritDoc */
_onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId) {
super._onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId);
this.#onModifyCombatants(parent, documents, options);
}
/* -------------------------------------------- */
/** @inheritDoc */
_onDeleteDescendantDocuments(parent, collection, documents, ids, options, userId) {
super._onDeleteDescendantDocuments(parent, collection, documents, ids, options, userId);
this.#onModifyCombatants(parent, documents, options);
}
/* -------------------------------------------- */
/**
* Shared actions taken when Combatants are modified within this Combat document.
* @param {Document} parent The direct parent of the created Documents, may be this Document or a child
* @param {Document[]} documents The array of created Documents
* @param {object} options Options which modified the operation
*/
#onModifyCombatants(parent, documents, options) {
const {combatTurn, turnEvents, render} = options;
if ( parent === this ) this._refreshTokenHUD(documents);
const priorState = foundry.utils.deepClone(this.current);
if ( typeof combatTurn === "number" ) this.updateSource({turn: combatTurn});
this.setupTurns();
const turnChange = this.#recordPreviousState(priorState);
if ( turnChange && (turnEvents !== false) ) this._manageTurnEvents();
if ( (ui.combat.viewed === parent) && (render !== false) ) ui.combat.render();
}
/* -------------------------------------------- */
/**
* Get the current history state of the Combat encounter.
* @param {Combatant} [combatant] The new active combatant
* @returns {CombatHistoryData}
* @protected
*/
_getCurrentState(combatant) {
combatant ||= this.combatant;
return {
round: this.round,
turn: this.turn ?? null,
combatantId: combatant?.id || null,
tokenId: combatant?.tokenId || null
};
}
/* -------------------------------------------- */
/**
* Update the previous turn data.
* Compare the state with the new current state. Only update the previous state if there is a difference.
* @param {CombatHistoryData} priorState A cloned copy of the current history state before changes
* @returns {boolean} Has the combat round or current combatant changed?
*/
#recordPreviousState(priorState) {
const {round, combatantId} = this.current;
const turnChange = (combatantId !== priorState.combatantId) || (round !== priorState.round);
Object.assign(this.previous, priorState);
return turnChange;
}
/* -------------------------------------------- */
/* Turn Events */
/* -------------------------------------------- */
/**
* Manage the execution of Combat lifecycle events.
* This method orchestrates the execution of four events in the following order, as applicable:
* 1. End Turn
* 2. End Round
* 3. Begin Round
* 4. Begin Turn
* Each lifecycle event is an async method, and each is awaited before proceeding.
* @returns {Promise<void>}
* @protected
*/
async _manageTurnEvents() {
if ( !this.started ) return;
// Gamemaster handling only
if ( game.users.activeGM?.isSelf ) {
const advanceRound = this.current.round > (this.previous.round ?? -1);
const advanceTurn = advanceRound || (this.current.turn > (this.previous.turn ?? -1));
const changeCombatant = this.current.combatantId !== this.previous.combatantId;
if ( !(advanceTurn || advanceRound || changeCombatant) ) return;
// Conclude the prior Combatant turn
const prior = this.combatants.get(this.previous.combatantId);
if ( (advanceTurn || changeCombatant) && prior ) await this._onEndTurn(prior);
// Conclude the prior round
if ( advanceRound && this.previous.round ) await this._onEndRound();
// Begin the new round
if ( advanceRound ) await this._onStartRound();
// Begin a new Combatant turn
const next = this.combatant;
if ( (advanceTurn || changeCombatant) && next ) await this._onStartTurn(this.combatant);
}
// Hooks handled by all clients
Hooks.callAll("combatTurnChange", this, this.previous, this.current);
}
/* -------------------------------------------- */
/**
* A workflow that occurs at the end of each Combat Turn.
* This workflow occurs after the Combat document update, prior round information exists in this.previous.
* This can be overridden to implement system-specific combat tracking behaviors.
* This method only executes for one designated GM user. If no GM users are present this method will not be called.
* @param {Combatant} combatant The Combatant whose turn just ended
* @returns {Promise<void>}
* @protected
*/
async _onEndTurn(combatant) {
if ( CONFIG.debug.combat ) {
console.debug(`${vtt} | Combat End Turn: ${this.combatants.get(this.previous.combatantId).name}`);
}
// noinspection ES6MissingAwait
this.#triggerRegionEvents(CONST.REGION_EVENTS.TOKEN_TURN_END, [combatant]);
}
/* -------------------------------------------- */
/**
* A workflow that occurs at the end of each Combat Round.
* This workflow occurs after the Combat document update, prior round information exists in this.previous.
* This can be overridden to implement system-specific combat tracking behaviors.
* This method only executes for one designated GM user. If no GM users are present this method will not be called.
* @returns {Promise<void>}
* @protected
*/
async _onEndRound() {
if ( CONFIG.debug.combat ) console.debug(`${vtt} | Combat End Round: ${this.previous.round}`);
// noinspection ES6MissingAwait
this.#triggerRegionEvents(CONST.REGION_EVENTS.TOKEN_ROUND_END, this.combatants);
}
/* -------------------------------------------- */
/**
* A workflow that occurs at the start of each Combat Round.
* This workflow occurs after the Combat document update, new round information exists in this.current.
* This can be overridden to implement system-specific combat tracking behaviors.
* This method only executes for one designated GM user. If no GM users are present this method will not be called.
* @returns {Promise<void>}
* @protected
*/
async _onStartRound() {
if ( CONFIG.debug.combat ) console.debug(`${vtt} | Combat Start Round: ${this.round}`);
// noinspection ES6MissingAwait
this.#triggerRegionEvents(CONST.REGION_EVENTS.TOKEN_ROUND_START, this.combatants);
}
/* -------------------------------------------- */
/**
* A workflow that occurs at the start of each Combat Turn.
* This workflow occurs after the Combat document update, new turn information exists in this.current.
* This can be overridden to implement system-specific combat tracking behaviors.
* This method only executes for one designated GM user. If no GM users are present this method will not be called.
* @param {Combatant} combatant The Combatant whose turn just started
* @returns {Promise<void>}
* @protected
*/
async _onStartTurn(combatant) {
if ( CONFIG.debug.combat ) console.debug(`${vtt} | Combat Start Turn: ${this.combatant.name}`);
// noinspection ES6MissingAwait
this.#triggerRegionEvents(CONST.REGION_EVENTS.TOKEN_TURN_START, [combatant]);
}
/* -------------------------------------------- */
/**
* Trigger Region events for Combat events.
* @param {string} eventName The event name
* @param {Iterable<Combatant>} combatants The combatants to trigger the event for
* @returns {Promise<void>}
*/
async #triggerRegionEvents(eventName, combatants) {
const promises = [];
for ( const combatant of combatants ) {
const token = combatant.token;
if ( !token ) continue;
for ( const region of token.regions ) {
promises.push(region._triggerEvent(eventName, {token, combatant}));
}
}
await Promise.allSettled(promises);
}
/* -------------------------------------------- */
/* Deprecations and Compatibility */
/* -------------------------------------------- */
/**
* @deprecated since v11
* @ignore
*/
updateEffectDurations() {
const msg = "Combat#updateEffectDurations is renamed to Combat#updateCombatantActors";
foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
return this.updateCombatantActors();
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
getCombatantByActor(actor) {
const combatants = this.getCombatantsByActor(actor);
return combatants?.[0] || null;
}
/* -------------------------------------------- */
/**
* @deprecated since v12
* @ignore
*/
getCombatantByToken(token) {
const combatants = this.getCombatantsByToken(token);
return combatants?.[0] || null;
}
}