diff --git a/module.json b/module.json index bf7f3e4..2c41f47 100644 --- a/module.json +++ b/module.json @@ -1,7 +1,7 @@ { "id": "effectivetray", "title": "Effective Tray", - "version": "1.1.15", + "version": "1.1.16", "compatibility": { "minimum": "11", "verified": "11" diff --git a/scripts/damage-application.mjs b/scripts/damage-application.mjs index 3e04147..d26d3d7 100644 --- a/scripts/damage-application.mjs +++ b/scripts/damage-application.mjs @@ -1,19 +1,101 @@ -import { socketID } from "./const.mjs"; +import { MODULE, socketID } from "./const.mjs"; - /* -------------------------------------------- */ - /* Damage Application Extension (from dnd5e) */ - /* Refer to dnd5e for full documentation */ - /* -------------------------------------------- */ +/* -------------------------------------------- */ +/* Functions */ +/* -------------------------------------------- */ + +/** +* Check targets for ownership when determining which target selection mode to use. +* @param {Array} targets Array of objects with target data, including UUID. +* @returns {boolean} +*/ +async function ownershipCheck(targets) { + for (const target of targets) { + const token = await fromUuid(target.uuid); + if (token?.isOwner) return true; + else continue; + }; + return false; +}; + + +/* -------------------------------------------- */ +/* Damage Application Extension (from dnd5e) */ +/* Refer to dnd5e for full documentation */ +/* -------------------------------------------- */ const MULTIPLIERS = [[-1, "-1"], [0, "0"], [.25, "¼"], [.5, "½"], [1, "1"], [2, "2"]]; export default class EffectiveDAE extends dnd5e.applications.components.DamageApplicationElement { + /** + * Determine which target selection mode to use based on damageTarget setting state. + */ + /** @override */ + async connectedCallback() { + // Fetch the associated chat message + const messageId = this.closest("[data-message-id]")?.dataset.messageId; + this.chatMessage = game.messages.get(messageId); + if (!this.chatMessage) return; + + // Build the frame HTML only once (technically, we've built it twice by this point :weary:) + if (!this.targetList) { + const div = document.createElement("div"); + div.classList.add("card-tray", "damage-tray", "collapsible", "collapsed"); + div.innerHTML = ` + +
+
+
+ + +
+ + +
+
+ `; + this.replaceChildren(div); + this.applyButton = div.querySelector(".apply-damage"); + this.applyButton.addEventListener("click", this._onApplyDamage.bind(this)); + this.targetList = div.querySelector(".targets"); + this.targetSourceControl = this.querySelector(".target-source-control"); + this.targetSourceControl.querySelectorAll("button").forEach(b => + b.addEventListener("click", this._onChangeTargetMode.bind(this)) + ); + if (!this.chatMessage.getFlag("dnd5e", "targets")?.length) this.targetSourceControl.hidden = true; + + // If damageTarget setting is disabled, check if users own any targeted tokens, otherwise use "selected" mode + if (!game.settings.get(MODULE, "damageTarget")) { + const targets = this.chatMessage.getFlag("dnd5e", "targets"); + if (!await ownershipCheck(targets)) this.targetSourceControl.hidden = true; + }; + + div.querySelector(".collapsible-content").addEventListener("click", event => { + event.stopImmediatePropagation(); + }); + } + + this.targetingMode = this.targetSourceControl.hidden ? "selected" : "targeted"; + } + /** * Create a list entry for a single target. * @param {string} uuid UUID of the token represented by this entry. * Extends this method to remove checking for token owner. */ + /** @override */ buildTargetListEntry(uuid) { const token = fromUuidSync(uuid); @@ -75,11 +157,12 @@ export default class EffectiveDAE extends dnd5e.applications.components.DamageAp } /** - * Handle clicking the apply damage button. - * @param {PointerEvent} event Triggering click event. - * Extends this method to emit a request for the active GM client to damage a non-owned actor. - * Special handling is required for the Set `this.damages.properties`. - */ + * Handle clicking the apply damage button. + * @param {PointerEvent} event Triggering click event. + * Extends this method to emit a request for the active GM client to damage a non-owned actor. + * Special handling is required for the Set `this.damages.properties`. + */ + /** @override */ async _onApplyDamage(event) { event.preventDefault(); for (const target of this.targetList.querySelectorAll("[data-target-uuid]")) { @@ -90,19 +173,17 @@ export default class EffectiveDAE extends dnd5e.applications.components.DamageAp await token?.applyDamage(this.damages, options); } else { - const damage = [] - for (const d of this.damages) { - const damageObject = { - properties: Array.from(d.properties), - type: d.type, - value: d.value - }; - damage.push(damageObject); - }; + const damage = []; + + // Convert damage properties to an Array for socket emission + this.damages.forEach(d => { + foundry.utils.mergeObject(d, { properties: Array.from(d.properties) }); + damage.push(d); + }); if (!game.users.activeGM) return ui.notifications.warn(game.i18n.localize("EFFECTIVETRAY.NOTIFICATION.NoActiveGMDamage")); await game.socket.emit(socketID, { type: "damage", data: { id, options, damage } }); }; } this.querySelector(".collapsible").dispatchEvent(new PointerEvent("click", { bubbles: true, cancelable: true })); } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/scripts/effective-tray.mjs b/scripts/effective-tray.mjs index 8829e6f..fa21687 100644 --- a/scripts/effective-tray.mjs +++ b/scripts/effective-tray.mjs @@ -101,7 +101,7 @@ export class effectiveTray { tray.querySelector('ul.effects.unlist').insertAdjacentHTML("beforeend", contents); // Handle click events - tray.querySelector(`li[data-uuid="${uuid}.ActiveEffect.${effect.id}"]`)?.querySelector("button").addEventListener('click', async function() { + tray.querySelector(`li[data-uuid="${uuid}.ActiveEffect.${effect.id}"]`)?.querySelector("button").addEventListener('click', async() => { const mode = tray.querySelector(`[aria-pressed="true"]`)?.dataset?.mode; if (!mode || mode === "selected") { const actors = new Set(); @@ -177,12 +177,12 @@ export class effectiveTray { if (game.settings.get(MODULE, "dontCloseOnPress")) { const buttons = tray.querySelectorAll("button"); for (const button of buttons) { - button.addEventListener('click', async function() { + button.addEventListener('click', async() => { if (!tray.querySelector(".et-uncollapsed")) { await tray.classList.add("et-uncollapsed"); await new Promise(r => setTimeout(r, 108)); await tray.classList.remove("collapsed"); - } + }; }); }; }; @@ -253,9 +253,7 @@ export class effectiveDamage { if (message.flags?.dnd5e?.roll?.type === "damage") { if (!game.user.isGM) { if (message.whisper.length && !message.whisper.includes(game.user.id)) return; - const damageApplication = game.settings.get(MODULE, "damageTarget") ? - document.createElement("effective-damage-application") : - document.createElement("damage-application"); + const damageApplication = document.createElement("effective-damage-application"); damageApplication.classList.add("dnd5e2"); damageApplication.damages = dnd5e.dice.aggregateDamageRolls(message.rolls, { respectProperties: true }).map(roll => ({ value: roll.total, @@ -373,20 +371,17 @@ async function _effectSocket(request) { }; // Make the GM client apply damage to the requested targets -// Convert damage properties back from an Array into a Set async function _damageSocket(request) { if (game.user !== game.users.activeGM) return; const id = request.data.id; const options = request.data.options; const damage = []; - for (const d of request.data.damage) { - const damageObject = { - properties: new Set(d.properties), - type: d.type, - value: d.value - }; - damage.push(damageObject); - }; + + // Convert damage properties back into a Set for damage application + request.data.damage.forEach(d => { + foundry.utils.mergeObject(d, { properties: new Set(d.properties) }); + damage.push(d); + }); return await _applyTargetDamage(id, options, damage); };