Reworked summon action and clowncar functionality to work with levels (#1791)

This commit is contained in:
WBHarry 2026-04-12 00:25:43 +02:00 committed by GitHub
parent 94f1fbdd9b
commit 3ec013ff50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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) { async toggleClowncar(actors) {
const animationDuration = 500; 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; const { x: actorX, y: actorY } = this.document;
if (activeTokens.length > 0) { if (activeTokens.length > 0) {
for (let token of activeTokens) { for (let token of activeTokens) {
await token.document.update( await token.update({ x: actorX, y: actorY, alpha: 0 }, { animation: { duration: animationDuration } });
{ x: actorX, y: actorY, alpha: 0 }, setTimeout(() => token.delete(), animationDuration);
{ animation: { duration: animationDuration } }
);
setTimeout(() => token.document.delete(), animationDuration);
} }
} else { } else {
const activeScene = game.scenes.find(x => x.id === game.user.viewedScene); 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()); 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( const newTokens = await activeScene.createEmbeddedDocuments(
'Token', 'Token',
tokenData.map(tokenData => ({ tokenData.map(tokenData => ({
...tokenData, ...tokenData,
alpha: 0, alpha: 0,
level: viewedLevel,
elevation: elevation,
x: actorX, x: actorX,
y: actorY y: actorY
})) }))

View file

@ -1,16 +1 @@
export default class DhTokenLayer extends foundry.canvas.layers.TokenLayer { 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);
}
}

View file

@ -44,12 +44,18 @@ export default class DHSummonField extends fields.ArrayField {
count = roll.total; 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. */ /* 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(); 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))); 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); 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 */ /* 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 getWorldActor(baseActor) { static async getWorldActor(baseActor) {
const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`]; const dataType = game.system.api.data.actors[`Dh${baseActor.type.capitalize()}`];
if (baseActor.inCompendium && dataType && baseActor.img === dataType.DEFAULT_ICON) { if (baseActor.inCompendium && dataType && baseActor.img === dataType.DEFAULT_ICON) {
const worldActorCopy = game.actors.find(x => x.name === baseActor.name); 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; return baseActor;
} }
static async handleSummon(summonData, actionActor, summonIndex = 0) { static async handleSummon(summonData, actionActor) {
const summon = summonData[summonIndex]; await CONFIG.ux.TokenManager.createTokensWithPreview(summonData, { elevation: actionActor.token?.elevation });
const result = await CONFIG.ux.TokenManager.createPreviewAsync(summon.actor, {
name: `${summon.actor.prototypeToken.name}${summon.count > 1 ? ` (${summon.count}x)` : ''}`
});
if (!result) return actionActor.sheet?.maximize(); 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);
} }
} }

View file

@ -1,104 +1,68 @@
/** /**
* A singleton class that handles preview tokens. * A singleton class that handles creating tokens.
*/ */
export default class DhTokenManager { export default class DhTokenManager {
#activePreview;
#actor;
#resolve;
/** /**
* Create a template preview, deactivating any existing ones. * Create a token previer
* @param {object} data * @param {Actor} actor
* @param {object} tokenData
*/ */
async createPreview(actor, tokenData) { async createPreview(actor, tokenData) {
this.#actor = actor; const tokenSizes = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).tokenSizes;
const token = await canvas.tokens._createPreview( if (actor?.system.metadata.usesSize) {
{ const tokenSize = tokenSizes[actor.system.size];
...actor.prototypeToken, if (tokenSize && actor.system.size !== CONFIG.DH.ACTOR.tokenSize.custom.id) {
displayName: 50, tokenData.width = tokenSize;
...tokenData tokenData.height = tokenSize;
}, }
{ renderSheet: false, actor } }
return await canvas.tokens.placeTokens(
[
{
...actor.prototypeToken.toObject(),
actorId: actor.id,
displayName: 50,
...tokenData
}
],
{ 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 } } });
});
} }
/** /**
* Handles the movement of the token preview on mousedrag. * Creates new tokens on the canvas by placing previews.
* @param {mousemove Event} event * @param {object} tokenData
* @param {object} options
*/ */
#onDragMouseMove(event) { async createTokensWithPreview(tokensData, { elevation } = {}) {
event.stopPropagation(); const scene = game.scenes.get(game.user.viewedScene);
const { moveTime, object } = this.#activePreview; if (!scene) return;
const update = {};
const now = Date.now(); const level = scene.levels.get(game.user.viewedLevel);
if (now - (moveTime || 0) <= 16) return; if (!level) return;
this.#activePreview.moveTime = now;
let cursor = event.getLocalPosition(canvas.templates); 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;
Object.assign(update, canvas.grid.getTopLeftPoint(cursor)); await canvas.scene.createEmbeddedDocuments(
'Token',
object.document.updateSource(update); previewTokens.map(x => ({
object.renderFlags.set({ refresh: true }); ...x.toObject(),
} name: tokenData.actor.prototypeToken.name,
displayName: tokenData.actor.prototypeToken.displayName,
/** flags: tokenData.actor.prototypeToken.flags
* Cancels the preview token on right-click. })),
* @param {contextmenu Event} event { controlObject: true, parent: canvas.scene }
*/ );
#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]);
} }
} }