diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index 13ff0a38..9efbe7d7 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -1,6 +1,6 @@ import DHBaseActorSettings from '../../applications/sheets/api/actor-setting.mjs'; import DHItem from '../../documents/item.mjs'; -import { getScrollTextData } from '../../helpers/utils.mjs'; +import { createShallowProxy, getScrollTextData } from '../../helpers/utils.mjs'; const fields = foundry.data.fields; @@ -180,8 +180,7 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel { * @returns {object} */ getRollData() { - const data = { ...this }; - return data; + return createShallowProxy(this); } /** diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index f6c794f1..aebf33bf 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -7,7 +7,12 @@ * @property {boolean} isInventoryItem- Indicates whether items of this type is a Inventory Item */ -import { addLinkedItemsDiff, getScrollTextData, updateLinkedItemApps } from '../../helpers/utils.mjs'; +import { + addLinkedItemsDiff, + getScrollTextData, + createShallowProxy, + updateLinkedItemApps +} from '../../helpers/utils.mjs'; import { ActionsField } from '../fields/actionField.mjs'; import FormulaField from '../fields/formulaField.mjs'; @@ -159,8 +164,8 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { * @returns {object} */ getRollData(options = {}) { - const actorRollData = this.actor?.getRollData() ?? {}; - const data = { ...actorRollData, item: { ...this } }; + const data = this.actor?.getRollData() ?? {}; + data.item = createShallowProxy(this); return data; } diff --git a/module/documents/activeEffect.mjs b/module/documents/activeEffect.mjs index 227e2613..08463818 100644 --- a/module/documents/activeEffect.mjs +++ b/module/documents/activeEffect.mjs @@ -169,27 +169,36 @@ export default class DhActiveEffect extends foundry.documents.ActiveEffect { super._applyChangeUnguided(actor, change, changes, options); } + /** Recursively finds the first parent document of the given object */ + static #resolveParentDocument(model, documentClass) { + return model instanceof documentClass + ? model + : model.parent + ? this.#resolveParentDocument(model.parent, documentClass) + : null; + } + static getChangeValue(model, change, effect) { - let key = change.value.toString(); - const isOriginTarget = key.toLowerCase().includes('origin.@'); - let parseModel = model; - if (isOriginTarget && effect.origin) { - key = change.key.replaceAll(/origin\.@/gi, '@'); - try { - const originEffect = foundry.utils.fromUuidSync(effect.origin); - const doc = - originEffect.parent?.parent instanceof game.system.api.documents.DhpActor - ? originEffect.parent - : originEffect.parent.parent; - if (doc) parseModel = doc; - } catch (_) {} + let value = change.value.toString(); + const useOrigin = value.toLowerCase().includes('origin.@') && effect.origin; + let origin = null; + if (effect.origin) { + if (useOrigin) value = value.replaceAll(/origin\.@/gi, '@'); + const originEffect = foundry.utils.fromUuidSync(effect.origin); + origin = this.#resolveParentDocument(originEffect, Item); } + // Get the actor and item documents. Note that actor roll data is inclusive of system roll data + const actor = this.#resolveParentDocument(model, Actor); + const item = + (useOrigin ? origin : null) ?? + this.#resolveParentDocument(effect.parent, Item) ?? + (origin?.actor === actor ? origin : null); const stackingParsedValue = effect.system.stacking - ? Roll.replaceFormulaData(key, { stacks: effect.system.stacking.value }) - : key; - const evalValue = itemAbleRollParse(stackingParsedValue, parseModel, effect.parent); - return evalValue ?? key; + ? Roll.replaceFormulaData(value, { stacks: effect.system.stacking.value }) + : value; + const evalValue = this.effectSafeEval(itemAbleRollParse(stackingParsedValue, actor, item)); + return evalValue ?? value; } /** diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 52150dae..eb57a186 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -1,7 +1,7 @@ import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs'; import { LevelOptionType } from '../data/levelTier.mjs'; import DHFeature from '../data/item/feature.mjs'; -import { createScrollText, damageKeyToNumber, getDamageKey } from '../helpers/utils.mjs'; +import { createScrollText, damageKeyToNumber, getDamageKey, createShallowProxy } from '../helpers/utils.mjs'; import DhCompanionLevelUp from '../applications/levelup/companionLevelup.mjs'; import { ResourceUpdateMap } from '../data/action/baseAction.mjs'; import { abilities } from '../config/actorConfig.mjs'; @@ -595,10 +595,7 @@ export default class DhpActor extends Actor { /**@inheritdoc */ getRollData() { - const rollData = foundry.utils.deepClone(super.getRollData()); - /* system gets repeated infinately which causes issues when trying to use the data for document creation */ - delete rollData.system; - + const rollData = createShallowProxy(super.getRollData()); rollData.id = this.id; rollData.name = this.name; rollData.system = this.system.getRollData(); diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 8ece56fa..93aa3b28 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -54,13 +54,7 @@ export default class DHItem extends foundry.documents.Item { * @returns */ getRollData(options = {}) { - let data; - if (this.system.getRollData) data = this.system.getRollData(options); - else { - const actorRollData = this.actor?.getRollData(options) ?? {}; - data = { ...actorRollData, item: { ...this.system } }; - } - + let data = this.system.getRollData(options); if (data?.item) { data.item.flags = { ...this.flags }; data.item.name = this.name; diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index b4a04579..1650b505 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -372,10 +372,11 @@ export const itemAbleRollParse = (value, actor, item) => { const isItemTarget = value.toLowerCase().includes('item.@'); const slicedValue = isItemTarget ? value.replaceAll(/item\.@/gi, '@') : value; - const model = isItemTarget ? item : actor; + const model = isItemTarget || item instanceof Item ? item : actor; + const rollData = isItemTarget || !model?.getRollData ? model : model.getRollData(); try { - return Roll.replaceFormulaData(slicedValue, isItemTarget || !model?.getRollData ? model : model.getRollData()); + return Roll.replaceFormulaData(slicedValue, rollData); } catch (_) { return ''; } @@ -809,3 +810,31 @@ export function sortBy(arr, fn) { }; return arr.sort(cmp); } + +/** + * Creates a proxy that allows retrieval of top data but diverts updates to a different object. + * Generally used for roll data + */ +export function createShallowProxy(obj) { + if (obj._isShallowProxy) return obj; + + const overrides = {}; + return new Proxy(obj, { + get(target, prop, receiver) { + if (prop === '_isShallowProxy') return true; + if (prop in overrides) return overrides[prop]; + return Reflect.get(target, prop, receiver); + }, + set(_target, prop, newValue) { + overrides[prop] = newValue; + return true; + }, + deleteProperty(_target, prop) { + delete overrides[prop]; + return true; + }, + has(target, key) { + return key in overrides || key in target; + } + }); +}