mirror of
https://github.com/Foundryborne/daggerheart.git
synced 2026-07-21 18:09:54 +02:00
[Feature] Transform Linked Actors (#2046)
* Added ability to transform linked actors * Updated to use the utils getWorldActor function * Animate change and fix error when converting to unlinked --------- Co-authored-by: Carlos Fernandez <cfern1990@gmail.com>
This commit is contained in:
parent
44dd3bd2e1
commit
98fedf3990
9 changed files with 101 additions and 32 deletions
23
daggerheart.d.ts
vendored
23
daggerheart.d.ts
vendored
|
|
@ -4,6 +4,15 @@ import '@common/primitives/global.mjs';
|
||||||
import Canvas from '@client/canvas/board.mjs';
|
import Canvas from '@client/canvas/board.mjs';
|
||||||
import { ResourceUpdateMap } from './module/data/action/baseAction.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
|
// Foundry's use of `Object.assign(globalThis) means many globally available objects are not read as such
|
||||||
// This declare global hopefully fixes that
|
// 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
|
// 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;
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
"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.",
|
"canvasError": "There is no active scene.",
|
||||||
"prototypeError": "You can only use a transform action from a Token",
|
"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"
|
"actorLinkError": "You cannot transform a token with Actor Link set to true"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
get element() {
|
||||||
return document.body.querySelector('.daggerheart.dh-style.effects-display');
|
return document.body.querySelector('.daggerheart.dh-style.effects-display');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,10 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
return this._id;
|
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() {
|
get isOwner() {
|
||||||
return this.item?.isOwner ?? true;
|
return this.item?.isOwner ?? true;
|
||||||
}
|
}
|
||||||
|
|
@ -143,6 +146,7 @@ export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the first Actor parent found.
|
* Return the first Actor parent found.
|
||||||
|
* @returns {DhpActor | null}
|
||||||
*/
|
*/
|
||||||
get actor() {
|
get actor() {
|
||||||
return this.item instanceof DhpActor
|
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.
|
* 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.
|
* An action is usable on any actor type. For example, an adversary might have a base attack action.
|
||||||
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
get usable() {
|
get usable() {
|
||||||
const actor = this.actor;
|
const actor = this.actor;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
|
import { getWorldActor } from '../../../helpers/utils.mjs';
|
||||||
|
|
||||||
const fields = foundry.data.fields;
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @import DHSummonAction from '../../action/summonAction.mjs'
|
||||||
|
*/
|
||||||
|
|
||||||
export default class DHSummonField extends fields.SchemaField {
|
export default class DHSummonField extends fields.SchemaField {
|
||||||
/**
|
/**
|
||||||
* Action Workflow order
|
* Action Workflow order
|
||||||
|
|
@ -20,6 +26,11 @@ export default class DHSummonField extends fields.SchemaField {
|
||||||
super(transformFields, options, context);
|
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() {
|
static async execute() {
|
||||||
if (!this.transform.actorUUID) {
|
if (!this.transform.actorUUID) {
|
||||||
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.noTransformActor'));
|
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.noTransformActor'));
|
||||||
|
|
@ -37,26 +48,37 @@ export default class DHSummonField extends fields.SchemaField {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.actor.prototypeToken.actorLink) {
|
const activeTokens = this.actor.getActiveTokens(false, true);
|
||||||
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.actorLinkError'));
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.actor.token) {
|
if (!token) {
|
||||||
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.prototypeError'));
|
ui.notifications.warn(game.i18n.localize('DAGGERHEART.ACTIONS.TYPES.transform.prototypeError'));
|
||||||
return false;
|
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 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;
|
const tokenSize = actor?.system.metadata.usesSize ? tokenSizes[actor.system.size] : actor.prototypeToken.width;
|
||||||
|
|
||||||
await this.actor.token.update(
|
// Update token. Avoid using recursive: false, since that prevents animations
|
||||||
{ ...actor.prototypeToken.toJSON(), actorId: actor.id, width: tokenSize, height: tokenSize },
|
await token.update(
|
||||||
{ diff: false, recursive: false, noHook: true }
|
{ ...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 });
|
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) {
|
if (!this.transform.resourceRefresh.hitPoints) {
|
||||||
marks.hitPoints = Math.min(
|
marks.hitPoints = Math.min(
|
||||||
this.actor.system.resources.hitPoints.value,
|
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) {
|
if (!this.transform.resourceRefresh.stress) {
|
||||||
marks.stress = Math.min(
|
marks.stress = Math.min(
|
||||||
this.actor.system.resources.stress.value,
|
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) {
|
if (marks.hitPoints || marks.stress) {
|
||||||
this.actor.token.actor.update({
|
token.actor.update({
|
||||||
'system.resources': {
|
'system.resources': {
|
||||||
hitPoints: { value: marks.hitPoints },
|
hitPoints: { value: marks.hitPoints },
|
||||||
stress: { value: marks.stress }
|
stress: { value: marks.stress }
|
||||||
|
|
@ -84,20 +106,9 @@ export default class DHSummonField extends fields.SchemaField {
|
||||||
|
|
||||||
const prevPosition = { ...this.actor.sheet.position };
|
const prevPosition = { ...this.actor.sheet.position };
|
||||||
this.actor.sheet.close();
|
this.actor.sheet.close();
|
||||||
this.actor.token.actor.sheet.render({ force: true, position: prevPosition });
|
token.actor.sheet.render({ force: true, position: prevPosition });
|
||||||
}
|
if (token.object.controlled) {
|
||||||
|
ui.effectsDisplay.refresh();
|
||||||
/* 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const worldActor = await game.system.api.documents.DhpActor.create(baseActor.toObject());
|
|
||||||
return worldActor;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
module/documents/_types.d.ts
vendored
13
module/documents/_types.d.ts
vendored
|
|
@ -3,17 +3,26 @@ import DHItem from './item.mjs';
|
||||||
import BaseDataItem from '../data/item/base.mjs';
|
import BaseDataItem from '../data/item/base.mjs';
|
||||||
import DhActiveEffect from './activeEffect.mjs';
|
import DhActiveEffect from './activeEffect.mjs';
|
||||||
import EmbeddedCollection from '@common/abstract/embedded-collection.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' {
|
declare module './actor.mjs' {
|
||||||
export default interface DhpActor<T extends BaseDataActor = BaseDataActor> {
|
export default interface DhpActor<T extends BaseDataActor = BaseDataActor> extends Actor {
|
||||||
system: T;
|
system: T;
|
||||||
items: EmbeddedCollection<DHItem>;
|
items: EmbeddedCollection<DHItem>;
|
||||||
effects: EmbeddedCollection<DhActiveEffect>;
|
effects: EmbeddedCollection<DhActiveEffect>;
|
||||||
|
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' {
|
declare module './item.mjs' {
|
||||||
export default interface DHItem<T extends BaseDataItem = BaseDataItem> {
|
export default interface DHItem<T extends BaseDataItem = BaseDataItem> extends Item {
|
||||||
parent: DhpActor;
|
parent: DhpActor;
|
||||||
actor: DhpActor;
|
actor: DhpActor;
|
||||||
system: T;
|
system: T;
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,14 @@ export default class DhpActor extends Actor {
|
||||||
super.prepareData();
|
super.prepareData();
|
||||||
|
|
||||||
// Update effects if it is the user's character or is controlled
|
// 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);
|
const controlled = canvas.tokens.controlled.some(t => t.actor === this);
|
||||||
if (game.user.character === this || controlled) {
|
if (game.user.character === this || controlled) {
|
||||||
ui.effectsDisplay.render();
|
ui.effectsDisplay.refresh();
|
||||||
}
|
}
|
||||||
}
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ export default class DHToken extends CONFIG.Token.documentClass {
|
||||||
tokens.filter(x => x.actor).map(x => x.actor)
|
tokens.filter(x => x.actor).map(x => x.actor)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
super.createCombatants(tokens, combat ?? {});
|
|
||||||
|
await super.createCombatants(tokens, combat ?? {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**@inheritdoc */
|
/**@inheritdoc */
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import { diceTypes, getDiceSoNicePresets, getDiceSoNicePreset, range } from '../config/generalConfig.mjs';
|
import { diceTypes, getDiceSoNicePresets, getDiceSoNicePreset, range } from '../config/generalConfig.mjs';
|
||||||
import Tagify from '@yaireo/tagify';
|
import Tagify from '@yaireo/tagify';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @import DhpActor from '../documents/actor.mjs';
|
||||||
|
*/
|
||||||
|
|
||||||
export const capitalize = string => {
|
export const capitalize = string => {
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
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;
|
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<DhpActor>} a world actor
|
||||||
|
*/
|
||||||
export async function getWorldActor(baseActor) {
|
export async function getWorldActor(baseActor) {
|
||||||
if (baseActor.inCompendium) {
|
if (baseActor.inCompendium) {
|
||||||
const worldActorCandidates = game.actors.filter(x =>
|
const worldActorCandidates = game.actors.filter(x =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue