203 lines
6.9 KiB
JavaScript
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;
|
|
};
|