Merge branch 'v14-dev' of https://github.com/Foundryborne/daggerheart into v14-dev

This commit is contained in:
WBHarry 2026-04-12 00:33:04 +02:00
commit 6ed975f5b7
4 changed files with 79 additions and 130 deletions

View file

@ -122,15 +122,14 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
async toggleClowncar(actors) {
const animationDuration = 500;
const activeTokens = actors.flatMap(member => member.getActiveTokens());
const scene = game.scenes.get(game.user.viewedScene);
/* getDependentTokens returns already removed tokens with id = null. Need to filter that until it's potentially fixed from Foundry */
const activeTokens = actors.flatMap(member => member.getDependentTokens({ scenes: scene }).filter(x => x._id));
const { x: actorX, y: actorY } = this.document;
if (activeTokens.length > 0) {
for (let token of activeTokens) {
await token.document.update(
{ x: actorX, y: actorY, alpha: 0 },
{ animation: { duration: animationDuration } }
);
setTimeout(() => token.document.delete(), animationDuration);
await token.update({ x: actorX, y: actorY, alpha: 0 }, { animation: { duration: animationDuration } });
setTimeout(() => token.delete(), animationDuration);
}
} else {
const activeScene = game.scenes.find(x => x.id === game.user.viewedScene);
@ -140,11 +139,16 @@ export default class DHTokenHUD extends foundry.applications.hud.TokenHUD {
tokenData.push(data.toObject());
}
const viewedLevel = game.scenes.get(game.user.viewedScene).levels.get(game.user.viewedLevel);
const elevation = this.actor.token?.elevation ?? viewedLevel.elevation.bottom;
const newTokens = await activeScene.createEmbeddedDocuments(
'Token',
tokenData.map(tokenData => ({
...tokenData,
alpha: 0,
level: viewedLevel,
elevation: elevation,
x: actorX,
y: actorY
}))

View file

@ -1,16 +1 @@
export default class DhTokenLayer extends foundry.canvas.layers.TokenLayer {
async _createPreview(createData, options) {
if (options.actor) {
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
if (options.actor?.system.metadata.usesSize) {
const tokenSize = tokenSizes[options.actor.system.size];
if (tokenSize && options.actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) {
createData.width = tokenSize;
createData.height = tokenSize;
}
}
}
return super._createPreview(createData, options);
}
}
export default class DhTokenLayer extends foundry.canvas.layers.TokenLayer {}

View file

@ -44,12 +44,18 @@ export default class DHSummonField extends fields.ArrayField {
count = roll.total;
}
const actor = DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID));
const actor = await DHSummonField.getWorldActor(await foundry.utils.fromUuid(summon.actorUUID));
/* Extending summon data in memory so it's available in actionField.toChat. Think it's harmless, but ugly. Could maybe find a better way. */
summon.rolledCount = count;
summon.actor = actor.toObject();
summonData.push({ actor, count: count });
const countNumber = Number.parseInt(count);
for (let i = 0; i < countNumber; i++) {
const remaining = countNumber - i;
summonData.push({
actor,
tokenPreviewName: `${actor.prototypeToken.name}${remaining > 1 ? ` (${remaining}x)` : ''}`
});
}
}
if (rolls.length) await Promise.all(rolls.map(roll => game.dice3d.showForRoll(roll, game.user, true)));
@ -58,32 +64,22 @@ export default class DHSummonField extends fields.ArrayField {
DHSummonField.handleSummon(summonData, this.actor);
}
/* Check for any available instances of the actor present in the world if we're missing artwork in the compendium */
static getWorldActor(baseActor) {
/* Check for any available instances of the actor present in the world if we're missing artwork in the compendium. If none exists, create one. */
static async getWorldActor(baseActor) {
const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`];
if (baseActor.inCompendium && dataType && baseActor.img === dataType.DEFAULT_ICON) {
const worldActorCopy = game.actors.find(x => x.name === baseActor.name);
return worldActorCopy ?? baseActor;
if (worldActorCopy) return worldActorCopy;
return await game.system.api.documents.DhpActor.create(baseActor.toObject());
}
return baseActor;
}
static async handleSummon(summonData, actionActor, summonIndex = 0) {
const summon = summonData[summonIndex];
const result = await CONFIG.ux.TokenManager.createPreviewAsync(summon.actor, {
name: `${summon.actor.prototypeToken.name}${summon.count > 1 ? ` (${summon.count}x)` : ''}`
});
static async handleSummon(summonData, actionActor) {
await CONFIG.ux.TokenManager.createTokensWithPreview(summonData, { elevation: actionActor.token?.elevation });
if (!result) return actionActor.sheet?.maximize();
summon.actor = result.actor;
summon.count--;
if (summon.count <= 0) {
summonIndex++;
if (summonIndex === summonData.length) return actionActor.sheet?.maximize();
}
DHSummonField.handleSummon(summonData, actionActor, summonIndex);
return actionActor.sheet?.maximize();
}
}

View file

@ -1,104 +1,68 @@
/**
* A singleton class that handles preview tokens.
* A singleton class that handles creating tokens.
*/
export default class DhTokenManager {
#activePreview;
#actor;
#resolve;
/**
* Create a template preview, deactivating any existing ones.
* @param {object} data
* Create a token previer
* @param {Actor} actor
* @param {object} tokenData
*/
async createPreview(actor, tokenData) {
this.#actor = actor;
const token = await canvas.tokens._createPreview(
const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
if (actor?.system.metadata.usesSize) {
const tokenSize = tokenSizes[actor.system.size];
if (tokenSize && actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) {
tokenData.width = tokenSize;
tokenData.height = tokenSize;
}
}
return await canvas.tokens.placeTokens(
[
{
...actor.prototypeToken,
...actor.prototypeToken.toObject(),
actorId: actor.id,
displayName: 50,
...tokenData
},
{ renderSheet: false, actor }
}
],
{ create: false }
);
this.#activePreview = {
document: token.document,
object: token,
origin: { x: token.document.x, y: token.document.y }
};
this.#activePreview.events = {
contextmenu: this.#cancelTemplate.bind(this),
mousedown: this.#confirmTemplate.bind(this),
mousemove: this.#onDragMouseMove.bind(this)
};
canvas.stage.on('mousemove', this.#activePreview.events.mousemove);
canvas.stage.on('mousedown', this.#activePreview.events.mousedown);
canvas.app.view.addEventListener('contextmenu', this.#activePreview.events.contextmenu);
}
/* Currently intended for using as a preview of where to create a token. (note the flag) */
async createPreviewAsync(actor, tokenData = {}) {
return new Promise(resolve => {
this.#resolve = resolve;
this.createPreview(actor, { ...tokenData, flags: { daggerheart: { createPlacement: true } } });
/**
* Creates new tokens on the canvas by placing previews.
* @param {object} tokenData
* @param {object} options
*/
async createTokensWithPreview(tokensData, { elevation } = {}) {
const scene = game.scenes.get(game.user.viewedScene);
if (!scene) return;
const level = scene.levels.get(game.user.viewedLevel);
if (!level) return;
const createElevation = elevation ?? level.elevation.bottom;
for (const tokenData of tokensData) {
const previewTokens = await this.createPreview(tokenData.actor, {
name: tokenData.tokenPreviewName,
level: game.user.viewedLevel,
elevation: createElevation,
flags: { daggerheart: { createPlacement: true } }
});
}
if (!previewTokens?.length) return null;
/**
* Handles the movement of the token preview on mousedrag.
* @param {mousemove Event} event
*/
#onDragMouseMove(event) {
event.stopPropagation();
const { moveTime, object } = this.#activePreview;
const update = {};
const now = Date.now();
if (now - (moveTime || 0) <= 16) return;
this.#activePreview.moveTime = now;
let cursor = event.getLocalPosition(canvas.templates);
Object.assign(update, canvas.grid.getTopLeftPoint(cursor));
object.document.updateSource(update);
object.renderFlags.set({ refresh: true });
}
/**
* Cancels the preview token on right-click.
* @param {contextmenu Event} event
*/
#cancelTemplate(_event, resolved) {
const { mousemove, mousedown, contextmenu } = this.#activePreview.events;
this.#activePreview.object.destroy();
canvas.stage.off('mousemove', mousemove);
canvas.stage.off('mousedown', mousedown);
canvas.app.view.removeEventListener('contextmenu', contextmenu);
if (this.#resolve && !resolved) this.#resolve(false);
}
/**
* Creates a real Actor and token at the preview location and cancels the preview.
* @param {click Event} event
*/
async #confirmTemplate(event) {
event.stopPropagation();
this.#cancelTemplate(event, true);
const actor = this.#actor.inCompendium
? await game.system.api.documents.DhpActor.create(this.#actor.toObject())
: this.#actor;
const tokenData = await actor.getTokenDocument();
const result = await canvas.scene.createEmbeddedDocuments('Token', [
{ ...tokenData.toObject(), x: this.#activePreview.document.x, y: this.#activePreview.document.y }
]);
this.#activePreview = undefined;
if (this.#resolve && result.length) this.#resolve(result[0]);
await canvas.scene.createEmbeddedDocuments(
'Token',
previewTokens.map(x => ({
...x.toObject(),
name: tokenData.actor.prototypeToken.name,
displayName: tokenData.actor.prototypeToken.displayName,
flags: tokenData.actor.prototypeToken.flags
})),
{ controlObject: true, parent: canvas.scene }
);
}
}
}