Merged with v14-Dev

This commit is contained in:
WBHarry 2026-03-15 19:13:37 +01:00
commit 88be00567e
650 changed files with 6323 additions and 4508 deletions

View file

@ -8,5 +8,4 @@ export { default as DhRollTable } from './rollTable.mjs';
export { default as DhScene } from './scene.mjs';
export { default as DhToken } from './token.mjs';
export { default as DhTooltipManager } from './tooltipManager.mjs';
export { default as DhTemplateManager } from './templateManager.mjs';
export { default as DhTokenManager } from './tokenManager.mjs';

View file

@ -50,10 +50,55 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
});
}
/**
* Whether this Active Effect is eligible to be registered with the {@link ActiveEffectRegistry}
*/
get isExpiryTrackable() {
return (
this.persisted &&
!this.inCompendium &&
this.modifiesActor &&
this.start &&
this.isTemporary &&
!this.isExpired
);
}
/* -------------------------------------------- */
/* Event Handlers */
/* -------------------------------------------- */
/** @inheritdoc */
static async createDialog(data = {}, createOptions = {}, options = {}) {
const { folders, types, template, context = {}, ...dialogOptions } = options;
if (types?.length === 0) {
throw new Error('The array of sub-types to restrict to must not be empty.');
}
const creatableEffects = ['base'];
const documentTypes = this.TYPES.filter(type => creatableEffects.includes(type)).map(type => {
const labelKey = `TYPES.ActiveEffect.${type}`;
const label = game.i18n.has(labelKey) ? game.i18n.localize(labelKey) : type;
return { value: type, label };
});
if (!documentTypes.length) {
throw new Error('No document types were permitted to be created.');
}
const sortedTypes = documentTypes.sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang));
return await super.createDialog(data, createOptions, {
folders,
types,
template,
context: { types: sortedTypes, ...context },
...dialogOptions
});
}
/**@inheritdoc*/
async _preCreate(data, options, user) {
const update = {};
@ -109,9 +154,11 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
/* -------------------------------------------- */
/**@inheritdoc*/
static applyField(model, change, field) {
change.value = DhActiveEffect.getChangeValue(model, change, change.effect);
super.applyField(model, change, field);
static applyChangeField(model, change, field) {
change.value = Number.isNumeric(change.value)
? change.value
: DhActiveEffect.getChangeValue(model, change, change.effect);
super.applyChangeField(model, change, field);
}
_applyLegacy(actor, change, changes) {
@ -119,13 +166,12 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
super._applyLegacy(actor, change, changes);
}
/** */
static getChangeValue(model, change, effect) {
let value = change.value;
const isOriginTarget = value.toLowerCase().includes('origin.@');
let key = change.value.toString();
const isOriginTarget = key.toLowerCase().includes('origin.@');
let parseModel = model;
if (isOriginTarget && effect.origin) {
value = change.value.replaceAll(/origin\.@/gi, '@');
key = change.key.replaceAll(/origin\.@/gi, '@');
try {
const originEffect = foundry.utils.fromUuidSync(effect.origin);
const doc =
@ -136,8 +182,8 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect {
} catch (_) {}
}
const evalValue = this.effectSafeEval(itemAbleRollParse(value, parseModel, effect.parent));
return evalValue ?? value;
const evalValue = this.effectSafeEval(itemAbleRollParse(key, parseModel, effect.parent));
return evalValue ?? key;
}
/**

View file

@ -143,7 +143,7 @@ export default class DhpActor extends Actor {
}
const updatedLevelups = Object.keys(this.system.levelData.levelups).reduce((acc, level) => {
if (Number(level) > usedLevel) acc[`-=${level}`] = null;
if (Number(level) > usedLevel) acc[level] = _del;
return acc;
}, {});
@ -188,7 +188,7 @@ export default class DhpActor extends Actor {
if (experiences.length > 0) {
const getUpdate = () => ({
'system.experiences': experiences.reduce((acc, key) => {
acc[`-=${key}`] = null;
acc[key] = _del;
return acc;
}, {})
});

View file

@ -1,105 +0,0 @@
/**
* A singleton class that handles preview templates.
*/
export default class DhTemplateManager {
#activePreview;
/**
* Create a template preview, deactivating any existing ones.
* @param {object} data
*/
async createPreview(data) {
const template = await canvas.templates._createPreview(data, { renderSheet: false });
this.#activePreview = {
document: template.document,
object: template,
origin: { x: template.document.x, y: template.document.y }
};
this.#activePreview.events = {
contextmenu: this.#cancelTemplate.bind(this),
mousedown: this.#confirmTemplate.bind(this),
mousemove: this.#onDragMouseMove.bind(this),
wheel: this.#onMouseWheel.bind(this)
};
canvas.stage.on('mousemove', this.#activePreview.events.mousemove);
canvas.stage.on('mousedown', this.#activePreview.events.mousedown);
canvas.app.view.addEventListener('wheel', this.#activePreview.events.wheel, true);
canvas.app.view.addEventListener('contextmenu', this.#activePreview.events.contextmenu);
}
/**
* Handles the movement of the temlate 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.getCenterPoint(cursor));
object.document.updateSource(update);
object.renderFlags.set({ refresh: true });
}
/**
* Handles the rotation of the preview template on scrolling.
* @param {wheel Event} event
*/
#onMouseWheel(event) {
if (!this.#activePreview) {
return;
}
if (!event.shiftKey && !event.ctrlKey) return;
event.stopPropagation();
event.preventDefault();
const { moveTime, object } = this.#activePreview;
const now = Date.now();
if (now - (moveTime || 0) <= 16) return;
this.#activePreview.moveTime = now;
const multiplier = event.shiftKey ? 0.2 : 0.1;
object.document.updateSource({
direction: object.document.direction + event.deltaY * multiplier
});
object.renderFlags.set({ refresh: true });
}
/**
* Cancels the preview template on right-click.
* @param {contextmenu Event} event
*/
#cancelTemplate(event) {
const { mousemove, mousedown, contextmenu, wheel } = this.#activePreview.events;
canvas.templates._onDragLeftCancel(event);
canvas.stage.off('mousemove', mousemove);
canvas.stage.off('mousedown', mousedown);
canvas.app.view.removeEventListener('contextmenu', contextmenu);
canvas.app.view.removeEventListener('wheel', wheel);
}
/**
* Creates a real MeasuredTemplate at the preview location and cancels the preview.
* @param {click Event} event
*/
#confirmTemplate(event) {
event.stopPropagation();
this.#cancelTemplate(event);
canvas.scene.createEmbeddedDocuments('MeasuredTemplate', [this.#activePreview.document.toObject()]);
this.#activePreview = undefined;
}
}

View file

@ -494,4 +494,62 @@ export default class DHToken extends CONFIG.Token.documentClass {
game.system.registeredTriggers.unregisterItemTriggers(this.actor.items);
}
}
/* V14 TEMP until foundry fixes: https://discord.com/channels/170995199584108546/1421197211194228907/1467296028700049566 */
_onRelatedUpdate(update = {}, operation = {}) {
this.#refreshOverrides(operation);
this._prepareBars();
// Update tracked Combat resource
const combatant = this.combatant;
if (combatant) {
const isActorUpdate = [this, null, undefined].includes(operation.parent);
const resource = game.combat.settings.resource;
const updates = Array.isArray(update) ? update : [update];
if (isActorUpdate && resource && updates.some(u => foundry.utils.hasProperty(u.system ?? {}, resource))) {
combatant.updateResource();
}
ui.combat.render();
}
// Trigger redraws on the token
if (this.parent.isView) {
if (this.object?.hasActiveHUD) canvas.tokens.hud.render();
this.object?.renderFlags.set({ redrawEffects: true });
for (const key of ['bar1', 'bar2']) {
const name = `${this.object?.objectId}.animate${key.capitalize()}`;
const easing = foundry.canvas.animation.CanvasAnimation.easeInOutCosine;
this.object?.animate({ [key]: this[key] }, { name, easing });
}
for (const app of foundry.applications.sheets.TokenConfig.instances()) {
app._preview?.updateSource({ delta: this.toObject().delta }, { diff: false, recursive: false });
app._preview?.object?.renderFlags.set({ refreshBars: true, redrawEffects: true });
}
}
}
/* V14 TEMP until foundry fixes: https://discord.com/channels/170995199584108546/1421197211194228907/1467296028700049566 */
#refreshOverrides(operation) {
if (!this.actor) return;
const { deepClone, mergeObject, equals, isEmpty } = foundry.utils;
const oldOverrides = deepClone(this._overrides) ?? {};
const newOverrides = deepClone(this.actor?.tokenOverrides ?? {}, { prune: true });
if (!equals(oldOverrides, newOverrides)) {
this._overrides = newOverrides;
this.reset();
// Send emulated update data to the PlaceableObject
if (!canvas.ready || canvas.scene !== this.scene) return;
const { width, height, depth, ...changes } = mergeObject(
mergeObject(oldOverrides, this, { insertKeys: false, insertValues: false }),
this._overrides
);
this.object?._onUpdate(changes, {}, game.user.id);
// Hand off size changes to a secondary handler requiring downstream implementation.
const sizeChanges = deepClone({ width, height, depth }, { prune: true });
if (!isEmpty(sizeChanges)) this._onOverrideSize(sizeChanges, operation);
}
}
}