diff --git a/lang/en.json b/lang/en.json index 64ebb776..c8a605ba 100755 --- a/lang/en.json +++ b/lang/en.json @@ -1893,7 +1893,8 @@ "domains": "Domains", "downtime": "Downtime", "rules": "Rules", - "partyMembers": "Party Members" + "partyMembers": "Party Members", + "projects": "Projects" }, "Tiers": { "singular": "Tier", diff --git a/module/applications/sheets/actors/party.mjs b/module/applications/sheets/actors/party.mjs index d366669a..b0495d9a 100644 --- a/module/applications/sheets/actors/party.mjs +++ b/module/applications/sheets/actors/party.mjs @@ -1,5 +1,7 @@ import DHBaseActorSheet from '../api/base-actor.mjs'; import { getDocFromElement } from '../../../helpers/utils.mjs'; +import { ItemBrowser } from '../../ui/itemBrowser.mjs'; +import FilterMenu from '../../ux/filter-menu.mjs'; export default class Party extends DHBaseActorSheet { /**@inheritdoc */ @@ -16,7 +18,8 @@ export default class Party extends DHBaseActorSheet { toggleHope: Party.#toggleHope, toggleHitPoints: Party.#toggleHitPoints, toggleStress: Party.#toggleStress, - toggleArmorSlot: Party.#toggleArmorSlot + toggleArmorSlot: Party.#toggleArmorSlot, + tempBrowser: Party.#tempBrowser }, dragDrop: [{ dragSelector: '.actors-section .inventory-item', dropSelector: null }] }; @@ -30,18 +33,67 @@ export default class Party extends DHBaseActorSheet { template: 'systems/daggerheart/templates/sheets/actors/party/resources.hbs', scrollable: ['.resources'] }, + projects: { + template: 'systems/daggerheart/templates/sheets/actors/party/projects.hbs', + scrollable: ['.projects'] + }, + inventory: { + template: 'systems/daggerheart/templates/sheets/actors/party/inventory.hbs', + scrollable: ['.inventory'] + }, notes: { template: 'systems/daggerheart/templates/sheets/actors/party/notes.hbs' } }; /** @inheritdoc */ static TABS = { primary: { - tabs: [{ id: 'partyMembers' }, { id: 'resources' }, { id: 'notes' }], + tabs: [ + { id: 'partyMembers' }, + { id: 'resources' }, + { id: 'projects' }, + { id: 'inventory' }, + { id: 'notes' } + ], initial: 'partyMembers', labelPrefix: 'DAGGERHEART.GENERAL.Tabs' } }; + async _onRender(context, options) { + await super._onRender(context, options); + this._createFilterMenus(); + this._createSearchFilter(); + } + + /* -------------------------------------------- */ + /* Prepare Context */ + /* -------------------------------------------- */ + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + + context.inventory = { + currency: { + title: game.i18n.localize('DAGGERHEART.CONFIG.Gold.title'), + coins: game.i18n.localize('DAGGERHEART.CONFIG.Gold.coins'), + handfuls: game.i18n.localize('DAGGERHEART.CONFIG.Gold.handfuls'), + bags: game.i18n.localize('DAGGERHEART.CONFIG.Gold.bags'), + chests: game.i18n.localize('DAGGERHEART.CONFIG.Gold.chests') + } + }; + + const homebrewCurrency = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).currency; + if (homebrewCurrency.enabled) { + context.inventory.currency = homebrewCurrency; + } + + if (context.inventory.length === 0) { + context.inventory = Array(1).fill(Array(5).fill([])); + } + + return context; + } + async _preparePartContext(partId, context, options) { context = await super._preparePartContext(partId, context, options); switch (partId) { @@ -148,6 +200,162 @@ export default class Party extends DHBaseActorSheet { this.render(); } + /** + * Opens Compedium Browser + */ + static async #tempBrowser(_, target) { + new ItemBrowser().render({ force: true }); + } + + /** + * Get the set of ContextMenu options for Consumable and Loot. + * @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 */ + /* -------------------------------------------- */ + + /** + * The currently active search filter. + * @type {foundry.applications.ux.SearchFilter} + */ + #search = {}; + + /** + * 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: { + search: new Set(), + menu: new Set() + }, + loadout: { + search: new Set(), + menu: new Set() + } + }; + + /* -------------------------------------------- */ + /* Search Inputs */ + /* -------------------------------------------- */ + + /** + * Create and initialize search filter instances for the inventory and loadout sections. + * + * Sets up two {@link foundry.applications.ux.SearchFilter} instances: + * - One for the inventory, which filters items in the inventory grid. + * - One for the loadout, which filters items in the loadout/card grid. + * @private + */ + _createSearchFilter() { + //Filters could be a application option if needed + const filters = [ + { + key: 'inventory', + input: 'input[type="search"].search-inventory', + content: '[data-application-part="inventory"] .items-section', + callback: this._onSearchFilterInventory.bind(this) + } + ]; + + for (const { key, input, content, callback } of filters) { + const filter = new foundry.applications.ux.SearchFilter({ + inputSelector: input, + contentSelector: content, + callback + }); + filter.bind(this.element); + this.#search[key] = filter; + } + } + + /** + * Handle invetory items search and filtering. + * @param {KeyboardEvent} event The keyboard input event. + * @param {string} query The input search string. + * @param {RegExp} rgx The regular expression query that should be matched against. + * @param {HTMLElement} html The container to filter items from. + * @protected + */ + async _onSearchFilterInventory(_event, query, rgx, html) { + this.#filteredItems.inventory.search.clear(); + + for (const li of html.querySelectorAll('.inventory-item')) { + const item = await getDocFromElement(li); + 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); + } + } + + /* -------------------------------------------- */ + /* 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 + } + ]; + + 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 + */ + async _onMenuFilterInventory(_event, html, filters) { + this.#filteredItems.inventory.menu.clear(); + + for (const li of html.querySelectorAll('.inventory-item')) { + const item = await getDocFromElement(li); + + 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); + } + } + /* -------------------------------------------- */ async _onDragStart(event) { diff --git a/module/data/actor/party.mjs b/module/data/actor/party.mjs index 61edd627..3078bedd 100644 --- a/module/data/actor/party.mjs +++ b/module/data/actor/party.mjs @@ -8,7 +8,13 @@ export default class DhParty extends BaseDataActor { return { ...super.defineSchema(), partyMembers: new ForeignDocumentUUIDArrayField({ type: 'Actor' }), - notes: new fields.HTMLField() + notes: new fields.HTMLField(), + gold: new fields.SchemaField({ + coins: new fields.NumberField({ initial: 0, integer: true }), + handfuls: new fields.NumberField({ initial: 1, integer: true }), + bags: new fields.NumberField({ initial: 0, integer: true }), + chests: new fields.NumberField({ initial: 0, integer: true }) + }) }; } diff --git a/styles/less/sheets/actors/party/inventory.less b/styles/less/sheets/actors/party/inventory.less new file mode 100644 index 00000000..1dfc66de --- /dev/null +++ b/styles/less/sheets/actors/party/inventory.less @@ -0,0 +1,73 @@ +@import '../../../utils/colors.less'; +@import '../../../utils/fonts.less'; + +.application.sheet.daggerheart.actor.dh-style.party { + .tab.inventory { + .search-section { + display: flex; + gap: 10px; + align-items: center; + + .search-bar { + position: relative; + color: light-dark(@dark-blue-50, @beige-50); + width: 100%; + padding-top: 5px; + + input { + border-radius: 50px; + background: light-dark(@dark-blue-10, @golden-10); + border: none; + outline: 2px solid transparent; + transition: all 0.3s ease; + padding: 0 20px; + + &:hover { + outline: 2px solid light-dark(@dark, @golden); + } + + &:placeholder { + color: light-dark(@dark-blue-50, @beige-50); + } + + &::-webkit-search-cancel-button { + -webkit-appearance: none; + display: none; + } + } + + .icon { + align-content: center; + height: 32px; + position: absolute; + right: 20px; + font-size: 16px; + z-index: 1; + color: light-dark(@dark-blue-50, @beige-50); + } + } + } + + .items-section { + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: auto; + mask-image: linear-gradient(0deg, transparent 0%, black 5%, black 95%, transparent 100%); + padding: 20px 0; + + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + } + + .currency-section { + display: flex; + gap: 10px; + padding: 10px 10px 0; + + input { + color: light-dark(@dark, @beige); + } + } + } +} diff --git a/styles/less/sheets/actors/party/sheet.less b/styles/less/sheets/actors/party/sheet.less index 9f5b6b1c..4e1676e2 100644 --- a/styles/less/sheets/actors/party/sheet.less +++ b/styles/less/sheets/actors/party/sheet.less @@ -14,6 +14,7 @@ .application.sheet.daggerheart.actor.dh-style.party { .tab { + height: -webkit-fill-available; max-height: 300px; overflow-y: auto; scrollbar-width: thin; diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less index 82510c7a..fe4efac7 100644 --- a/styles/less/sheets/index.less +++ b/styles/less/sheets/index.less @@ -25,6 +25,7 @@ @import './actors/party/header.less'; @import './actors/party/party-members.less'; @import './actors/party/sheet.less'; +@import './actors/party/inventory.less'; @import './actors/party/resources.less'; @import './items/beastform.less'; diff --git a/templates/sheets/actors/party/inventory.hbs b/templates/sheets/actors/party/inventory.hbs new file mode 100644 index 00000000..4d315610 --- /dev/null +++ b/templates/sheets/actors/party/inventory.hbs @@ -0,0 +1,78 @@ +
+
+ + + + + + + +
+ +
+
+ {{localize this.inventory.currency.coins}} + {{formInput systemFields.gold.fields.coins value=source.system.gold.coins enriched=source.system.gold.coins + localize=true toggled=true}} +
+
+ {{localize this.inventory.currency.handfuls}} + {{formInput systemFields.gold.fields.handfuls value=source.system.gold.handfuls + enriched=source.system.gold.handfuls localize=true toggled=true}} +
+
+ {{localize this.inventory.currency.bags}} + {{formInput systemFields.gold.fields.bags value=source.system.gold.bags enriched=source.system.gold.bags + localize=true toggled=true}} +
+
+ {{localize this.inventory.currency.chests}} + {{formInput systemFields.gold.fields.chests value=source.system.gold.chests + enriched=source.system.gold.chests localize=true toggled=true}} +
+ +
+ +
+ {{> 'daggerheart.inventory-items' + title='TYPES.Item.weapon' + type='weapon' + collection=document.itemTypes.weapon + isGlassy=true + canCreate=true + hideResources=true + hideControls=true + }} + {{> 'daggerheart.inventory-items' + title='TYPES.Item.armor' + type='armor' + collection=document.itemTypes.armor + isGlassy=true + canCreate=true + hideResources=true + hideControls=true + }} + {{> 'daggerheart.inventory-items' + title='TYPES.Item.consumable' + type='consumable' + collection=document.itemTypes.consumable + isGlassy=true + canCreate=true + hideControls=true + }} + {{> 'daggerheart.inventory-items' + title='TYPES.Item.loot' + type='loot' + collection=document.itemTypes.loot + isGlassy=true + canCreate=true + hideControls=true + }} +
+
\ No newline at end of file diff --git a/templates/sheets/actors/party/projects.hbs b/templates/sheets/actors/party/projects.hbs new file mode 100644 index 00000000..6338626e --- /dev/null +++ b/templates/sheets/actors/party/projects.hbs @@ -0,0 +1,4 @@ +
+

Soon tm

+
\ No newline at end of file