tickwerk/src/data/documents/combatant.js

203 lines
6.9 KiB
JavaScript

// SPDX-FileCopyrightText: 2022 Johannes Loher
//
// SPDX-License-Identifier: MIT
import { packageId } from '../../constants';
import { getGame } from '../../helpers';
export const registerCombatantFunctionality = () => {
CONFIG.Combatant.documentClass = CombatantMixin(CONFIG.Combatant.documentClass);
};
/**
* Enhance a co,batant class with functionality for the Tickwerk.
* @param {typeof Combatant} BaseCombatant The combat class to enhance
* @returns A combat class, adapted for the Tickwerk
*/
const CombatantMixin = (BaseCombatant) => {
return class TickwerkCombatant extends BaseCombatant {
/**
* An temporary property to make changes to the initiative available to other instances in their `_pre…` methods.
* @type {number|null|undefined}
*/
_newInitiative;
/**
* An temporary property to make changes to the tiebreaker available to other instances in their `_pre…` methods.
* @type {number|undefined}
*/
_newTiebreaker;
/***
* Is this combatant currently waiting?
* @type {boolean}
*/
get waiting() {
return this.getFlag(packageId, 'waiting') ?? false;
}
/**
* Toggle the waiting state of this combatant.
* @returns {Promise<this|undefined>} The updated combatant
*/
toggleWaiting() {
const update = { [`flags.${packageId}.waiting`]: !this.waiting };
if (this.parent?.started && this.waiting) update.initiative = this.parent?.tickValue;
return this.update(update);
}
/**
* Advance for the given number of ticks.
* @param {number} ticks The number of ticks to advance for
* @returns {Promise<void>} A promise that resolves once when the combatant has advanced
*/
async advanceTicks(ticks) {
if (this.initiative === null) {
ui.notifications?.warn('TICKWERK.WarningCannotAdvanceWithoutTickValue', { localize: true });
return;
}
if (this.waiting) {
ui.notifications?.warn('TICKWERK.WarningCannotAdvanceWhileWaiting', { localize: true });
return;
}
if (!this.combat?.started) {
ui.notifications?.warn('TICKWERK.WarningCannotAdvanceWithoutStartedCombat', { localize: true });
return;
}
await this.update({ initiative: this.initiative + ticks });
const tickTime = CONFIG.tickwerk?.tickTime;
const advanceTime = tickTime !== undefined ? ticks * tickTime : undefined;
if (advanceTime !== 0) {
await this.combat?.update(undefined, { diff: false, advanceTime });
}
}
/**
* Show a dialog for advancing the combatant a certain number of ticks.
* @returns {Promise<void>} A promise that resolves when the dialog has been confirmed and the combatant has advanced
*/
async advanceTicksDialog() {
const game = getGame();
const id = foundry.utils.randomID();
const form = `<form><div class="form-group">
<label for="ticks-${id}">${game.i18n.localize('TICKWERK.NumberOfTicks')}</label>
<input id="ticks-${id}" name="ticks" type="number" value="5" required />
</div></form>`;
const ticks = await Dialog.confirm({
title: game.i18n.localize('TICKWERK.AdvanceTicks'),
content: form,
yes: (html) => {
const ticks = html[0]?.querySelector('input[name="ticks"]')?.value;
const parsedTicks = ticks !== undefined ? parseInt(ticks) : undefined;
return Number.isSafeInteger(parsedTicks) ? parsedTicks : NaN;
},
rejectClose: false,
});
if (Number.isNaN(ticks)) {
ui.notifications?.warn('TICKWERK.WarningInvalidNumberOfTicks', { localize: true });
return;
}
if (ticks !== null && ticks !== false) {
await this.advanceTicks(ticks);
}
}
/**
* Update tiebreaker data for a given creation or update.
* @param {object} data The data of the creation / update
*/
async #updateTiebreakerData(data) {
const waiting = data.flags?.[packageId]?.waiting;
if ('initiative' in data || waiting !== undefined) {
const newInitiative = data.initiative ?? this.initiative;
const combatantsWithSameTickValue =
this.parent?.combatants.filter((combatant) => {
const otherInitiative =
combatant._newInitiative !== undefined ? combatant._newInitiative : combatant.initiative;
return otherInitiative === newInitiative && combatant !== this;
}) ?? [];
const tiebreaker = await this.#getTiebreaker(combatantsWithSameTickValue, waiting);
foundry.utils.setProperty(data, `flags.${packageId}.tiebreaker`, tiebreaker);
this._newInitiative = data.initiative;
this._newTiebreaker = tiebreaker;
}
}
/**
* Get a tiebreaker between this combatant and the given other combatants.
* @param {TickwerkCombatant[]} combatants The other combatants among which to find a tiebreaker
* @param {boolean | undefined} waiting The change of the waiting state of the combatanmt
* @returns {Promise<number>} A promise that resolves to the tiebreaker
*/
async #getTiebreaker(combatants, waiting) {
const getTiebreaker = CONFIG.tickwerk?.getTiebreaker ?? defaultGetTiebreaker;
return getTiebreaker(this, combatants, waiting);
}
/** @override */
testUserPermission(user, permission, { exact } = {}) {
if (user.isGM) return true;
return super.testUserPermission(user, permission, { exact });
}
/** @override */
_getInitiativeFormula() {
const getInitiativeFormula = CONFIG.tickwerk?.getInitiativeFormula;
if (getInitiativeFormula) return getInitiativeFormula(this);
return super._getInitiativeFormula();
}
/** @override */
async _preCreate(...args) {
await super._preCreate(...args);
await this.#updateTiebreakerData(args[0]);
}
/** @override */
async _preUpdate(...args) {
await super._preUpdate(...args);
await this.#updateTiebreakerData(args[0]);
}
/** @override */
_onCreate(...args) {
super._onCreate(...args);
this._newInitiative = undefined;
this._newTiebreaker = undefined;
}
/** @override */
_onUpdate(...args) {
super._onUpdate(...args);
this._newInitiative = undefined;
this._newTiebreaker = undefined;
}
};
};
/**
* A function to get a tiebreaker for a combatant
* @typedef {(combatant: TickwerkCombatant, combatants: TickwerkCombatant[], waiting: boolean | undefined) => Promise<number>} GetTiebreaker
*/
/**
* Default implementation to get a tiebreaker for a combatant.
* @type {GetTiebreaker}
*/
export const defaultGetTiebreaker = async (combatant, combatants) => {
if (combatants.length === 0) return 0;
const tiebreakers = combatants.map((combatant) => {
return (
(combatant._newTiebreaker !== undefined
? combatant._newTiebreaker
: combatant.getFlag(packageId, 'tiebreaker')) ?? 0
);
});
return Math.max(...tiebreakers) + 1;
};