diff --git a/lang/en.json b/lang/en.json index 02ea41eb..e1006827 100755 --- a/lang/en.json +++ b/lang/en.json @@ -104,15 +104,6 @@ "Character": { "age": "Age", "companionFeatures": "Companion Features", - "contextMenu": { - "consume": "Consume Item", - "equip": "Equip", - "sendToChat": "Send To Chat", - "toLoadout": "Send to Loadout", - "toVault": "Send to Vault", - "unequip": "Unequip", - "useItem": "Use Item" - }, "faith": "Faith", "levelUp": "You can level up", "pronouns": "Pronouns", @@ -189,6 +180,16 @@ "requestingSpotlight": "Requesting The Spotlight", "requestSpotlight": "Request The Spotlight" }, + "ContextMenu": { + "disableEffect": "Disable Effect", + "enableEffect": "Enable Effect", + "equip": "Equip", + "sendToChat": "Send To Chat", + "toLoadout": "Send to Loadout", + "toVault": "Send to Vault", + "unequip": "Unequip", + "useItem": "Use Item" + }, "Countdown": { "addCountdown": "Add Countdown", "FIELDS": { @@ -1372,6 +1373,8 @@ "damageIgnore": "{character} did not take damage" }, "Tooltip": { + "disableEffect": "Disable Effect", + "enableEffect": "Enable Effect", "openItemWorld": "Open Item World", "openActorWorld": "Open Actor World", "sendToChat": "Send to Chat", diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 55fb616d..4f144645 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -15,7 +15,6 @@ export default class CharacterSheet extends DHBaseActorSheet { classes: ['character'], position: { width: 850, height: 800 }, actions: { - triggerContextMenu: CharacterSheet.#triggerContextMenu, toggleVault: CharacterSheet.#toggleVault, rollAttribute: CharacterSheet.#rollAttribute, toggleHope: CharacterSheet.#toggleHope, @@ -29,16 +28,30 @@ export default class CharacterSheet extends DHBaseActorSheet { resizable: true }, dragDrop: [], - contextMenus: [ - { - handler: CharacterSheet._getContextMenuOptions, - selector: '[data-item-uuid]', - options: { - parentClassHooks: false, - fixed: true - } + contextMenus: [{ + handler: CharacterSheet.#getDomainCardContextOptions, + selector: '[data-item-uuid][data-type="domainCard"]', + options: { + parentClassHooks: false, + fixed: true } - ] + }, + { + handler: CharacterSheet.#getEquipamentContextOptions, + selector: '[data-item-uuid][data-type="armor"], [data-item-uuid][data-type="weapon"]', + options: { + parentClassHooks: false, + fixed: true + } + }, + { + handler: CharacterSheet.#getItemContextOptions, + selector: '[data-item-uuid][data-type="consumable"], [data-item-uuid][data-type="miscellaneous"]', + options: { + parentClassHooks: false, + fixed: true + } + }] }; /**@override */ @@ -208,77 +221,70 @@ export default class CharacterSheet extends DHBaseActorSheet { /* -------------------------------------------- */ /** - * Get the set of ContextMenu options. + * Get the set of ContextMenu options for DomainCards. * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance * @this {CharacterSheet} * @protected */ - static _getContextMenuOptions() { - return [ + static #getDomainCardContextOptions() { + /**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */ + const options = [ { - name: 'DAGGERHEART.ACTORS.Character.contextMenu.useItem', - icon: '', - condition: target => { - const doc = getDocFromElement(target); - return typeof doc.use === 'function'; - }, - callback: (target, event) => getDocFromElement(target).use(event) - }, - { - name: 'DAGGERHEART.ACTORS.Character.contextMenu.equip', - icon: '', - condition: target => { - const item = getDocFromElement(target); - return ['weapon', 'armor'].includes(item.type) && !item.system.equipped; - }, - callback: CharacterSheet.#toggleEquipItem.bind(this) - }, - { - name: 'DAGGERHEART.ACTORS.Character.contextMenu.unequip', - icon: '', - condition: el => { - const item = getDocFromElement(el); - return ['weapon', 'armor'].includes(item.type) && item.system.equipped; - }, - callback: CharacterSheet.#toggleEquipItem.bind(this) - }, - { - name: 'DAGGERHEART.ACTORS.Character.contextMenu.toLoadout', - icon: '', - condition: target => { - const item = getDocFromElement(target); - return ['domainCard'].includes(item.type) && item.system.inVault; - }, + name: 'toLoadout', + icon: 'fa-solid fa-arrow-up', + condition: target => getDocFromElement(target).system.inVault, callback: target => getDocFromElement(target).update({ 'system.inVault': false }) }, { - name: 'DAGGERHEART.ACTORS.Character.contextMenu.toVault', - icon: '', - condition: target => { - const item = getDocFromElement(target); - return ['domainCard'].includes(item.type) && !item.system.inVault; - }, + name: 'toVault', + icon: 'fa-solid fa-arrow-down', + condition: target => !getDocFromElement(target).system.inVault, callback: target => getDocFromElement(target).update({ 'system.inVault': true }) }, - { - name: 'DAGGERHEART.ACTORS.Character.contextMenu.sendToChat', - icon: '', - condition: target => { - return typeof getDocFromElement(target).toChat === 'function'; - }, - callback: (target) => getDocFromElement(target).toChat(this.document.id), - }, - { - name: 'CONTROLS.CommonEdit', - icon: '', - callback: target => getDocFromElement(target).sheet.render({ force: true }) - }, - { - name: 'CONTROLS.CommonDelete', - icon: '', - callback: async el => getDocFromElement(el).deleteDialog(), - } - ]; + ].map(option => ({ + ...option, + name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`, + icon: `` + })); + + return [...options, ...this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true })]; + } + + /** + * Get the set of ContextMenu options for Armors and Weapons. + * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance + * @this {CharacterSheet} + * @protected + */ + static #getEquipamentContextOptions() { + const options = [{ + name: 'equip', + icon: 'fa-solid fa-hands', + condition: target => !getDocFromElement(target).system.equipped, + callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target), + }, + { + name: 'unequip', + icon: 'fa-solid fa-hands', + condition: target => getDocFromElement(target).system.equipped, + callback: (target, event) => CharacterSheet.#toggleEquipItem.call(this, event, target), + }].map(option => ({ + ...option, + name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`, + icon: `` + })); + + return [...options, ...this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true })]; + } + + /** + * Get the set of ContextMenu options for Consumable and Miscellaneous. + * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance + * @this {CharacterSheet} + * @protected + */ + static #getItemContextOptions() { + return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true }); } /* -------------------------------------------- */ /* Filter Tracking */ @@ -532,7 +538,7 @@ export default class CharacterSheet extends DHBaseActorSheet { this.document.diceRoll(config); } - //TODO: redo toggleEquipItem method + //TODO: redo toggleEquipItem method /** @@ -593,14 +599,6 @@ export default class CharacterSheet extends DHBaseActorSheet { await doc?.update({ 'system.inVault': !doc.system.inVault }); } - /** - * Trigger the context menu. - * @type {ApplicationClickAction} - */ - static #triggerContextMenu(event, _) { - return CONFIG.ux.ContextMenu.triggerContextMenu(event); - } - async _onDrop(event) { super._onDrop(event); this._onDropItem(event, TextEditor.getDragEventData(event)); diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index e72b36b1..e27ae7a6 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -3,7 +3,7 @@ import { getDocFromElement, tagifyElement } from '../../../helpers/utils.mjs'; import DHActionConfig from '../../sheets-configs/action-config.mjs'; /** - * @typedef {import('@client/applications/_types.mjs').ApplicationClickAction} + * @typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ /** @@ -76,13 +76,30 @@ export default function DHApplicationMixin(Base) { static DEFAULT_OPTIONS = { classes: ['daggerheart', 'sheet', 'dh-style'], actions: { + triggerContextMenu: DHSheetV2.#triggerContextMenu, createDoc: DHSheetV2.#createDoc, editDoc: DHSheetV2.#editDoc, deleteDoc: DHSheetV2.#deleteDoc, toChat: DHSheetV2.#toChat, useItem: DHSheetV2.#useItem, + toggleEffect: DHSheetV2.#toggleEffect, }, - contextMenus: [], + contextMenus: [{ + handler: DHSheetV2.#getEffectContextOptions, + selector: '[data-item-uuid][data-type="effect"]', + options: { + parentClassHooks: false, + fixed: true + }, + }, + { + handler: DHSheetV2.#getActionContextOptions, + selector: '[data-item-uuid][data-type="action"]', + options: { + parentClassHooks: false, + fixed: true + } + }], dragDrop: [], tagifyConfigs: [] }; @@ -192,12 +209,134 @@ export default function DHApplicationMixin(Base) { /* -------------------------------------------- */ /** - * Get the set of ContextMenu options which should be used for journal entry pages in the sidebar. - * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} + * Get the set of ContextMenu options for DomainCards. + * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance + * @this {CharacterSheet} * @protected */ - _getEntryContextOptions() { - return []; + static #getEffectContextOptions() { + /**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */ + const options = [ + { + name: 'disableEffect', + icon: 'fa-solid fa-lightbulb', + condition: target => !getDocFromElement(target).disabled, + callback: target => getDocFromElement(target).update({ disabled: true }) + }, + { + name: 'enableEffect', + icon: 'fa-regular fa-lightbulb', + condition: target => getDocFromElement(target).disabled, + callback: target => getDocFromElement(target).update({ disabled: false }) + }, + ].map(option => ({ + ...option, + name: `DAGGERHEART.APPLICATIONS.ContextMenu.${option.name}`, + icon: `` + })); + + return [...options, ...this._getContextMenuCommonOptions.call(this, { toChat: true })]; + } + + /** + * Get the set of ContextMenu options for Actions. + * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance + * @this {DHSheetV2} + * @protected + */ + static #getActionContextOptions() { + /**@type {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} */ + const getAction = (target) => { + const { actionId } = target.closest('[data-action-id]').dataset; + const { actions, attack } = this.document.system; + return attack.id === actionId ? attack : actions?.find(a => a.id === actionId); + }; + + const options = [ + { + name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', + icon: 'fa-solid fa-burst', + callback: (target, event) => getAction(target).use(event), + }, + { + name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', + icon: 'fa-solid fa-message', + callback: (target) => getAction(target).toChat(this.document.id), + }, + { + name: 'CONTROLS.CommonEdit', + icon: 'fa-solid fa-pen-to-square', + callback: (target) => new DHActionConfig(getAction(target)).render({ force: true }) + }, + { + name: 'CONTROLS.CommonDelete', + icon: 'fa-solid fa-trash', + condition: (target) => { + const { actionId } = target.closest('[data-action-id]').dataset; + const { attack } = this.document.system; + return attack.id !== actionId + }, + callback: async (target) => { + const action = getAction(target) + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { + type: game.i18n.localize(`DAGGERHEART.GENERAL.Action.single`), + name: action.name + }) + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: action.name }) + }); + if (!confirmed) return; + + return this.document.update({ + 'system.actions': this.document.system.actions.do.filter((a) => a.id !== action.id) + }); + } + } + ].map(option => ({ + ...option, + icon: `` + })); + + return options; + } + + /** + * Get the set of ContextMenu options. + * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance + */ + _getContextMenuCommonOptions({ usable = false, toChat = false, deletable = true }) { + const options = [ + { + name: 'CONTROLS.CommonEdit', + icon: 'fa-solid fa-pen-to-square', + callback: target => getDocFromElement(target).sheet.render({ force: true }) + }, + ]; + + if (usable) options.unshift({ + name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', + icon: 'fa-solid fa-burst', + callback: (target, event) => getDocFromElement(target).use(event), + }); + + if (toChat) options.unshift({ + name: 'DAGGERHEART.APPLICATIONS.ContextMenu.sendToChat', + icon: 'fa-solid fa-message', + callback: (target) => getDocFromElement(target).toChat(this.document.id), + }); + + if (deletable) options.push({ + name: 'CONTROLS.CommonDelete', + icon: 'fa-solid fa-trash', + callback: target => getDocFromElement(target).deleteDialog(), + }) + + return options.map(option => ({ + ...option, + icon: `` + })) } /* -------------------------------------------- */ @@ -270,7 +409,7 @@ export default function DHApplicationMixin(Base) { const doc = await cls.create(data, { parent, renderSheet: !event.shiftKey }); if (parentIsItem && type === 'feature') { await this.document.update({ - 'system.features': [...this.document.system.features, doc] + 'system.features': this.document.system.toObject().features.concat(doc.uuid) }); } return doc; @@ -357,6 +496,23 @@ export default function DHApplicationMixin(Base) { await doc.use(event); } + /** + * Toggle a ActiveEffect + * @type {ApplicationClickAction} + */ + static async #toggleEffect(_, target) { + const doc = getDocFromElement(target); + await doc.update({ disabled: !doc.disabled }); + } + + /** + * Trigger the context menu. + * @type {ApplicationClickAction} + */ + static #triggerContextMenu(event, _) { + return CONFIG.ux.ContextMenu.triggerContextMenu(event); + } + } return DHSheetV2; diff --git a/module/applications/sheets/api/base-actor.mjs b/module/applications/sheets/api/base-actor.mjs index 86c97b6b..0331f617 100644 --- a/module/applications/sheets/api/base-actor.mjs +++ b/module/applications/sheets/api/base-actor.mjs @@ -24,6 +24,16 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { openSettings: DHBaseActorSheet.#openSettings, sendExpToChat: DHBaseActorSheet.#sendExpToChat, }, + contextMenus: [ + { + handler: DHBaseActorSheet.#getFeatureContextOptions, + selector: '[data-item-uuid][data-type="feature"]', + options: { + parentClassHooks: false, + fixed: true + } + } + ], dragDrop: [] }; @@ -80,6 +90,21 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { } } + /* -------------------------------------------- */ + /* Context Menu */ + /* -------------------------------------------- */ + + /** + * Get the set of ContextMenu options for Features. + * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance + * @this {DHSheetV2} + * @protected + */ + static #getFeatureContextOptions() { + return this._getContextMenuCommonOptions.call(this, { usable: true, toChat: true }); + } + + /* -------------------------------------------- */ /* Application Clicks Actions */ /* -------------------------------------------- */ @@ -91,7 +116,7 @@ export default class DHBaseActorSheet extends DHApplicationMixin(ActorSheetV2) { static async #openSettings() { await this.settingSheet.render({ force: true }); } - + /** * Send Experience to Chat * @type {ApplicationClickAction} diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index 8649c08c..74ba22f0 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -1,4 +1,4 @@ -import DHActionConfig from '../../sheets-configs/action-config.mjs'; +import { getDocFromElement } from '../../../helpers/utils.mjs'; import DHApplicationMixin from './application-mixin.mjs'; const { ItemSheetV2 } = foundry.applications.sheets; @@ -21,12 +21,21 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { }, actions: { removeAction: DHBaseItemSheet.#removeAction, - addFeature: DHBaseItemSheet.#addFeature, - removeFeature: DHBaseItemSheet.#removeFeature + addFeature: DHBaseItemSheet.#addFeature }, dragDrop: [ { dragSelector: null, dropSelector: '.tab.features .drop-section' }, { dragSelector: '.feature-item', dropSelector: null } + ], + contextMenus: [ + { + handler: DHBaseItemSheet.#getFeatureContextOptions, + selector: '[data-item-uuid][data-type="feature"]', + options: { + parentClassHooks: false, + fixed: true + } + } ] }; @@ -62,6 +71,9 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { case "effects": await this._prepareEffectsContext(context, options) break; + case "features": + context.isGM = game.user.isGM; + break; } return context; @@ -87,9 +99,46 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { } /* -------------------------------------------- */ - /* Application Clicks Actions */ + /* Context Menu */ /* -------------------------------------------- */ + /** + * Get the set of ContextMenu options for Features. + * @returns {import('@client/applications/ux/context-menu.mjs').ContextMenuEntry[]} - The Array of context options passed to the ContextMenu instance + * @this {DHSheetV2} + * @protected + */ + static #getFeatureContextOptions() { + const options = this._getContextMenuCommonOptions({ usable: true, toChat: true, deletable: false }) + options.push( + { + name: 'CONTROLS.CommonDelete', + icon: '', + callback: async (target) => { + const feature = getDocFromElement(target); + if (!feature) return; + const confirmed = await foundry.applications.api.DialogV2.confirm({ + window: { + title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { + type: game.i18n.localize(`TYPES.Item.feature`), + name: feature.name + }) + }, + content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: feature.name }) + }); + if (!confirmed) return; + await this.document.update({ + 'system.features': this.document.system.toObject().features.filter(uuid => uuid !== feature.uuid) + }); + }, + } + ) + return options; + } + + /* -------------------------------------------- */ + /* Application Clicks Actions */ + /* -------------------------------------------- */ /** * Remove an action from the item. @@ -130,36 +179,6 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { 'system.features': [...this.document.system.features, feature] }); } - - /** - * Remove a feature from the item. - * @type {ApplicationClickAction} - */ - static async #removeFeature(event, button) { - event.stopPropagation(); - const target = button.closest('.feature-item'); - const feature = this.document.system.features.find(x => x && x.id === target.id); - - if (feature) { - const confirmed = await foundry.applications.api.DialogV2.confirm({ - window: { - title: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.title', { - type: game.i18n.localize(`TYPES.Item.feature`), - name: feature.name - }) - }, - content: game.i18n.format('DAGGERHEART.APPLICATIONS.DeleteConfirmation.text', { name: feature.name }) - }); - if (!confirmed) return; - } - - await this.document.update({ - 'system.features': this.document.system.features - .filter(feature => feature && feature.id !== target.id) - .map(x => x.uuid) - }); - } - /* -------------------------------------------- */ /* Application Drag/Drop */ /* -------------------------------------------- */ diff --git a/module/applications/ux/contextMenu.mjs b/module/applications/ux/contextMenu.mjs index a913c450..09454848 100644 --- a/module/applications/ux/contextMenu.mjs +++ b/module/applications/ux/contextMenu.mjs @@ -100,7 +100,7 @@ export default class DHContextMenu extends foundry.applications.ux.ContextMenu { event.preventDefault(); event.stopPropagation(); const { clientX, clientY } = event; - const selector = '[data-item-id]'; + const selector = '[data-item-uuid]'; const target = event.target.closest(selector) ?? event.currentTarget.closest(selector); target?.dispatchEvent( new PointerEvent('contextmenu', { diff --git a/templates/sheets/global/partials/feature-section-item.hbs b/templates/sheets/global/partials/feature-section-item.hbs index 617c2dfc..7d9d881a 100644 --- a/templates/sheets/global/partials/feature-section-item.hbs +++ b/templates/sheets/global/partials/feature-section-item.hbs @@ -6,8 +6,7 @@ {{#unless hideContrals}}
- + +
  • {{!-- Image --}} + {{else if (eq type 'effect')}} + + + {{/if}} {{!-- I had to use the {{not}} helper because otherwise the function is called when rendering --}} {{#unless (not item.toChat)}} @@ -179,7 +184,6 @@ Parameters: {{/unless}} - diff --git a/templates/sheets/global/tabs/tab-features.hbs b/templates/sheets/global/tabs/tab-features.hbs index 96b351a2..cb3e106e 100644 --- a/templates/sheets/global/tabs/tab-features.hbs +++ b/templates/sheets/global/tabs/tab-features.hbs @@ -5,6 +5,6 @@ type='feature' isGlassy=true collection=document.system.features - canCreate=true + canCreate=(or document.parent isGM) }} \ No newline at end of file