From 9fb9a4af5505bc8ef74e7cfb381bec47f00fcee9 Mon Sep 17 00:00:00 2001 From: IrkTheImp <41175833+IrkTheImp@users.noreply.github.com> Date: Wed, 2 Jul 2025 17:02:20 -0500 Subject: [PATCH 1/3] fix dr command roll bug (#241) * swap to use the DualityRoll not base roll * update command to use new dice roll. * reinstate DhpActor in action (which causes circular reference) * fix additional dr options --- daggerheart.mjs | 55 +++++---------- module/applications/chatMessage.mjs | 8 ++- module/applications/roll.mjs | 106 ++++++++++++++++++---------- module/helpers/utils.mjs | 8 +-- templates/chat/duality-roll.hbs | 2 +- 5 files changed, 96 insertions(+), 83 deletions(-) diff --git a/daggerheart.mjs b/daggerheart.mjs index 2415c857..ac444c44 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -213,46 +213,27 @@ Hooks.on('chatMessage', (_, message) => { }) : game.i18n.localize('DAGGERHEART.General.Duality'); - const hopeAndFearRoll = `1${rollCommand.hope ?? 'd12'}+1${rollCommand.fear ?? 'd12'}`; - const advantageRoll = `${advantageState === true ? '+d6' : advantageState === false ? '-d6' : ''}`; - const attributeRoll = `${trait?.value ? `${trait.value > 0 ? `+${trait.value}` : `${trait.value}`}` : ''}`; - const roll = await Roll.create(`${hopeAndFearRoll}${advantageRoll}${attributeRoll}`).evaluate(); - - setDiceSoNiceForDualityRoll(roll, advantageState); - - resolve({ - roll, - trait: trait - ? { - value: trait.value, - label: `${game.i18n.localize(abilities[traitValue].label)} ${trait.value >= 0 ? `+` : ``}${trait.value}` - } - : undefined, - title - }); - }).then(async ({ roll, trait, title }) => { - const cls = getDocumentClass('ChatMessage'); - const systemData = new DHDualityRoll({ + const config = { title: title, - origin: target?.id, - roll: roll, - modifiers: trait ? [trait] : [], - hope: { dice: rollCommand.hope ?? 'd12', value: roll.dice[0].total }, - fear: { dice: rollCommand.fear ?? 'd12', value: roll.dice[1].total }, - advantage: advantageState !== null ? { dice: 'd6', value: roll.dice[2].total } : undefined, - advantageState - }); - - const msgData = { - type: 'dualityRoll', - sound: CONFIG.sounds.dice, - system: systemData, - user: game.user.id, - content: 'systems/daggerheart/templates/chat/duality-roll.hbs', - rolls: [roll] + roll: { + trait: traitValue + }, + data: { + traits: { + [traitValue]: trait + } + }, + source: target, + hasSave: false, + dialog: { configure: false }, + evaluate: true, + advantage: rollCommand.advantage == true, + disadvantage: rollCommand.disadvantage == true }; - cls.create(msgData); + await CONFIG.Dice.daggerheart['DualityRoll'].build(config); + + resolve(); }); } diff --git a/module/applications/chatMessage.mjs b/module/applications/chatMessage.mjs index 1328de57..ef76d18f 100644 --- a/module/applications/chatMessage.mjs +++ b/module/applications/chatMessage.mjs @@ -1,6 +1,10 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { async renderHTML() { - if(this.system.messageTemplate) this.content = await foundry.applications.handlebars.renderTemplate(this.system.messageTemplate, this.system); + if (this.system.messageTemplate) + this.content = await foundry.applications.handlebars.renderTemplate( + this.system.messageTemplate, + this.system + ); /* We can change to fully implementing the renderHTML function if needed, instead of augmenting it. */ const html = await super.renderHTML(); @@ -14,7 +18,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { break; case -1: html.classList.add('fear'); - break; + break; default: html.classList.add('critical'); break; diff --git a/module/applications/roll.mjs b/module/applications/roll.mjs index 75d78938..321ead6f 100644 --- a/module/applications/roll.mjs +++ b/module/applications/roll.mjs @@ -1,6 +1,7 @@ import DHDamageRoll from '../data/chat-message/damageRoll.mjs'; import D20RollDialog from '../dialogs/d20RollDialog.mjs'; import DamageDialog from '../dialogs/damageDialog.mjs'; +import { setDiceSoNiceForDualityRoll } from '../helpers/utils.mjs'; /* - Damage & other resources roll @@ -38,7 +39,7 @@ export class DHRoll extends Roll { if (config.dialog.configure !== false) { // Open Roll Dialog const DialogClass = config.dialog?.class ?? this.DefaultDialog; - console.log(roll, config) + console.log(roll, config); const configDialog = await DialogClass.configure(roll, config, message); if (!configDialog) return; } @@ -96,7 +97,8 @@ export class DHRoll extends Roll { } static applyKeybindings(config) { - config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey); + if (config.event) + config.dialog.configure ??= !(config.event.shiftKey || config.event.altKey || config.event.ctrlKey); } formatModifier(modifier) { @@ -108,7 +110,7 @@ export class DHRoll extends Roll { } getFaces(faces) { - return Number((faces.startsWith('d') ? faces.replace('d', '') : faces)); + return Number(faces.startsWith('d') ? faces.replace('d', '') : faces); } constructFormula(config) { @@ -131,7 +133,6 @@ export class DualityDie extends foundry.dice.terms.Die { } export class D20Roll extends DHRoll { - constructor(formula, data = {}, options = {}) { super(formula, data, options); this.constructFormula(); @@ -177,18 +178,27 @@ export class D20Roll extends DHRoll { } static applyKeybindings(config) { - const keys = { - normal: config.event.shiftKey || config.event.altKey || config.event.ctrlKey, - advantage: config.event.altKey, - disadvantage: config.event.ctrlKey + let keys = { + normal: true, + advantage: false, + disadvantage: false }; + if (config.event) { + keys = { + normal: config.event.shiftKey || config.event.altKey || config.event.ctrlKey, + advantage: config.event.altKey, + disadvantage: config.event.ctrlKey + }; + } + // Should the roll configuration dialog be displayed? config.dialog.configure ??= !Object.values(keys).some(k => k); // Determine advantage mode - const advantage = config.roll.advantage === this.ADV_MODE.ADVANTAGE || keys.advantage; - const disadvantage = config.roll.advantage === this.ADV_MODE.DISADVANTAGE || keys.disadvantage; + const advantage = config.roll.advantage === this.ADV_MODE.ADVANTAGE || keys.advantage || config.advantage; + const disadvantage = + config.roll.advantage === this.ADV_MODE.DISADVANTAGE || keys.disadvantage || config.disadvantage; if (advantage && !disadvantage) config.roll.advantage = this.ADV_MODE.ADVANTAGE; else if (!advantage && disadvantage) config.roll.advantage = this.ADV_MODE.DISADVANTAGE; else config.roll.advantage = this.ADV_MODE.NORMAL; @@ -247,14 +257,24 @@ export class D20Roll extends DHRoll { applyBaseBonus() { this.options.roll.modifiers = []; - if(!this.options.roll.bonus) return; - this.options.roll.modifiers.push( - { - label: 'Bonus to Hit', - value: this.options.roll.bonus - // value: Roll.replaceFormulaData('@attackBonus', this.data) - } - ); + if (!this.options.roll.bonus) return; + this.options.roll.modifiers.push({ + label: 'Bonus to Hit', + value: this.options.roll.bonus + // value: Roll.replaceFormulaData('@attackBonus', this.data) + }); + } + + static async buildEvaluate(roll, config = {}, message = {}) { + if (config.evaluate !== false) await roll.evaluate(); + const advantageState = + config.roll.advantage == this.ADV_MODE.ADVANTAGE + ? true + : config.roll.advantage == this.ADV_MODE.DISADVANTAGE + ? false + : null; + setDiceSoNiceForDualityRoll(roll, advantageState); + this.postEvaluate(roll, config); } static postEvaluate(roll, config = {}) { @@ -264,21 +284,29 @@ export class D20Roll extends DHRoll { const difficulty = config.roll.difficulty ?? target.difficulty ?? target.evasion; target.hit = this.isCritical || roll.total >= difficulty; }); - } else if (config.roll.difficulty) config.roll.success = roll.isCritical || roll.total >= config.roll.difficulty; + } else if (config.roll.difficulty) + config.roll.success = roll.isCritical || roll.total >= config.roll.difficulty; config.roll.advantage = { type: config.roll.advantage, dice: roll.dAdvantage?.denomination, value: roll.dAdvantage?.total }; - config.roll.extra = roll.dice.filter(d => !roll.baseTerms.includes(d)).map(d => { - return { - dice: d.denomination, - value: d.total - } - }) + config.roll.extra = roll.dice + .filter(d => !roll.baseTerms.includes(d)) + .map(d => { + return { + dice: d.denomination, + value: d.total + }; + }); config.roll.modifierTotal = 0; - for(let i = 0; i < roll.terms.length; i++) { - if(roll.terms[i] instanceof foundry.dice.terms.NumericTerm && !!roll.terms[i-1] && roll.terms[i-1] instanceof foundry.dice.terms.OperatorTerm) config.roll.modifierTotal += Number(`${roll.terms[i-1].operator}${roll.terms[i].total}`); + for (let i = 0; i < roll.terms.length; i++) { + if ( + roll.terms[i] instanceof foundry.dice.terms.NumericTerm && + !!roll.terms[i - 1] && + roll.terms[i - 1] instanceof foundry.dice.terms.OperatorTerm + ) + config.roll.modifierTotal += Number(`${roll.terms[i - 1].operator}${roll.terms[i].total}`); } } @@ -365,12 +393,13 @@ export class DualityRoll extends D20Roll { return game.i18n.localize(label); } - updateFormula() { - - } + updateFormula() {} createBaseDice() { - if (this.dice[0] instanceof CONFIG.Dice.daggerheart.DualityDie && this.dice[1] instanceof CONFIG.Dice.daggerheart.DualityDie) { + if ( + this.dice[0] instanceof CONFIG.Dice.daggerheart.DualityDie && + this.dice[1] instanceof CONFIG.Dice.daggerheart.DualityDie + ) { this.terms = [this.terms[0], this.terms[1], this.terms[2]]; return; } @@ -383,7 +412,8 @@ export class DualityRoll extends D20Roll { const dieFaces = this.advantageFaces, bardRallyFaces = this.hasBarRally, advDie = new foundry.dice.terms.Die({ faces: dieFaces }); - if (this.hasAdvantage || this.hasDisadvantage || bardRallyFaces) this.terms.push(new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' })); + if (this.hasAdvantage || this.hasDisadvantage || bardRallyFaces) + this.terms.push(new foundry.dice.terms.OperatorTerm({ operator: this.hasDisadvantage ? '-' : '+' })); if (bardRallyFaces) { const rallyDie = new foundry.dice.terms.Die({ faces: bardRallyFaces }); if (this.hasAdvantage) { @@ -401,13 +431,11 @@ export class DualityRoll extends D20Roll { applyBaseBonus() { this.options.roll.modifiers = []; - if(!this.options.roll.trait) return; - this.options.roll.modifiers.push( - { - label: `DAGGERHEART.Abilities.${this.options.roll.trait}.name`, - value: Roll.replaceFormulaData(`@traits.${this.options.roll.trait}.total`, this.data) - } - ); + if (!this.options.roll.trait) return; + this.options.roll.modifiers.push({ + label: `DAGGERHEART.Abilities.${this.options.roll.trait}.name`, + value: Roll.replaceFormulaData(`@traits.${this.options.roll.trait}.total`, this.data) + }); } static postEvaluate(roll, config = {}) { diff --git a/module/helpers/utils.mjs b/module/helpers/utils.mjs index 990d0b35..62248af6 100644 --- a/module/helpers/utils.mjs +++ b/module/helpers/utils.mjs @@ -124,13 +124,13 @@ export const getCommandTarget = () => { export const setDiceSoNiceForDualityRoll = (rollResult, advantageState) => { const diceSoNicePresets = getDiceSoNicePresets(); - rollResult.dice[0].options.appearance = diceSoNicePresets.hope; - rollResult.dice[1].options.appearance = diceSoNicePresets.fear; + rollResult.dice[0].options = { appearance: diceSoNicePresets.hope }; + rollResult.dice[1].options = { appearance: diceSoNicePresets.fear }; //diceSoNicePresets.fear; if (rollResult.dice[2]) { if (advantageState === true) { - rollResult.dice[2].options.appearance = diceSoNicePresets.advantage; + rollResult.dice[2].options = { appearance: diceSoNicePresets.advantage }; } else if (advantageState === false) { - rollResult.dice[2].options.appearance = diceSoNicePresets.disadvantage; + rollResult.dice[2].options = { appearance: diceSoNicePresets.disadvantage }; } } }; diff --git a/templates/chat/duality-roll.hbs b/templates/chat/duality-roll.hbs index 9a530649..ff1e9894 100644 --- a/templates/chat/duality-roll.hbs +++ b/templates/chat/duality-roll.hbs @@ -123,7 +123,7 @@
- {{> 'systems/daggerheart/templates/chat/parts/damage-chat.hbs' damage=damage noTitle=true}} + {{!-- {{> 'systems/daggerheart/templates/chat/parts/damage-chat.hbs' damage=damage noTitle=true}} --}}
From 1b9bd45e9cd685f5b4b451dace341221b2468b84 Mon Sep 17 00:00:00 2001 From: joaquinpereyra98 <24190917+joaquinpereyra98@users.noreply.github.com> Date: Thu, 3 Jul 2025 01:30:23 -0300 Subject: [PATCH 2/3] Feature/ 179 apply items filter in actors sheet (#249) * FEAT: create FilterMenu class FEAT: add FilterMenu to CharacterSheet * FEAT: filter menu style * FIX: file's names and import * FEAT: add filters getters on FilterMenu class * REFACTOR: prettier * FIX: add again the Filter Menu implementation --------- Co-authored-by: Joaquin Pereyra --- module/applications/_module.mjs | 1 + .../applications/sheets/actors/character.mjs | 157 ++++++++++-- module/applications/ux/_module.mjs | 1 + module/applications/ux/filter-menu.mjs | 237 ++++++++++++++++++ styles/daggerheart.css | 42 ++++ styles/daggerheart.less | 2 + styles/less/actors/character/loadout.less | 5 + styles/less/global/filter-menu.less | 47 ++++ .../sheets/actors/character/inventory.hbs | 4 +- templates/sheets/actors/character/loadout.hbs | 4 +- 10 files changed, 478 insertions(+), 22 deletions(-) create mode 100644 module/applications/ux/_module.mjs create mode 100644 module/applications/ux/filter-menu.mjs create mode 100644 styles/less/global/filter-menu.less diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index 1a769052..1ee7f37a 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -18,3 +18,4 @@ export { default as DhContextMenu } from './contextMenu.mjs'; export { default as DhTooltipManager } from './tooltipManager.mjs'; export * as api from './sheets/api/_modules.mjs'; +export * as ux from "./ux/_module.mjs"; diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 8be31690..1a6fec84 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -6,6 +6,7 @@ import DaggerheartSheet from '.././daggerheart-sheet.mjs'; import { abilities } from '../../../config/actorConfig.mjs'; import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs'; import DhCharacterCreation from '../../characterCreation.mjs'; +import FilterMenu from '../../ux/filter-menu.mjs'; const { ActorSheetV2 } = foundry.applications.sheets; const { TextEditor } = foundry.applications.ux; @@ -215,6 +216,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { await super._onFirstRender(context, options); this._createContextMenues(); + this._createFilterMenus(); } /** @inheritDoc */ @@ -366,7 +368,7 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { } /* -------------------------------------------- */ - /* Search Filter */ + /* Filter Tracking */ /* -------------------------------------------- */ /** @@ -376,12 +378,33 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { #search = {}; /** - * Track which item IDs are currently displayed due to a search filter. - * @type {{ inventory: Set, loadout: Set }} + * The currently active search filter. + * @type {FilterMenu} + */ + #menu = {}; + + /** + * Tracks which item IDs are currently displayed, organized by filter type and section. + * @type {{ + * inventory: { + * search: Set, + * menu: Set + * }, + * loadout: { + * search: Set, + * menu: Set + * }, + * }} */ #filteredItems = { - inventory: new Set(), - loadout: new Set() + inventory: { + search: new Set(), + menu: new Set() + }, + loadout: { + search: new Set(), + menu: new Set() + } }; /** @@ -429,15 +452,14 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { * @protected */ _onSearchFilterInventory(event, query, rgx, html) { - this.#filteredItems.inventory.clear(); + this.#filteredItems.inventory.search.clear(); - for (const ul of html.querySelectorAll('.items-list')) { - for (const li of ul.querySelectorAll('.inventory-item')) { - const item = this.document.items.get(li.dataset.itemId); - const match = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); - if (match) this.#filteredItems.inventory.add(item.id); - li.hidden = !match; - } + for (const li of html.querySelectorAll('.inventory-item')) { + const item = this.document.items.get(li.dataset.itemId); + const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); + if (matchesSearch) this.#filteredItems.inventory.search.add(item.id); + const { menu } = this.#filteredItems.inventory; + li.hidden = !(menu.has(item.id) && matchesSearch); } } @@ -450,15 +472,14 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { * @protected */ _onSearchFilterCard(event, query, rgx, html) { - this.#filteredItems.loadout.clear(); + this.#filteredItems.loadout.search.clear(); - const elements = html.querySelectorAll('.items-list .inventory-item, .card-list .card-item'); - - for (const li of elements) { + for (const li of html.querySelectorAll('.items-list .inventory-item, .card-list .card-item')) { const item = this.document.items.get(li.dataset.itemId); - const match = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); - if (match) this.#filteredItems.loadout.add(item.id); - li.hidden = !match; + const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); + if (matchesSearch) this.#filteredItems.loadout.search.add(item.id); + const { menu } = this.#filteredItems.loadout; + li.hidden = !(menu.has(item.id) && matchesSearch); } } @@ -474,6 +495,102 @@ export default class CharacterSheet extends DaggerheartSheet(ActorSheetV2) { this.document.diceRoll(config); } + /* -------------------------------------------- */ + /* Filter Menus */ + /* -------------------------------------------- */ + + _createFilterMenus() { + //Menus could be a application option if needed + const menus = [ + { + key: 'inventory', + container: '[data-application-part="inventory"]', + content: '.items-section', + callback: this._onMenuFilterInventory.bind(this), + target: '.filter-button', + filters: FilterMenu.invetoryFilters + }, + { + key: 'loadout', + container: '[data-application-part="loadout"]', + content: '.items-section', + callback: this._onMenuFilterLoadout.bind(this), + target: '.filter-button', + filters: FilterMenu.cardsFilters + } + ]; + + menus.forEach(m => { + const container = this.element.querySelector(m.container); + this.#menu[m.key] = new FilterMenu(container, m.target, m.filters, m.callback, { + contentSelector: m.content + }); + }); + } + + /** + * Callback when filters change + * @param {PointerEvent} event + * @param {HTMLElement} html + * @param {import('../ux/filter-menu.mjs').FilterItem[]} filters + */ + _onMenuFilterInventory(event, html, filters) { + this.#filteredItems.inventory.menu.clear(); + + for (const li of html.querySelectorAll('.inventory-item')) { + const item = this.document.items.get(li.dataset.itemId); + + const matchesMenu = + filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f)); + if (matchesMenu) this.#filteredItems.inventory.menu.add(item.id); + + const { search } = this.#filteredItems.inventory; + li.hidden = !(search.has(item.id) && matchesMenu); + } + } + + /** + * Callback when filters change + * @param {PointerEvent} event + * @param {HTMLElement} html + * @param {import('../ux/filter-menu.mjs').FilterItem[]} filters + */ + _onMenuFilterLoadout(event, html, filters) { + this.#filteredItems.loadout.menu.clear(); + + for (const li of html.querySelectorAll('.items-list .inventory-item, .card-list .card-item')) { + const item = this.document.items.get(li.dataset.itemId); + + const matchesMenu = + filters.length === 0 || filters.some(f => foundry.applications.ux.SearchFilter.evaluateFilter(item, f)); + if (matchesMenu) this.#filteredItems.loadout.menu.add(item.id); + + const { search } = this.#filteredItems.loadout; + li.hidden = !(search.has(item.id) && matchesMenu); + } + } + /* -------------------------------------------- */ + + async mapFeatureType(data, configType) { + return await Promise.all( + data.map(async x => { + const abilities = x.system.abilities + ? await Promise.all(x.system.abilities.map(async x => await fromUuid(x.uuid))) + : []; + + return { + ...x, + uuid: x.uuid, + system: { + ...x.system, + abilities: abilities, + type: game.i18n.localize(configType[x.system.type ?? x.type].label) + } + }; + }) + ); + } + static async toggleMarks(_, button) { const markValue = Number.parseInt(button.dataset.value); const newValue = this.document.system.armor.system.marks.value >= markValue ? markValue - 1 : markValue; diff --git a/module/applications/ux/_module.mjs b/module/applications/ux/_module.mjs new file mode 100644 index 00000000..a16a4d6f --- /dev/null +++ b/module/applications/ux/_module.mjs @@ -0,0 +1 @@ +export { default as FilterMenu } from './filter-menu.mjs'; diff --git a/module/applications/ux/filter-menu.mjs b/module/applications/ux/filter-menu.mjs new file mode 100644 index 00000000..0973a358 --- /dev/null +++ b/module/applications/ux/filter-menu.mjs @@ -0,0 +1,237 @@ +/** + * @typedef {Object} FilterItem + * @property {string} group - The group name this filter belongs to (e.g., "Type"). + * @property {string} name - The display name of the filter (e.g., "Weapons"). + * @property {import("@client/applications/ux/search-filter.mjs").FieldFilter} filter - The filter condition. + */ + +export default class FilterMenu extends foundry.applications.ux.ContextMenu { + /** + * Filter Menu + * @param {HTMLElement} container - Container element + * @param {string} selector - CSS selector for menu targets + * @param {Array} menuItems - Array of menu entries + * @param {Function} callback - Callback when filters change + * @param {Object} [options] - Additional options + */ + constructor(container, selector, menuItems, callback, options = {}) { + // Set default options + const mergedOptions = { + eventName: 'click', + fixed: true, + ...options + }; + + super(container, selector, menuItems, mergedOptions); + + // Initialize filter states + this.menuItems = menuItems.map(item => ({ + ...item, + enabled: false + })); + + this.callback = callback; + this.contentElement = container.querySelector(mergedOptions.contentSelector); + + const syntheticEvent = { + type: 'pointerdown', + bubbles: true, + cancelable: true, + pointerType: 'mouse', + isPrimary: true, + button: 0 + }; + + this.callback(syntheticEvent, this.contentElement, this.getActiveFilterData()); + } + + /** @inheritdoc */ + async render(target, options = {}) { + await super.render(target, { ...options, animate: false }); + + // Create menu structure + const menu = document.createElement('menu'); + menu.className = 'filter-menu'; + + // Group items by their group property + const groups = this.#groupItems(this.menuItems); + + // Create sections for each group + for (const [groupName, items] of Object.entries(groups)) { + if (!items.length) continue; + + const section = this.#createSection(groupName, items); + menu.appendChild(section); + } + + // Update menu and set position + this.element.replaceChildren(menu); + + menu.addEventListener('click', this.#handleClick.bind(this)); + + this._setPosition(this.element, target, options); + + if (options.animate !== false) await this._animate(true); + return this._onRender(options); + } + + /** + * Groups an array of items by their `group`. + * @param {Array} items - The array of items to group. Each item is expected to have an optional `group` property. + * @returns {Object>} An object where keys are group names and values are arrays of items belonging to each group. + */ + #groupItems(items) { + return items.reduce((groups, item) => { + const group = item.group ?? '_none'; + groups[group] = groups[group] || []; + groups[group].push(item); + return groups; + }, {}); + } + + /** + * Creates a DOM section element for a group of items with corresponding filter buttons. + * @param {string} groupName - The name of the group, used as the section label. + * @param {Array} items - The items to create buttons for. Each item should have: + * @returns {HTMLDivElement} The section DOM element containing the label and buttons. + */ + #createSection(groupName, items) { + const section = document.createElement('fieldset'); + section.className = 'filter-section'; + + const header = document.createElement('legend'); + header.textContent = groupName; + section.appendChild(header); + + const buttons = document.createElement('div'); + buttons.className = 'filter-buttons'; + + items.forEach(item => { + const button = document.createElement('button'); + button.className = `filter-button ${item.enabled ? 'active' : ''}`; + button.textContent = item.name; + item.element = button; + buttons.appendChild(button); + }); + + section.appendChild(buttons); + return section; + } + + /** + * Get filter data from active filters + * @returns {Array} Array of filter configurations + */ + getActiveFilterData() { + return this.menuItems.filter(item => item.enabled).map(item => item.filter); + } + + /** + * Handles click events on filter buttons. + * Toggles the active state of the clicked button and updates the corresponding item's `enabled` state. + * Then triggers the provided callback with the event, the content element, and the current active filter data. + * @param {PointerEvent} event - The click event triggered by interacting with a filter button. + * @returns {void} + */ + #handleClick(event) { + event.preventDefault(); + event.stopPropagation(); + + const button = event.target.closest('.filter-button'); + if (!button) return; + + const clickedItem = this.menuItems.find(item => item.element === button); + if (!clickedItem) return; + + const isActive = button.classList.toggle('active'); + clickedItem.enabled = isActive; + + const filters = this.getActiveFilterData(); + + if (filters.length > 0) { + this.target.classList.add('fa-beat', 'active'); + } else { + this.target.classList.remove('fa-beat', 'active'); + } + + this.callback(event, this.contentElement, filters); + } + + /** + * Generate and return a sorted array of inventory filters. + * @returns {Array} An array of filter objects, sorted by name within each group. + */ + static get invetoryFilters() { + const { OPERATORS } = foundry.applications.ux.SearchFilter; + + const typesFilters = Object.entries(CONFIG.Item.dataModels) + .filter(([, { metadata }]) => metadata.isInventoryItem) + .map(([type, { metadata }]) => ({ + group: game.i18n.localize('Type'), + name: game.i18n.localize(metadata.label), + filter: { + field: 'type', + operator: OPERATORS.EQUALS, + value: type + } + })); + + const burdenFilter = Object.values(CONFIG.daggerheart.GENERAL.burden).map(({ value, label }) => ({ + group: game.i18n.localize('DAGGERHEART.Sheets.Weapon.Burden'), + name: game.i18n.localize(label), + filter: { + field: 'system.burden', + operator: OPERATORS.EQUALS, + value: value + } + })); + + const damageTypeFilter = Object.values(CONFIG.daggerheart.GENERAL.damageTypes).map(({ id, abbreviation }) => ({ + group: 'Damage Type', //TODO localize + name: game.i18n.localize(abbreviation), + filter: { + field: 'system.damage.type', + operator: OPERATORS.EQUALS, + value: id + } + })); + + return [ + ...game.i18n.sortObjects(typesFilters, 'name'), + ...game.i18n.sortObjects(burdenFilter, 'name'), + ...game.i18n.sortObjects(damageTypeFilter, 'name') + ]; + } + + /** + * Generate and return a sorted array of inventory filters. + * @returns {Array} An array of filter objects, sorted by name within each group. + */ + static get cardsFilters() { + const { OPERATORS } = foundry.applications.ux.SearchFilter; + + const typesFilters = Object.values(CONFIG.daggerheart.DOMAIN.cardTypes).map(({ id, label }) => ({ + group: game.i18n.localize('Type'), + name: game.i18n.localize(label), + filter: { + field: 'system.type', + operator: OPERATORS.EQUALS, + value: id + } + })); + + const domainFilter = Object.values(CONFIG.daggerheart.DOMAIN.domains).map(({ id, label }) => ({ + group: game.i18n.localize('DAGGERHEART.Sheets.DomainCard.Domain'), + name: game.i18n.localize(label), + filter: { + field: 'system.domain', + operator: OPERATORS.EQUALS, + value: id + } + })); + + const sort = arr => game.i18n.sortObjects(arr, 'name'); + + return [...sort(typesFilters), ...sort(domainFilter)]; + } +} diff --git a/styles/daggerheart.css b/styles/daggerheart.css index 4d8546b1..763b7e7d 100755 --- a/styles/daggerheart.css +++ b/styles/daggerheart.css @@ -4206,6 +4206,10 @@ div.daggerheart.views.multiclass { .application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .search-bar input:placeholder { color: light-dark(#18162e50, #efe6d850); } +.application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .search-bar input::-webkit-search-cancel-button { + -webkit-appearance: none; + display: none; +} .application.sheet.daggerheart.actor.dh-style.character .tab.loadout .search-section .search-bar .icon { align-content: center; height: 32px; @@ -6054,6 +6058,44 @@ div.daggerheart.views.multiclass { .application prose-mirror .editor-content h3 { font-size: 24px; } +.filter-menu { + width: auto; +} +.filter-menu fieldset.filter-section { + align-items: center; + margin: 5px; + border-radius: 6px; + border-color: light-dark(#18162e, #f3c267); + padding: 5px; +} +.filter-menu fieldset.filter-section legend { + font-family: 'Montserrat', sans-serif; + font-weight: bold; + color: light-dark(#18162e, #f3c267); + font-size: var(--font-size-12); +} +.filter-menu fieldset.filter-section .filter-buttons { + display: flex; + flex-wrap: wrap; + justify-content: space-evenly; + gap: 5px; +} +.filter-menu fieldset.filter-section .filter-buttons button { + background: light-dark(rgba(0, 0, 0, 0.3), #18162e); + color: light-dark(#18162e, #f3c267); + outline: none; + box-shadow: none; + border: 1px solid light-dark(#18162e, #18162e); + padding: 0 0.2rem; + font-size: var(--font-size-12); +} +.filter-menu fieldset.filter-section .filter-buttons button:hover { + background: light-dark(transparent, #f3c267); + color: light-dark(#18162e, #18162e); +} +.filter-menu fieldset.filter-section .filter-buttons button.active { + animation: glow 0.75s infinite alternate; +} .daggerheart { /* Flex */ /****/ diff --git a/styles/daggerheart.less b/styles/daggerheart.less index 362856ce..8903dcf0 100755 --- a/styles/daggerheart.less +++ b/styles/daggerheart.less @@ -64,6 +64,8 @@ @import './less/global/inventory-item.less'; @import './less/global/inventory-fieldset-items.less'; @import './less/global/prose-mirror.less'; +@import "./less/global/filter-menu.less"; + @import '../node_modules/@yaireo/tagify/dist/tagify.css'; .daggerheart { diff --git a/styles/less/actors/character/loadout.less b/styles/less/actors/character/loadout.less index bf1474a2..72393597 100644 --- a/styles/less/actors/character/loadout.less +++ b/styles/less/actors/character/loadout.less @@ -30,6 +30,11 @@ &:placeholder { color: light-dark(@dark-blue-50, @beige-50); } + + &::-webkit-search-cancel-button { + -webkit-appearance: none; + display: none; + } } .icon { diff --git a/styles/less/global/filter-menu.less b/styles/less/global/filter-menu.less new file mode 100644 index 00000000..09962afb --- /dev/null +++ b/styles/less/global/filter-menu.less @@ -0,0 +1,47 @@ +@import '../utils/colors.less'; +@import '../utils/fonts.less'; + +.filter-menu { + width: auto; + + fieldset.filter-section { + align-items: center; + margin: 5px; + border-radius: 6px; + border-color: light-dark(@dark-blue, @golden); + padding: 5px; + + legend { + font-family: @font-body; + font-weight: bold; + color: light-dark(@dark-blue, @golden); + font-size: var(--font-size-12); + } + + .filter-buttons { + display: flex; + flex-wrap: wrap; + justify-content: space-evenly; + gap: 5px; + + button { + background: light-dark(@light-black, @dark-blue); + color: light-dark(@dark-blue, @golden); + outline: none; + box-shadow: none; + border: 1px solid light-dark(@dark-blue, @dark-blue); + padding: 0 0.2rem; + font-size: var(--font-size-12); + + &:hover { + background: light-dark(transparent, @golden); + color: light-dark(@dark-blue, @dark-blue); + } + + &.active { + animation: glow 0.75s infinite alternate; + } + } + } + } +} diff --git a/templates/sheets/actors/character/inventory.hbs b/templates/sheets/actors/character/inventory.hbs index 3f4b98be..be8bb251 100644 --- a/templates/sheets/actors/character/inventory.hbs +++ b/templates/sheets/actors/character/inventory.hbs @@ -10,7 +10,9 @@ - + + +
diff --git a/templates/sheets/actors/character/loadout.hbs b/templates/sheets/actors/character/loadout.hbs index de63323c..c9c9d4b2 100644 --- a/templates/sheets/actors/character/loadout.hbs +++ b/templates/sheets/actors/character/loadout.hbs @@ -10,7 +10,9 @@
- + + + + + \ No newline at end of file diff --git a/templates/sheets/applications/companion-settings/attack.hbs b/templates/sheets/applications/companion-settings/attack.hbs new file mode 100644 index 00000000..f0a26769 --- /dev/null +++ b/templates/sheets/applications/companion-settings/attack.hbs @@ -0,0 +1,21 @@ +
+
+ {{localize "DAGGERHEART.General.basics"}} + {{formGroup systemFields.attack.fields.img value=document.system.attack.img label="Image Path" name="system.attack.img"}} + {{formGroup systemFields.attack.fields.name value=document.system.attack.name label="Attack Name" name="system.attack.name"}} +
+
+ {{localize "DAGGERHEART.Sheets.Adversary.Attack"}} + {{formField systemFields.attack.fields.range value=document.system.attack.range label="Range" name="system.attack.range" localize=true}} + {{#if systemFields.attack.fields.target.fields}} + {{ formField systemFields.attack.fields.target.fields.type value=document.system.attack.target.type label="Target" name="system.attack.target.type" localize=true }} + {{#if (and document.system.attack.target.type (not (eq document.system.attack.target.type 'self')))}} + {{ formField systemFields.attack.fields.target.fields.amount value=document.system.attack.target.amount label="Amount" name="system.attack.target.amount" }} + {{/if}} + {{/if}} +
+
\ No newline at end of file diff --git a/templates/sheets/applications/companion-settings/details.hbs b/templates/sheets/applications/companion-settings/details.hbs new file mode 100644 index 00000000..d047752c --- /dev/null +++ b/templates/sheets/applications/companion-settings/details.hbs @@ -0,0 +1,23 @@ +
+
+ {{localize 'DAGGERHEART.General.basics'}} +
+ {{formGroup systemFields.evasion.fields.value value=document.system.evasion.value localize=true}} + {{formGroup systemFields.resources.fields.stress.fields.value value=document.system.resources.stress.value label='Current Stress'}} + {{formGroup systemFields.resources.fields.stress.fields.max value=document.system.resources.stress.max label='Max Stress'}} +
+
+
+ + +
+
+
+ +
\ No newline at end of file diff --git a/templates/sheets/applications/companion-settings/experiences.hbs b/templates/sheets/applications/companion-settings/experiences.hbs new file mode 100644 index 00000000..124a1a86 --- /dev/null +++ b/templates/sheets/applications/companion-settings/experiences.hbs @@ -0,0 +1,18 @@ +
+
+ {{localize tabs.experiences.label}} +
    + {{#each document.system.experiences as |experience key|}} +
  • + + +
  • + {{/each}} +
+
+ +
\ No newline at end of file diff --git a/templates/sheets/applications/companion-settings/header.hbs b/templates/sheets/applications/companion-settings/header.hbs new file mode 100644 index 00000000..0978f2c3 --- /dev/null +++ b/templates/sheets/applications/companion-settings/header.hbs @@ -0,0 +1,3 @@ +
+

{{document.name}}

+
\ No newline at end of file diff --git a/templates/sheets/global/partials/inventory-item.hbs b/templates/sheets/global/partials/inventory-item.hbs index b69f1a2d..9b1c392b 100644 --- a/templates/sheets/global/partials/inventory-item.hbs +++ b/templates/sheets/global/partials/inventory-item.hbs @@ -1,7 +1,11 @@ -
  • +
  • -
    {{item.name}}
    + {{#if isCompanion}} + {{item.name}} + {{else}} +
    {{item.name}}
    + {{/if}} {{#if (eq type 'weapon')}}
    {{#if isSidebar}} @@ -114,6 +118,11 @@ {{#unless hideControls}} {{#if isActor}}
    + {{#if (eq type 'actor')}} + + + + {{/if}} {{#if (eq type 'adversary')}}