diff --git a/daggerheart.d.ts b/daggerheart.d.ts index 1641faa8..891a3a2a 100644 --- a/daggerheart.d.ts +++ b/daggerheart.d.ts @@ -4,6 +4,15 @@ import '@common/primitives/global.mjs'; import Canvas from '@client/canvas/board.mjs'; import { ResourceUpdateMap } from './module/data/action/baseAction.mjs'; +import * as applications from './module/applications/_module.mjs'; +import * as data from './module/data/_module.mjs'; +import * as models from './module/data/_module.mjs'; +import * as documents from './module/documents/_module.mjs'; +import { macros } from './module/_module.mjs'; +import * as dice from './module/dice/_module.mjs'; +import * as fields from './module/data/fields/_module.mjs'; + + // Foundry's use of `Object.assign(globalThis) means many globally available objects are not read as such // This declare global hopefully fixes that // Note: eslint is not aware of these, whatever is added here should go in the eslint's globals list @@ -80,3 +89,17 @@ declare global { damageOptions: object; } } + +declare module '@client/packages/system.mjs' { + export default interface System { + api: { + applications: typeof applications, + data: typeof data, + models: typeof models, + documents: typeof documents, + macros: typeof macros, + dice: typeof dice, + fields: typeof fields + }; + } +} diff --git a/lang/en.json b/lang/en.json index 0180b085..b9a81b29 100755 --- a/lang/en.json +++ b/lang/en.json @@ -84,6 +84,7 @@ "transformActorMissing": "The assigned actor to transform into does not exist. It was probably deleted or moved in/out of a compendium", "canvasError": "There is no active scene.", "prototypeError": "You can only use a transform action from a Token", + "linkedSelectedError": "To transform a linked actor there either needs to be only a single token of it on the canvas, or you need to left-click select only one of them.", "actorLinkError": "You cannot transform a token with Actor Link set to true" } }, diff --git a/module/applications/ui/effectsDisplay.mjs b/module/applications/ui/effectsDisplay.mjs index a64b1b22..c9739f56 100644 --- a/module/applications/ui/effectsDisplay.mjs +++ b/module/applications/ui/effectsDisplay.mjs @@ -39,6 +39,12 @@ export default class DhEffectsDisplay extends HandlebarsApplicationMixin(Applica } }; + /** + * Debounce and slightly delayed request to re-render this panel. Necessary for situations where it is not possible + * to properly wait for promises to resolve before refreshing the UI. + */ + refresh = foundry.utils.debounce(this.render.bind(this), 50); + get element() { return document.body.querySelector('.daggerheart.dh-style.effects-display'); } diff --git a/module/data/action/baseAction.mjs b/module/data/action/baseAction.mjs index 58be672b..be7224cd 100644 --- a/module/data/action/baseAction.mjs +++ b/module/data/action/baseAction.mjs @@ -114,7 +114,10 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel return this._id; } - /** Returns true if the current user is the owner of the containing item */ + /** + * Returns true if the current user is the owner of the containing item. + * @returns {boolean} + */ get isOwner() { return this.item?.isOwner ?? true; } @@ -143,6 +146,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel /** * Return the first Actor parent found. + * @returns {DhpActor | null} */ get actor() { return this.item instanceof DhpActor @@ -155,6 +159,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel /** * Returns true if the action is usable. * An action is usable on any actor type. For example, an adversary might have a base attack action. + * @returns {boolean} */ get usable() { const actor = this.actor; diff --git a/module/data/fields/action/transformField.mjs b/module/data/fields/action/transformField.mjs index 07b32166..0fc35742 100644 --- a/module/data/fields/action/transformField.mjs +++ b/module/data/fields/action/transformField.mjs @@ -1,5 +1,11 @@ +import { getWorldActor } from '../../../helpers/utils.mjs'; + const fields = foundry.data.fields; +/** + * @import DHSummonAction from '../../action/summonAction.mjs' + */ + export default class DHSummonField extends fields.SchemaField { /** * Action Workflow order @@ -20,6 +26,11 @@ export default class DHSummonField extends fields.SchemaField { super(transformFields, options, context); } + /** + * Runs the execute. This is run on behalf of DHSummonAction. + * @todo move this function to be on the summon action. + * @this DHSummonAction + */ static async execute() { if (!this.transform.actorUUID) { ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.noTransformActor')); @@ -37,26 +48,37 @@ export default class DHSummonField extends fields.SchemaField { return false; } - if (this.actor.prototypeToken.actorLink) { - ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.actorLinkError')); + const activeTokens = this.actor.getActiveTokens(false, true); + const controlledMatchingTokens = canvas.tokens.controlled + .filter(x => x.actor && x.actor.uuid === this.actor.uuid) + .map(x => x.document); + /** @type {typeof game.system.api.documents.DhToken | null} */ + const token = this.actor.token ?? ( + activeTokens.length === 1 ? activeTokens[0] : + (controlledMatchingTokens.length === 1 ? controlledMatchingTokens[0] : null) + ); + + if (!this.actor.token && !token) { + ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.linkedSelectedError')); return false; } - if (!this.actor.token) { + if (!token) { ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.prototypeError')); return false; } - const actor = await DHSummonField.getWorldActor(baseActor); + const actor = await getWorldActor(baseActor); const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes; const tokenSize = actor?.system.metadata.usesSize ? tokenSizes[actor.system.size] : actor.prototypeToken.width; - await this.actor.token.update( - { ...actor.prototypeToken.toJSON(), actorId: actor.id, width: tokenSize, height: tokenSize }, - { diff: false, recursive: false, noHook: true } + // Update token. Avoid using recursive: false, since that prevents animations + await token.update( + { ...actor.prototypeToken.toObject(), actorId: actor.id, width: tokenSize, height: tokenSize }, + { diff: false, noHook: true } ); - if (this.actor.token.combatant) { + if (token.combatant) { this.actor.token.combatant.update({ actorId: actor.id, img: actor.prototypeToken.texture.src }); } @@ -64,17 +86,17 @@ export default class DHSummonField extends fields.SchemaField { if (!this.transform.resourceRefresh.hitPoints) { marks.hitPoints = Math.min( this.actor.system.resources.hitPoints.value, - this.actor.token.actor.system.resources.hitPoints.max - 1 + token.actor.system.resources.hitPoints.max - 1 ); } if (!this.transform.resourceRefresh.stress) { marks.stress = Math.min( this.actor.system.resources.stress.value, - this.actor.token.actor.system.resources.stress.max - 1 + token.actor.system.resources.stress.max - 1 ); } if (marks.hitPoints || marks.stress) { - this.actor.token.actor.update({ + token.actor.update({ 'system.resources': { hitPoints: { value: marks.hitPoints }, stress: { value: marks.stress } @@ -84,20 +106,9 @@ export default class DHSummonField extends fields.SchemaField { const prevPosition = { ...this.actor.sheet.position }; this.actor.sheet.close(); - this.actor.token.actor.sheet.render({ force: true, position: prevPosition }); - } - - /* Check for any available instances of the actor present in the world, or create a world actor based on compendium */ - static async getWorldActor(baseActor) { - if (!baseActor.inCompendium) return baseActor; - - const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`]; - if (dataType && baseActor.img === dataType.DEFAULT_ICON) { - const worldActorCopy = game.actors.find(x => x.name === baseActor.name); - if (worldActorCopy) return worldActorCopy; + token.actor.sheet.render({ force: true, position: prevPosition }); + if (token.object.controlled) { + ui.effectsDisplay.refresh(); } - - const worldActor = await game.system.api.documents.DhpActor.create(baseActor.toObject()); - return worldActor; } } diff --git a/module/documents/_types.d.ts b/module/documents/_types.d.ts index a94d6395..a3af1b33 100644 --- a/module/documents/_types.d.ts +++ b/module/documents/_types.d.ts @@ -3,17 +3,26 @@ import DHItem from './item.mjs'; import BaseDataItem from '../data/item/base.mjs'; import DhActiveEffect from './activeEffect.mjs'; import EmbeddedCollection from '@common/abstract/embedded-collection.mjs'; +import DHToken from './token.mjs'; +import Actor from '@client/documents/actor.mjs'; +import Item from '@client/documents/item.mjs'; declare module './actor.mjs' { - export default interface DhpActor { + export default interface DhpActor extends Actor { system: T; items: EmbeddedCollection; effects: EmbeddedCollection; + get token(): DHToken | null; + + /** @inheritdoc */ + getActiveTokens(linked?: boolean, document?: boolean): (DHToken | foundry.canvas.placeables.Token)[]; + getActiveTokens(linked?: boolean, document: true): DHToken[]; + getActiveTokens(linked?: boolean, document: false): foundry.canvas.placeables.Token[]; } } declare module './item.mjs' { - export default interface DHItem { + export default interface DHItem extends Item { parent: DhpActor; actor: DhpActor; system: T; diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 8ef64f65..30901374 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -34,12 +34,14 @@ export default class DhpActor extends Actor { super.prepareData(); // Update effects if it is the user's character or is controlled - if (canvas.ready) { + // A timeout avoids an infinite loop when accessing token actors before the delta is finished constructing + window.setTimeout(() => { + if (!canvas.ready) return; const controlled = canvas.tokens.controlled.some(t => t.actor === this); if (game.user.character === this || controlled) { - ui.effectsDisplay.render(); + ui.effectsDisplay.refresh(); } - } + }, 0); } /* -------------------------------------------- */ diff --git a/module/documents/token.mjs b/module/documents/token.mjs index 8e91d4f0..d70480db 100644 --- a/module/documents/token.mjs +++ b/module/documents/token.mjs @@ -38,7 +38,8 @@ export default class DHToken extends CONFIG.Token.documentClass { tokens.filter(x => x.actor).map(x => x.actor) ); } - super.createCombatants(tokens, combat ?? {}); + + await super.createCombatants(tokens, combat ?? {}); } /**@inheritdoc */ diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 71f43997..84bcacf2 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -1,6 +1,10 @@ import { diceTypes, getDiceSoNicePresets, getDiceSoNicePreset, range } from '../config/generalConfig.mjs'; import Tagify from '@yaireo/tagify'; +/** + * @import DhpActor from '../documents/actor.mjs'; + */ + export const capitalize = string => { return string.charAt(0).toUpperCase() + string.slice(1); }; @@ -891,6 +895,13 @@ export function shouldUseHopeFearAutomation(options = { gmAsPlayer: true }) { return (!game.user.isGM || options.gmAsPlayer) ? hopeFear.players : hopeFear.gm; } +/** + * Returns the given actor if its a world actor, + * finds a world actor equivalent, + * or imports the actor and returns the imported actor. + * @param {DhpActor} baseActor + * @returns {Promise} a world actor + */ export async function getWorldActor(baseActor) { if (baseActor.inCompendium) { const worldActorCandidates = game.actors.filter(x =>