diff --git a/lang/en.json b/lang/en.json index 32ea2227..d03ce369 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2361,6 +2361,7 @@ "diceIsRerolled": "The dice has been rerolled (x{times})", "pendingSaves": "Pending Reaction Rolls", "openSheetSettings": "Open Settings", + "compendiumBrowser": "Compendium Browser", "rulesOn": "Rules On", "rulesOff": "Rules Off", "remainingUses": "Uses refresh on {type}" diff --git a/module/applications/characterCreation/characterCreation.mjs b/module/applications/characterCreation/characterCreation.mjs index 649c7768..d45fe54e 100644 --- a/module/applications/characterCreation/characterCreation.mjs +++ b/module/applications/characterCreation/characterCreation.mjs @@ -1,5 +1,6 @@ import { abilities } from '../../config/actorConfig.mjs'; import { burden } from '../../config/generalConfig.mjs'; +import { ItemBrowser } from '../ui/itemBrowser.mjs'; import { createEmbeddedItemsWithEffects, createEmbeddedItemWithEffects } from '../../helpers/utils.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -42,6 +43,8 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl }; this._dragDrop = this._createDragDropHandlers(); + + this.itemBrowser = null; } get title() { @@ -491,8 +494,24 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl }); } - static async viewCompendium(_, target) { - (await game.packs.get(`daggerheart.${target.dataset.compendium}`))?.render(true); + static async viewCompendium(event, target) { + const type = target.dataset.compendium ?? target.dataset.type; + + const presets = { + compendium: 'daggerheart', + folder: type, + render: { + noFolder: true + } + }; + + if (type == 'domains') + presets.filter = { + 'level.max': { key: 'level.max', value: 1 }, + 'system.domain': { key: 'system.domain', value: this.setup.class?.system.domains ?? null } + }; + + return (this.itemBrowser = await new ItemBrowser({ presets }).render({ force: true })); } static async viewItem(_, target) { @@ -604,6 +623,7 @@ export default class DhCharacterCreation extends HandlebarsApplicationMixin(Appl { overwrite: true } ); + if (this.itemBrowser) this.itemBrowser.close(); this.close(); } diff --git a/module/applications/levelup/levelup.mjs b/module/applications/levelup/levelup.mjs index 1c3f2de7..4cca1194 100644 --- a/module/applications/levelup/levelup.mjs +++ b/module/applications/levelup/levelup.mjs @@ -1,5 +1,6 @@ import { abilities, subclassFeatureLabels } from '../../config/actorConfig.mjs'; import { getDeleteKeys, tagifyElement } from '../../helpers/utils.mjs'; +import { ItemBrowser } from '../ui/itemBrowser.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -11,6 +12,8 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) this._dragDrop = this._createDragDropHandlers(); this.tabGroups.primary = 'advancements'; + + this.itemBrowser = null; } get title() { @@ -533,8 +536,30 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) this.render(); } - static async viewCompendium(_, button) { - (await game.packs.get(`daggerheart.${button.dataset.compendium}`))?.render(true); + static async viewCompendium(event, target) { + const type = target.dataset.compendium ?? target.dataset.type; + + const presets = { + compendium: 'daggerheart', + folder: type, + render: { + noFolder: true + } + }; + + if (type == 'domains') { + const domains = this.actor.system.domains, + multiclassDomain = this.levelup.classUpgradeChoices?.multiclass?.domain; + if (multiclassDomain) { + if (!domains.includes(x => x === multiclassDomain)) domains.push(multiclassDomain); + } + presets.filter = { + 'level.max': { key: 'level.max', value: this.levelup.currentLevel }, + 'system.domain': { key: 'system.domain', value: domains } + }; + } + + return (this.itemBrowser = await new ItemBrowser({ presets }).render({ force: true })); } static async selectPreview(_, button) { @@ -635,6 +660,7 @@ export default class DhlevelUp extends HandlebarsApplicationMixin(ApplicationV2) }, {}); await this.actor.levelUp(levelupData); + if (this.itemBrowser) this.itemBrowser.close(); this.close(); } } diff --git a/module/applications/sheets/actors/character.mjs b/module/applications/sheets/actors/character.mjs index 07046831..55662f90 100644 --- a/module/applications/sheets/actors/character.mjs +++ b/module/applications/sheets/actors/character.mjs @@ -5,6 +5,7 @@ import DhCharacterlevelUp from '../../levelup/characterLevelup.mjs'; import DhCharacterCreation from '../../characterCreation/characterCreation.mjs'; import FilterMenu from '../../ux/filter-menu.mjs'; import { getDocFromElement, getDocFromElementSync } from '../../../helpers/utils.mjs'; +import { ItemBrowser } from '../../ui/itemBrowser.mjs'; /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ @@ -25,7 +26,8 @@ export default class CharacterSheet extends DHBaseActorSheet { toggleEquipItem: CharacterSheet.#toggleEquipItem, toggleResourceDice: CharacterSheet.#toggleResourceDice, handleResourceDice: CharacterSheet.#handleResourceDice, - useDowntime: this.useDowntime + useDowntime: this.useDowntime, + tempBrowser: CharacterSheet.#tempBrowser }, window: { resizable: true @@ -600,7 +602,16 @@ export default class CharacterSheet extends DHBaseActorSheet { */ static async #openPack(_event, button) { const { key } = button.dataset; - game.packs.get(key)?.render(true); + + const presets = { + compendium: 'daggerheart', + folder: key, + render: { + noFolder: true + } + }; + + return new ItemBrowser({ presets }).render({ force: true }); } /** @@ -715,6 +726,13 @@ export default class CharacterSheet extends DHBaseActorSheet { }); } + /** + * Temp + */ + static async #tempBrowser(_, target) { + new ItemBrowser().render({ force: true }); + } + /** * Handle the roll values of resource dice. * @type {ApplicationClickAction} diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 95f091ce..2a02ba01 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -1,5 +1,6 @@ const { HandlebarsApplicationMixin } = foundry.applications.api; import { getDocFromElement, getDocFromElementSync, tagifyElement } from '../../../helpers/utils.mjs'; +import { ItemBrowser } from '../../ui/itemBrowser.mjs'; /** * @typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction @@ -82,7 +83,9 @@ export default function DHApplicationMixin(Base) { toChat: DHSheetV2.#toChat, useItem: DHSheetV2.#useItem, toggleEffect: DHSheetV2.#toggleEffect, - toggleExtended: DHSheetV2.#toggleExtended + toggleExtended: DHSheetV2.#toggleExtended, + addNewItem: DHSheetV2.#addNewItem, + browseItem: DHSheetV2.#browseItem }, contextMenus: [ { @@ -333,19 +336,19 @@ export default function DHApplicationMixin(Base) { callback: async (target, event) => { const doc = await getDocFromElement(target), action = doc?.system?.attack ?? doc; - return action && action.use(event, { byPassRoll: true }) + return action && action.use(event, { byPassRoll: true }); } }); - options.unshift({ - name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', - icon: 'fa-solid fa-burst', - condition: target => { - const doc = getDocFromElementSync(target); - return doc && !(doc.type === 'domainCard' && doc.system.inVault); - }, - callback: async (target, event) => (await getDocFromElement(target)).use(event) - }); + options.unshift({ + name: 'DAGGERHEART.APPLICATIONS.ContextMenu.useItem', + icon: 'fa-solid fa-burst', + condition: target => { + const doc = getDocFromElementSync(target); + return doc && !(doc.type === 'domainCard' && doc.system.inVault); + }, + callback: async (target, event) => (await getDocFromElement(target)).use(event) + }); if (toChat) options.push({ @@ -423,6 +426,68 @@ export default function DHApplicationMixin(Base) { /* Application Clicks Actions */ /* -------------------------------------------- */ + static async #addNewItem(event, target) { + const { type } = target.dataset; + + const createChoice = await foundry.applications.api.DialogV2.wait({ + classes: ['dh-style', 'two-big-buttons'], + buttons: [ + { + action: 'create', + label: 'Create Item', + icon: 'fa-solid fa-plus' + }, + { + action: 'browse', + label: 'Browse Compendium', + icon: 'fa-solid fa-book' + } + ] + }); + + if (!createChoice) return; + + if (createChoice === 'browse') return DHSheetV2.#browseItem.call(this, event, target); + else return DHSheetV2.#createDoc.call(this, event, target); + } + + static async #browseItem(event, target) { + const type = target.dataset.compendium ?? target.dataset.type; + + const presets = {}; + + switch (type) { + case 'loot': + case 'consumable': + case 'armor': + case 'weapon': + presets.compendium = 'daggerheart'; + presets.folder = 'equipments'; + presets.render = { + noFolder: true + }; + presets.filter = { + type: { key: 'type', value: type, forced: true } + }; + break; + case 'domainCard': + presets.compendium = 'daggerheart'; + presets.folder = 'domains'; + presets.render = { + noFolder: true + }; + presets.filter = { + 'level.max': { key: 'level.max', value: this.document.system.levelData.level.current }, + 'system.domain': { key: 'system.domain', value: this.document.system.domains } + }; + break; + default: + return; + } + + return new ItemBrowser({ presets }).render({ force: true }); + } + /** * Create an embedded document. * @type {ApplicationClickAction} diff --git a/module/applications/ui/chatLog.mjs b/module/applications/ui/chatLog.mjs index 6c864d09..fd9ab096 100644 --- a/module/applications/ui/chatLog.mjs +++ b/module/applications/ui/chatLog.mjs @@ -194,11 +194,12 @@ export default class DhpChatLog extends foundry.applications.sidebar.tabs.ChatLo event.stopPropagation(); const item = await foundry.utils.fromUuid(message.system.origin); - const action = item.system.attack?.id === event.currentTarget.id ? item.system.attack : item.system.actions.get(event.currentTarget.id); - if(event.currentTarget.dataset.directDamage) - action.use(event, { byPassRoll: true }) - else - action.use(event); + const action = + item.system.attack?.id === event.currentTarget.id + ? item.system.attack + : item.system.actions.get(event.currentTarget.id); + if (event.currentTarget.dataset.directDamage) action.use(event, { byPassRoll: true }); + else action.use(event); } async actionUseButton(event, message) { diff --git a/module/applications/ui/itemBrowser.mjs b/module/applications/ui/itemBrowser.mjs new file mode 100644 index 00000000..69de5249 --- /dev/null +++ b/module/applications/ui/itemBrowser.mjs @@ -0,0 +1,425 @@ +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +/** + * A UI element which displays the Users defined for this world. + * Currently active users are always displayed, while inactive users can be displayed on toggle. + * + * @extends ApplicationV2 + * @mixes HandlebarsApplication + */ + +export class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(options = {}) { + super(options); + this.items = []; + this.fieldFilter = []; + this.selectedMenu = { path: [], data: null }; + this.config = CONFIG.DH.ITEMBROWSER.compendiumConfig; + this.presets = options.presets; + + if(this.presets?.compendium && this.presets?.folder) + ItemBrowser.selectFolder.call(this, null, null, this.presets.compendium, this.presets.folder); + } + + /** @inheritDoc */ + static DEFAULT_OPTIONS = { + id: 'itemBrowser', + classes: ['daggerheart', 'dh-style', 'dialog', 'compendium-browser'], + tag: 'div', + // title: 'Item Browser', + window: { + frame: true, + title: 'Compendium Browser', + icon: 'fa-solid fa-book-atlas', + positioned: true, + resizable: true + }, + actions: { + selectFolder: this.selectFolder, + expandContent: this.expandContent, + resetFilters: this.resetFilters, + sortList: this.sortList + }, + position: { + top: 330, + left: 120, + width: 800, + height: 600 + } + }; + + /** @override */ + static PARTS = { + sidebar: { + template: 'systems/daggerheart/templates/ui/itemBrowser/sidebar.hbs' + }, + list: { + template: 'systems/daggerheart/templates/ui/itemBrowser/itemBrowser.hbs' + } + }; + + /* -------------------------------------------- */ + /* Filter Tracking */ + /* -------------------------------------------- */ + + /** + * The currently active search filter. + * @type {foundry.applications.ux.SearchFilter} + */ + #search = {}; + + #input = {}; + + /** + * Tracks which item IDs are currently displayed, organized by filter type and section. + * @type {{ + * inventory: { + * search: Set, + * input: Set + * } + * }} + */ + #filteredItems = { + browser: { + search: new Set(), + input: new Set() + } + }; + + /** @inheritDoc */ + async _preFirstRender(context, options) { + if(context.presets?.render?.noFolder || context.presets?.render?.lite) + options.position.width = 600; + + await super._preFirstRender(context, options); + } + + /** @inheritDoc */ + async _preRender(context, options) { + + if(context.presets?.render?.noFolder || context.presets?.render?.lite) + options.parts.splice(options.parts.indexOf('sidebar'), 1); + + await super._preRender(context, options); + } + + /** @inheritDoc */ + async _onRender(context, options) { + await super._onRender(context, options); + + this._createSearchFilter(); + this._createFilterInputs(); + this._createDragProcess(); + + if(context.presets?.render?.lite) + this.element.classList.add('lite'); + + if(context.presets?.render?.noFolder) + this.element.classList.add('no-folder'); + + if(context.presets?.render?.noFilter) + this.element.classList.add('no-filter'); + + if(this.presets?.filter) { + Object.entries(this.presets.filter).forEach(([k,v]) => this.fieldFilter.find(c => c.name === k).value = v.value); + await this._onInputFilterBrowser(); + } + } + + /* -------------------------------------------- */ + /* Rendering */ + /* -------------------------------------------- */ + + /** @override */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + context.compendiums = this.getCompendiumFolders(foundry.utils.deepClone(this.config)); + // context.pathTitle = this.pathTile; + context.menu = this.selectedMenu; + context.formatLabel = this.formatLabel; + context.formatChoices = this.formatChoices; + context.fieldFilter = this.fieldFilter = this._createFieldFilter(); + context.items = this.items; + context.presets = this.presets; + return context; + } + + getCompendiumFolders(config, parent = null, depth = 0) { + let folders = []; + Object.values(config).forEach(c => { + const folder = { + id: c.id, + label: c.label, + selected: (!parent || parent.selected) && this.selectedMenu.path[depth] === c.id + }; + folder.folders = c.folders + ? ItemBrowser.sortBy(this.getCompendiumFolders(c.folders, folder, depth + 2), 'label') + : []; + folders.push(folder); + }); + + return folders; + } + + static async selectFolder(_, target, compend, folder) { + const config = foundry.utils.deepClone(this.config), + compendium = compend ?? target.closest('[data-compendium-id]').dataset.compendiumId, + folderId = folder ?? target.dataset.folderId, + folderPath = `${compendium}.folders.${folderId}`, + folderData = foundry.utils.getProperty(config, folderPath); + + this.selectedMenu = { + path: folderPath.split('.'), + data: { + ...folderData, + columns: ItemBrowser.getFolderConfig(folderData) + } + }; + + let items = []; + for (const key of folderData.keys) { + const comp = game.packs.get(`${compendium}.${key}`); + if (!comp) return; + items = items.concat(await comp.getDocuments({ type__in: folderData.type })); + } + + this.items = ItemBrowser.sortBy(items, 'name'); + this.render({ force: true }); + } + + static expandContent(_, target) { + const parent = target.parentElement; + parent.classList.toggle('expanded'); + } + + static sortBy(data, property) { + return data.sort((a, b) => (a[property] > b[property] ? 1 : -1)); + } + + formatLabel(item, field) { + const property = foundry.utils.getProperty(item, field.key); + if (typeof field.format !== 'function') return property ?? '-'; + return field.format(property); + } + + formatChoices(data) { + if (!data.field.choices) return null; + const config = { + choices: data.field.choices + }; + foundry.data.fields.StringField._prepareChoiceConfig(config); + return config.options.filter( + c => data.filtered.includes(c.value) || data.filtered.includes(c.label.toLowerCase()) + ); + } + + _createFieldFilter() { + const filters = ItemBrowser.getFolderConfig(this.selectedMenu.data, 'filters'); + filters.forEach(f => { + if (typeof f.field === 'string') f.field = foundry.utils.getProperty(game, f.field); + else if (typeof f.choices === 'function') { + f.choices = f.choices(); + } + f.name ??= f.key; + f.value = this.presets?.filter?.[f.name]?.value ?? null; + }); + return filters; + } + + /* -------------------------------------------- */ + /* 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: 'browser', + input: 'input[type="search"].search-input', + content: '[data-application-part="list"] .item-list', + callback: this._onSearchFilterBrowser.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; + } + } + + /* -------------------------------------------- */ + /* Filter Inputs */ + /* -------------------------------------------- */ + + _createFilterInputs() { + const inputs = [ + { + key: 'browser', + container: '[data-application-part="list"] .filter-content .wrapper', + content: '[data-application-part="list"] .item-list', + callback: this._onInputFilterBrowser.bind(this) + } + ]; + + inputs.forEach(m => { + const container = this.element.querySelector(m.container); + if (!container) return (this.#input[m.key] = {}); + const inputs = container.querySelectorAll('input, select'); + inputs.forEach(input => { + input.addEventListener('change', this._onInputFilterBrowser.bind(this)); + }); + this.#filteredItems[m.key].input = new Set(this.items.map(i => i.id)); + this.#input[m.key] = inputs; + }); + } + + /** + * 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 _onSearchFilterBrowser(event, query, rgx, html) { + this.#filteredItems.browser.search.clear(); + + for (const li of html.querySelectorAll('.item-container')) { + const itemUUID = li.dataset.itemUuid, + item = this.items.find(i => i.uuid === itemUUID); + const matchesSearch = !query || foundry.applications.ux.SearchFilter.testQuery(rgx, item.name); + if (matchesSearch) this.#filteredItems.browser.search.add(item.id); + const { input } = this.#filteredItems.browser; + li.hidden = !(input.has(item.id) && matchesSearch); + } + } + + /** + * Callback when filters change + * @param {PointerEvent} event + * @param {HTMLElement} html + */ + async _onInputFilterBrowser(event) { + this.#filteredItems.browser.input.clear(); + + if(event) this.fieldFilter.find(f => f.name === event.target.name).value = event.target.value; + + for (const li of this.element.querySelectorAll('.item-container')) { + const itemUUID = li.dataset.itemUuid, + item = this.items.find(i => i.uuid === itemUUID); + + if(!item) continue; + + const matchesMenu = + this.fieldFilter.length === 0 || + this.fieldFilter.every(f => ( + !f.value && f.value !== false) || + ItemBrowser.evaluateFilter(item, this.createFilterData(f)) + ); + if (matchesMenu) this.#filteredItems.browser.input.add(item.id); + + const { search } = this.#filteredItems.browser; + li.hidden = !(search.has(item.id) && matchesMenu); + } + } + + /** + * Foundry evaluateFilter doesn't allow you to match if filter values are included into item data + * @param {*} obj + * @param {*} filter + */ + static evaluateFilter(obj, filter) { + let docValue = foundry.utils.getProperty(obj, filter.field); + let filterValue = filter.value; + switch (filter.operator) { + case "contains2": + filterValue = Array.isArray(filterValue) ? filterValue : [filterValue]; + docValue = Array.isArray(docValue) ? docValue : [docValue]; + return docValue.some(dv => filterValue.includes(dv)); + case "contains3": + return docValue.some(f => f.value === filterValue); + default: + return foundry.applications.ux.SearchFilter.evaluateFilter(obj, filter); + } + } + + createFilterData(filter) { + return { + field: filter.key, + value: isNaN(filter.value) + ? ['true', 'false'].includes(filter.value) + ? filter.value === 'true' + : filter.value + : Number(filter.value), + operator: filter.operator, + negate: filter.negate + }; + } + + static resetFilters() { + this.render({ force: true }); + } + + static getFolderConfig(folder, property = "columns") { + if(!folder) return []; + return folder[property] ?? CONFIG.DH.ITEMBROWSER.typeConfig[folder.listType]?.[property] ?? []; + } + + static sortList(_, target) { + const key = target.dataset.sortKey, + type = !target.dataset.sortType || target.dataset.sortType === "DESC" ? "ASC" : "DESC", + itemListContainer = target.closest(".compendium-results").querySelector(".item-list"), + itemList = itemListContainer.querySelectorAll(".item-container"); + + target.closest(".item-list-header").querySelectorAll('[data-sort-key]').forEach(b => b.dataset.sortType = ""); + target.dataset.sortType = type; + + const newOrder = [...itemList].reverse().sort((a, b) => { + const aProp = a.querySelector(`[data-item-key="${key}"]`), + bProp = b.querySelector(`[data-item-key="${key}"]`) + if(type === "DESC") { + return aProp.innerText < bProp.innerText ? 1 : -1; + } else { + return aProp.innerText > bProp.innerText ? 1 : -1; + } + }); + + itemListContainer.replaceChildren(...newOrder); + } + + _createDragProcess() { + new foundry.applications.ux.DragDrop.implementation({ + dragSelector: '.item-container', + permissions: { + dragstart: this._canDragStart.bind(this) + }, + callbacks: { + dragstart: this._onDragStart.bind(this) + } + }).bind(this.element); + } + + async _onDragStart(event) { + const { itemUuid } = event.target.closest('[data-item-uuid]').dataset, + item = await foundry.utils.fromUuid(itemUuid), + dragData = item.toDragData(); + event.dataTransfer.setData('text/plain', JSON.stringify(dragData)); + } + + _canDragStart() { + return true; + } +} diff --git a/module/config/_module.mjs b/module/config/_module.mjs index 99069dda..63797607 100644 --- a/module/config/_module.mjs +++ b/module/config/_module.mjs @@ -7,3 +7,4 @@ export * as generalConfig from './generalConfig.mjs'; export * as itemConfig from './itemConfig.mjs'; export * as settingsConfig from './settingsConfig.mjs'; export * as systemConfig from './system.mjs'; +export * as itemBrowserConfig from './itemBrowserConfig.mjs'; diff --git a/module/config/itemBrowserConfig.mjs b/module/config/itemBrowserConfig.mjs new file mode 100644 index 00000000..5d20ae7d --- /dev/null +++ b/module/config/itemBrowserConfig.mjs @@ -0,0 +1,405 @@ +export const typeConfig = { + adversaries: { + columns: [ + { + key: "system.tier", + label: "Tier" + }, + { + key: "system.type", + label: "Type" + } + ], + filters: [ + { + key: "system.tier", + label: "Tier", + field: 'system.api.models.actors.DhAdversary.schema.fields.tier' + }, + { + key: "system.type", + label: "Type", + field: 'system.api.models.actors.DhAdversary.schema.fields.type' + }, + { + key: "system.difficulty", + name: "difficulty.min", + label: "Difficulty (Min)", + field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty', + operator: "gte" + }, + { + key: "system.difficulty", + name: "difficulty.max", + label: "Difficulty (Max)", + field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty', + operator: "lte" + }, + { + key: "system.resources.hitPoints.max", + name: "hp.min", + label: "Hit Points (Min)", + field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max', + operator: "gte" + }, + { + key: "system.resources.hitPoints.max", + name: "hp.max", + label: "Hit Points (Max)", + field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max', + operator: "lte" + }, + { + key: "system.resources.stress.max", + name: "stress.min", + label: "Stress (Min)", + field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max', + operator: "gte" + }, + { + key: "system.resources.stress.max", + name: "stress.max", + label: "Stress (Max)", + field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max', + operator: "lte" + }, + ] + }, + items: { + columns: [ + { + key: "type", + label: "Type" + }, + { + key: "system.secondary", + label: "Subtype", + format: (isSecondary) => isSecondary ? "secondary" : (isSecondary === false ? "primary" : '-') + }, + { + key: "system.tier", + label: "Tier" + } + ], + filters: [ + { + key: "type", + label: "Type", + choices: () => CONFIG.Item.documentClass.TYPES.filter(t => ["armor", "weapon", "consumable", "loot"].includes(t)).map(t => ({ value: t, label: t })) + }, + { + key: "system.secondary", + label: "Subtype", + choices: [ + { value: false, label: "Primary Weapon"}, + { value: true, label: "Secondary Weapon"} + ] + }, + { + key: "system.tier", + label: "Tier", + choices: [{ value: "1", label: "1"}, { value: "2", label: "2"}, { value: "3", label: "3"}, { value: "4", label: "4"}] + }, + { + key: "system.burden", + label: "Burden", + field: 'system.api.models.items.DHWeapon.schema.fields.burden' + }, + { + key: "system.attack.roll.trait", + label: "Trait", + field: 'system.api.models.actions.actionsTypes.attack.schema.fields.roll.fields.trait' + }, + { + key: "system.attack.range", + label: "Range", + field: 'system.api.models.actions.actionsTypes.attack.schema.fields.range' + }, + { + key: "system.baseScore", + name: "armor.min", + label: "Armor Score (Min)", + field: 'system.api.models.items.DHArmor.schema.fields.baseScore', + operator: "gte" + }, + { + key: "system.baseScore", + name: "armor.max", + label: "Armor Score (Max)", + field: 'system.api.models.items.DHArmor.schema.fields.baseScore', + operator: "lte" + }, + { + key: "system.itemFeatures", + label: "Features", + choices: () => [...Object.entries(CONFIG.DH.ITEM.weaponFeatures), ...Object.entries(CONFIG.DH.ITEM.armorFeatures)].map(([k,v]) => ({ value: k, label: v.label})), + operator: "contains3" + } + ] + }, + features: { + columns: [ + + ], + filters: [ + + ] + }, + cards: { + columns: [ + { + key: "system.type", + label: "Type" + }, + { + key: "system.domain", + label: "Domain" + }, + { + key: "system.level", + label: "Level" + } + ], + filters: [ + { + key: "system.type", + label: "Type", + field: 'system.api.models.items.DHDomainCard.schema.fields.type' + }, + { + key: "system.domain", + label: "Domain", + field: 'system.api.models.items.DHDomainCard.schema.fields.domain', + operator: "contains2" + }, + { + key: "system.level", + name: "level.min", + label: "Level (Min)", + field: 'system.api.models.items.DHDomainCard.schema.fields.level', + operator: "gte" + }, + { + key: "system.level", + name: "level.max", + label: "Level (Max)", + field: 'system.api.models.items.DHDomainCard.schema.fields.level', + operator: "lte" + }, + { + key: "system.recallCost", + name: "recall.min", + label: "Recall Cost (Min)", + field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost', + operator: "gte" + }, + { + key: "system.recallCost", + name: "recall.max", + label: "Recall Cost (Max)", + field: 'system.api.models.items.DHDomainCard.schema.fields.recallCost', + operator: "lte" + } + ] + }, + classes: { + columns: [ + { + key: "system.evasion", + label: "Evasion" + }, + { + key: "system.hitPoints", + label: "Hit Points" + }, + { + key: "system.domains", + label: "Domains" + } + ], + filters: [ + { + key: "system.evasion", + name: "evasion.min", + label: "Evasion (Min)", + field: 'system.api.models.items.DHClass.schema.fields.evasion', + operator: "gte" + }, + { + key: "system.evasion", + name: "evasion.max", + label: "Evasion (Max)", + field: 'system.api.models.items.DHClass.schema.fields.evasion', + operator: "lte" + }, + { + key: "system.hitPoints", + name: "hp.min", + label: "Hit Points (Min)", + field: 'system.api.models.items.DHClass.schema.fields.hitPoints', + operator: "gte" + }, + { + key: "system.hitPoints", + name: "hp.max", + label: "Hit Points (Max)", + field: 'system.api.models.items.DHClass.schema.fields.hitPoints', + operator: "lte" + }, + { + key: "system.domains", + label: "Domains", + choices: () => Object.values(CONFIG.DH.DOMAIN.domains).map(d => ({ value: d.id, label: d.label})), + operator: "contains2" + } + ] + }, + subclasses: { + columns: [ + { + key: "id", + label: "Class", + format: (id) => { + return ""; + } + }, + { + key: "system.spellcastingTrait", + label: "Spellcasting Trait" + } + ], + filters: [] + }, + beastforms: { + columns: [ + { + key: "system.tier", + label: "Tier" + }, + { + key: "system.mainTrait", + label: "Main Trait" + } + ], + filters: [ + { + key: "system.tier", + label: "Tier", + field: 'system.api.models.items.DHBeastform.schema.fields.tier' + }, + { + key: "system.mainTrait", + label: "Main Trait", + field: 'system.api.models.items.DHBeastform.schema.fields.mainTrait' + } + ] + } +} + +export const compendiumConfig = { + "daggerheart": { + id: "daggerheart", + label: "DAGGERHEART", + folders: { + "adversaries": { + id: "adversaries", + keys: ["adversaries"], + label: "Adversaries", + type: ["adversary"], + listType: "adversaries" + }, + "ancestries": { + id: "ancestries", + keys: ["ancestries"], + label: "Ancestries", + type: ["ancestry"], + folders: { + "features": { + id: "features", + keys: ["ancestries"], + label: "Features", + type: ["feature"] + } + } + }, + "equipments": { + id: "equipments", + keys: ["armors", "weapons", "consumables", "loot"], + label: "Equipments", + type: ["armor", "weapon", "consumable", "loot"], + listType: "items" + }, + "classes": { + id: "classes", + keys: ["classes"], + label: "Classes", + type: ["class"], + folders: { + "features": { + id: "features", + keys: ["classes"], + label: "Features", + type: ["feature"] + }, + "items": { + id: "items", + keys: ["classes"], + label: "Items", + type: ["armor", "weapon", "consumable", "loot"], + listType: "items" + } + }, + listType: "classes" + }, + "subclasses": { + id: "subclasses", + keys: ["subclasses"], + label: "Subclasses", + type: ["subclass"], + listType: "subclasses" + }, + "domains": { + id: "domains", + keys: ["domains"], + label: "Domain Cards", + type: ["domainCard"], + listType: "cards" + }, + "communities": { + id: "communities", + keys: ["communities"], + label: "Communities", + type: ["community"], + folders: { + "features": { + id: "features", + keys: ["communities"], + label: "Features", + type: ["feature"] + } + } + }, + "environments": { + id: "environments", + keys: ["environments"], + label: "Environments", + type: ["environment"] + }, + "beastforms": { + id: "beastforms", + keys: ["beastforms"], + label: "Beastforms", + type: ["beastform"], + listType: "beastforms", + folders: { + "features": { + id: "features", + keys: ["beastforms"], + label: "Features", + type: ["feature"] + } + } + } + } + } +} \ No newline at end of file diff --git a/module/config/itemConfig.mjs b/module/config/itemConfig.mjs index cb4c5f97..e9d8de4c 100644 --- a/module/config/itemConfig.mjs +++ b/module/config/itemConfig.mjs @@ -380,7 +380,7 @@ export const armorFeatures = { img: 'icons/magic/time/hourglass-brown-orange.webp', cost: [ { - key: 'armorStack', + key: 'armorSlot', value: 1 } ], diff --git a/module/config/system.mjs b/module/config/system.mjs index e72667b1..374fd58c 100644 --- a/module/config/system.mjs +++ b/module/config/system.mjs @@ -6,6 +6,7 @@ import * as SETTINGS from './settingsConfig.mjs'; import * as EFFECTS from './effectConfig.mjs'; import * as ACTIONS from './actionConfig.mjs'; import * as FLAGS from './flagsConfig.mjs'; +import * as ITEMBROWSER from './itemBrowserConfig.mjs' export const SYSTEM_ID = 'daggerheart'; @@ -18,5 +19,6 @@ export const SYSTEM = { SETTINGS, EFFECTS, ACTIONS, - FLAGS + FLAGS, + ITEMBROWSER }; diff --git a/module/data/action/damageAction.mjs b/module/data/action/damageAction.mjs index a5c58feb..88eec481 100644 --- a/module/data/action/damageAction.mjs +++ b/module/data/action/damageAction.mjs @@ -51,7 +51,7 @@ export default class DHDamageAction extends DHBaseAction { dialog: {}, data: this.getRollData(), targetSelection: systemData.targets.length > 0 - } + }; if (this.hasSave) config.onSave = this.save.damageMod; if (data.system) { config.source.message = data._id; diff --git a/module/data/chat-message/adversaryRoll.mjs b/module/data/chat-message/adversaryRoll.mjs index c0c218e3..fa6e48a6 100644 --- a/module/data/chat-message/adversaryRoll.mjs +++ b/module/data/chat-message/adversaryRoll.mjs @@ -1,20 +1,21 @@ const fields = foundry.data.fields; -const targetsField = () => new fields.ArrayField( - new fields.SchemaField({ - id: new fields.StringField({}), - actorId: new fields.StringField({}), - name: new fields.StringField({}), - img: new fields.StringField({}), - difficulty: new fields.NumberField({ integer: true, nullable: true }), - evasion: new fields.NumberField({ integer: true }), - hit: new fields.BooleanField({ initial: false }), - saved: new fields.SchemaField({ - result: new fields.NumberField(), - success: new fields.BooleanField({ nullable: true, initial: null }) +const targetsField = () => + new fields.ArrayField( + new fields.SchemaField({ + id: new fields.StringField({}), + actorId: new fields.StringField({}), + name: new fields.StringField({}), + img: new fields.StringField({}), + difficulty: new fields.NumberField({ integer: true, nullable: true }), + evasion: new fields.NumberField({ integer: true }), + hit: new fields.BooleanField({ initial: false }), + saved: new fields.SchemaField({ + result: new fields.NumberField(), + success: new fields.BooleanField({ nullable: true, initial: null }) + }) }) - }) -) + ); export default class DHActorRoll extends foundry.abstract.TypeDataModel { targetHook = null; @@ -40,27 +41,25 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { action: new fields.StringField() }), damage: new fields.ObjectField(), - costs: new fields.ArrayField( - new fields.ObjectField() - ), + costs: new fields.ArrayField(new fields.ObjectField()), successConsumed: new fields.BooleanField({ initial: false }) }; } get actionActor() { - if(!this.source.actor) return null; + if (!this.source.actor) return null; return fromUuidSync(this.source.actor); } get actionItem() { const actionActor = this.actionActor; - if(!actionActor || !this.source.item) return null; + if (!actionActor || !this.source.item) return null; return actionActor.items.get(this.source.item); } get action() { const actionItem = this.actionItem; - if(!actionItem || !this.source.action) return null; + if (!actionItem || !this.source.action) return null; return actionItem.system.actionsList?.find(a => a.id === this.source.action); } @@ -76,90 +75,85 @@ export default class DHActorRoll extends foundry.abstract.TypeDataModel { this.targetSelection = mode; this.updateTargets(); this.registerTargetHook(); - this.parent.update( - { - system: { - targetSelection: this.targetSelection, - oldTargets: this.oldTargets - } + this.parent.update({ + system: { + targetSelection: this.targetSelection, + oldTargets: this.oldTargets } - ); + }); } get hitTargets() { - return this.currentTargets.filter(t => (t.hit || !this.hasRoll || !this.targetSelection)); + return this.currentTargets.filter(t => t.hit || !this.hasRoll || !this.targetSelection); } async updateTargets() { this.currentTargets = this.getTargetList(); - if(!this.targetSelection) { + if (!this.targetSelection) { this.currentTargets.forEach(ct => { - if(this.targets.find(t => t.actorId === ct.actorId)) return; + if (this.targets.find(t => t.actorId === ct.actorId)) return; const indexTarget = this.oldTargets.findIndex(ot => ot.actorId === ct.actorId); - if(indexTarget === -1) - this.oldTargets.push(ct); + if (indexTarget === -1) this.oldTargets.push(ct); }); - if(this.hasSave) this.setPendingSaves(); - if(this.currentTargets.length) { - if(!this.parent._id) return; - const updates = await this.parent.update( - { - system: { - oldTargets: this.oldTargets - } + if (this.hasSave) this.setPendingSaves(); + if (this.currentTargets.length) { + if (!this.parent._id) return; + const updates = await this.parent.update({ + system: { + oldTargets: this.oldTargets } - ); - if(!updates && ui.chat.collection.get(this.parent.id)) - ui.chat.updateMessage(this.parent); + }); + if (!updates && ui.chat.collection.get(this.parent.id)) ui.chat.updateMessage(this.parent); } } } registerTargetHook() { - if(this.targetSelection && this.targetHook !== null) { - Hooks.off("targetToken", this.targetHook); + if (this.targetSelection && this.targetHook !== null) { + Hooks.off('targetToken', this.targetHook); this.targetHook = null; - } else if(!this.targetSelection && this.targetHook === null) { - this.targetHook = Hooks.on("targetToken", foundry.utils.debounce(this.updateTargets.bind(this), 50)); + } else if (!this.targetSelection && this.targetHook === null) { + this.targetHook = Hooks.on('targetToken', foundry.utils.debounce(this.updateTargets.bind(this), 50)); } } prepareDerivedData() { - if(this.hasTarget) { + if (this.hasTarget) { this.hasHitTarget = this.targets.filter(t => t.hit === true).length > 0; this.updateTargets(); this.registerTargetHook(); - if(this.targetSelection === true) { - this.targetShort = this.targets.reduce((a,c) => { - if(c.hit) a.hit += 1; - else a.miss += 1; - return a; - }, {hit: 0, miss: 0}) + if (this.targetSelection === true) { + this.targetShort = this.targets.reduce( + (a, c) => { + if (c.hit) a.hit += 1; + else a.miss += 1; + return a; + }, + { hit: 0, miss: 0 } + ); } - if(this.hasSave) this.setPendingSaves(); + if (this.hasSave) this.setPendingSaves(); } - + this.canViewSecret = this.parent.speakerActor?.testUserPermission(game.user, 'OBSERVER'); } getTargetList() { return this.targetSelection !== true - ? Array.from(game.user.targets).map(t =>{ - const target = game.system.api.fields.ActionFields.TargetField.formatTarget(t), - oldTarget = this.targets.find(ot => ot.actorId === target.actorId) ?? this.oldTargets.find(ot => ot.actorId === target.actorId); - if(oldTarget) return oldTarget; - return target; - }) + ? Array.from(game.user.targets).map(t => { + const target = game.system.api.fields.ActionFields.TargetField.formatTarget(t), + oldTarget = + this.targets.find(ot => ot.actorId === target.actorId) ?? + this.oldTargets.find(ot => ot.actorId === target.actorId); + if (oldTarget) return oldTarget; + return target; + }) : this.targets; } setPendingSaves() { this.pendingSaves = this.targetSelection - ? this.targets.filter( - target => target.hit && target.saved.success === null - ).length > 0 - : this.currentTargets.filter( - target => target.saved.success === null - ).length > 0; + ? this.targets.filter(target => target.hit && target.saved.success === null).length > 0 + : this.currentTargets.filter(target => target.saved.success === null).length > 0; } } diff --git a/module/data/fields/action/rangeField.mjs b/module/data/fields/action/rangeField.mjs index 2c906edb..221f00af 100644 --- a/module/data/fields/action/rangeField.mjs +++ b/module/data/fields/action/rangeField.mjs @@ -5,7 +5,8 @@ export default class RangeField extends fields.StringField { const options = { choices: CONFIG.DH.GENERAL.range, required: false, - blank: true + blank: true, + label: "DAGGERHEART.GENERAL.range" }; super(options, context); } diff --git a/module/data/fields/action/rollField.mjs b/module/data/fields/action/rollField.mjs index 86681265..a4df2a9e 100644 --- a/module/data/fields/action/rollField.mjs +++ b/module/data/fields/action/rollField.mjs @@ -5,7 +5,7 @@ export class DHActionRollData extends foundry.abstract.DataModel { static defineSchema() { return { type: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.GENERAL.rollTypes }), - trait: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.ACTOR.abilities }), + trait: new fields.StringField({ nullable: true, initial: null, choices: CONFIG.DH.ACTOR.abilities, label: "DAGGERHEART.GENERAL.Trait.single" }), difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true, min: 0 }), bonus: new fields.NumberField({ nullable: true, initial: null, integer: true }), advState: new fields.StringField({ diff --git a/module/data/item/armor.mjs b/module/data/item/armor.mjs index 3aefc86f..01e1d186 100644 --- a/module/data/item/armor.mjs +++ b/module/data/item/armor.mjs @@ -147,4 +147,8 @@ export default class DHArmor extends AttachableItem { const labels = [`${game.i18n.localize('DAGGERHEART.ITEMS.Armor.baseScore')}: ${this.baseScore}`]; return labels; } + + get itemFeatures() { + return this.armorFeatures; + } } diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index 9405be7b..f0b22b44 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -106,6 +106,10 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { return this.actions; } + get itemFeatures() { + return []; + } + /** * Obtain a data object used to evaluate any dice rolls associated with this Item Type * @param {object} [options] - Options which modify the getRollData method. diff --git a/module/data/item/subclass.mjs b/module/data/item/subclass.mjs index 735adb27..ce52fdc6 100644 --- a/module/data/item/subclass.mjs +++ b/module/data/item/subclass.mjs @@ -20,7 +20,8 @@ export default class DHSubclass extends BaseDataItem { choices: CONFIG.DH.ACTOR.abilities, integer: false, nullable: true, - initial: null + initial: null, + label: "DAGGERHEART.ITEMS.Subclass.spellcastingTrait" }), features: new ItemLinkFields(), featureState: new fields.NumberField({ required: true, initial: 1, min: 1 }), diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index c0d88c2c..60a17e3d 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -18,12 +18,12 @@ export default class DHWeapon extends AttachableItem { const fields = foundry.data.fields; return { ...super.defineSchema(), - tier: new fields.NumberField({ required: true, integer: true, initial: 1, min: 1 }), + tier: new fields.NumberField({ required: true, integer: true, initial: 1, min: 1, label: "DAGGERHEART.GENERAL.Tiers.singular" }), equipped: new fields.BooleanField({ initial: false }), //SETTINGS - secondary: new fields.BooleanField({ initial: false }), - burden: new fields.StringField({ required: true, choices: CONFIG.DH.GENERAL.burden, initial: 'oneHanded' }), + secondary: new fields.BooleanField({ initial: false, label: "DAGGERHEART.ITEMS.Weapon.secondaryWeapon" }), + burden: new fields.StringField({ required: true, choices: CONFIG.DH.GENERAL.burden, initial: 'oneHanded', label: "DAGGERHEART.GENERAL.burden" }), weaponFeatures: new fields.ArrayField( new fields.SchemaField({ value: new fields.StringField({ @@ -234,4 +234,8 @@ export default class DHWeapon extends AttachableItem { return labels; } + + get itemFeatures() { + return this.weaponFeatures; + } } diff --git a/module/dice/d20Roll.mjs b/module/dice/d20Roll.mjs index 62dc0d7f..45471532 100644 --- a/module/dice/d20Roll.mjs +++ b/module/dice/d20Roll.mjs @@ -18,9 +18,7 @@ export default class D20Roll extends DHRoll { static DefaultDialog = D20RollDialog; get title() { - return game.i18n.localize( - "DAGGERHEART.GENERAL.d20Roll" - ); + return game.i18n.localize('DAGGERHEART.GENERAL.d20Roll'); } get d20() { @@ -147,7 +145,7 @@ export default class D20Roll extends DHRoll { const difficulty = config.roll.difficulty ?? target.difficulty ?? target.evasion; target.hit = roll.isCritical || roll.total >= difficulty; }); - data.success = config.targets.some(target => target.hit) + data.success = config.targets.some(target => target.hit); } else if (config.roll.difficulty) { data.difficulty = config.roll.difficulty; data.success = roll.isCritical || roll.total >= config.roll.difficulty; diff --git a/module/documents/item.mjs b/module/documents/item.mjs index e1589661..a261677a 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -74,8 +74,8 @@ export default class DHItem extends foundry.documents.Item { isInventoryItem === true ? 'Inventory Items' //TODO localize : isInventoryItem === false - ? 'Character Items' //TODO localize - : 'Other'; //TODO localize + ? 'Character Items' //TODO localize + : 'Other'; //TODO localize return { value: type, label, group }; } @@ -130,7 +130,6 @@ export default class DHItem extends foundry.documents.Item { /* -------------------------------------------- */ - async use(event) { const actions = new Set(this.system.actionsList); if (actions?.size) { @@ -152,10 +151,10 @@ export default class DHItem extends foundry.documents.Item { this.type === 'ancestry' ? game.i18n.localize('DAGGERHEART.UI.Chat.foundationCard.ancestryTitle') : this.type === 'community' - ? game.i18n.localize('DAGGERHEART.UI.Chat.foundationCard.communityTitle') - : this.type === 'feature' - ? game.i18n.localize('TYPES.Item.feature') - : game.i18n.localize('DAGGERHEART.UI.Chat.foundationCard.subclassFeatureTitle'), + ? game.i18n.localize('DAGGERHEART.UI.Chat.foundationCard.communityTitle') + : this.type === 'feature' + ? game.i18n.localize('TYPES.Item.feature') + : game.i18n.localize('DAGGERHEART.UI.Chat.foundationCard.subclassFeatureTitle'), origin: origin, img: this.img, item: { diff --git a/module/helpers/handlebarsHelper.mjs b/module/helpers/handlebarsHelper.mjs index 6d6c2bbc..deb62659 100644 --- a/module/helpers/handlebarsHelper.mjs +++ b/module/helpers/handlebarsHelper.mjs @@ -11,6 +11,7 @@ export default class RegisterHandlebarsHelpers { damageSymbols: this.damageSymbols, rollParsed: this.rollParsed, hasProperty: foundry.utils.hasProperty, + getProperty: foundry.utils.getProperty, setVar: this.setVar, empty: this.empty }); diff --git a/src/packs/items/armors/armor_Dunamis_Silkchain_hAY6UgdGT7dj22Pr.json b/src/packs/items/armors/armor_Dunamis_Silkchain_hAY6UgdGT7dj22Pr.json index 15411b03..9a6b1baa 100644 --- a/src/packs/items/armors/armor_Dunamis_Silkchain_hAY6UgdGT7dj22Pr.json +++ b/src/packs/items/armors/armor_Dunamis_Silkchain_hAY6UgdGT7dj22Pr.json @@ -16,7 +16,7 @@ "img": "icons/magic/time/hourglass-brown-orange.webp", "cost": [ { - "key": "armorStack", + "key": "armorSlot", "value": 1, "keyIsID": false, "scalable": false, diff --git a/src/packs/items/weapons/weapon_Buckler_EmFTp9wzT6MHSaNz.json b/src/packs/items/weapons/weapon_Buckler_EmFTp9wzT6MHSaNz.json index 9232ac7d..852a213f 100644 --- a/src/packs/items/weapons/weapon_Buckler_EmFTp9wzT6MHSaNz.json +++ b/src/packs/items/weapons/weapon_Buckler_EmFTp9wzT6MHSaNz.json @@ -20,7 +20,7 @@ }, "cost": [ { - "key": "armorStack", + "key": "armorSlot", "value": 1, "keyIsID": false, "scalable": false, diff --git a/styles/less/global/dialog.less b/styles/less/global/dialog.less index 8c86e825..aaa5c812 100644 --- a/styles/less/global/dialog.less +++ b/styles/less/global/dialog.less @@ -67,4 +67,30 @@ gap: 5px; } } + + &.two-big-buttons { + .window-content { + padding-top: 0; + + .form-footer { + display: grid; + grid-auto-columns: 1fr; + grid-auto-flow: column; + } + + button[type='submit'] { + gap: 5px; + flex-direction: row; + font-family: @font-body; + font-weight: bold; + font-size: var(--font-size-14); + height: 40px; + white-space: nowrap; + + i { + font-size: var(--font-size-16); + } + } + } + } } diff --git a/styles/less/ui/index.less b/styles/less/ui/index.less index 52c400f9..4a93feb6 100644 --- a/styles/less/ui/index.less +++ b/styles/less/ui/index.less @@ -9,6 +9,7 @@ @import './combat-sidebar/encounter-controls.less'; @import './combat-sidebar/spotlight-control.less'; @import './combat-sidebar/token-actions.less'; +@import './item-browser/item-browser.less'; @import './countdown/countdown.less'; @import './countdown/sheet.less'; diff --git a/styles/less/ui/item-browser/item-browser.less b/styles/less/ui/item-browser/item-browser.less new file mode 100644 index 00000000..3b13056b --- /dev/null +++ b/styles/less/ui/item-browser/item-browser.less @@ -0,0 +1,410 @@ +@import '../../utils/colors.less'; +@import '../../utils/fonts.less'; + +.application.daggerheart.dh-style.compendium-browser { + border: initial; + .window-content { + display: flex; + flex-direction: row; + padding: 0px; + + > div { + overflow: hidden; + display: flex; + flex-direction: column; + gap: 20px; + } + + div[data-application-part='list'] { + flex: 1; + gap: 10px; + } + + .compendium-sidebar { + position: relative; + width: 200px; + padding: 16px; + + &::before { + content: ''; + position: absolute; + right: 0; + background: @golden; + mask-image: linear-gradient(180deg, transparent 0%, black 50%, transparent 100%); + width: 1px; + height: 100%; + } + + .compendium-container { + summary { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + font-family: @font-subtitle; + font-weight: bold; + padding: 2px 12px; + color: light-dark(@dark, @beige); + list-style: none; + cursor: pointer; + + &::marker, // Latest Chrome, Edge, Firefox + &::-webkit-details-marker // Safari + { + display: none; + } + } + + > .folder-list { + padding: 10px; + gap: 0; + + > div { + &.folder-list { + > div { + margin-top: 5px; + } + } + } + } + } + } + + .compendium-results { + padding: 16px; + } + + .menu-path, + option, + select { + text-transform: capitalize; + } + + .menu-path > :first-child { + font-weight: bold; + } + + .menu-path { + display: flex; + align-items: center; + gap: 10px; + + .item-path { + font-family: @font-body; + color: light-dark(@dark, @beige); + + &.path-link { + color: light-dark(@dark, @beige); + } + } + } + + .folder-list, + .item-list-header, + .item-header > div { + gap: 10px; + cursor: pointer; + } + + .item-filter { + display: flex; + align-items: center; + flex-direction: column; + + .wrapper, + .form-group { + display: flex; + flex-direction: column; + align-items: start; + gap: 10px; + } + + .form-group { + white-space: nowrap; + } + + .filter-header { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + + .search-bar { + position: relative; + color: light-dark(@dark-blue-50, @beige-50); + width: 100%; + + input { + border-radius: 50px; + font-family: @font-body; + background: light-dark(@dark-blue-10, @golden-10); + border: none; + outline: 2px solid transparent; + transition: all 0.3s ease; + padding: 0 20px; + width: 100%; + + &: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); + } + } + } + } + + .folder-list { + display: flex; + flex-direction: column; + [data-folder-id] { + padding: 5px 10px; + border: 1px solid transparent; + font-family: @font-body; + transition: all 0.1s ease; + } + + .is-selected, + [data-folder-id]:hover { + font-weight: bold; + border-radius: 3px; + background-color: light-dark(@dark-blue-40, @golden-40); + color: light-dark(@dark-blue, @golden); + } + + .subfolder-list { + margin: 5px 0; + gap: 0; + + .is-selected, + [data-folder-id]:hover { + font-weight: bold; + border-radius: 3px; + background-color: light-dark(@dark-blue-10, @golden-10); + color: light-dark(@dark-blue, @golden); + } + } + } + + .item-list-header, + .item-header { + .item-info { + display: grid; + grid-template-columns: 40px 400px repeat(auto-fit, minmax(100px, 1fr)); + align-items: center; + text-transform: capitalize; + font-family: @font-body; + } + } + + .item-list-header, + .item-list { + overflow-y: auto; + scrollbar-gutter: stable; + scrollbar-width: thin; + scrollbar-color: light-dark(@dark-blue, @golden) transparent; + } + + .item-list-header, + .item-list [data-action='expandContent'] { + display: flex; + + > * { + flex: 1; + } + .item-list-img { + width: 40px; + flex: unset; + } + .item-list-name { + flex-grow: 3 !important; + } + } + + .item-list-header { + align-items: center; + background-color: light-dark(@dark-15, @dark-golden-80); + color: light-dark(@dark-blue, @golden); + border: 1px solid light-dark(@dark-blue, @golden); + border-radius: 3px; + min-height: 30px; + font-family: @font-body; + font-weight: bold; + + > * { + white-space: nowrap; + } + + div[data-sort-key] { + &:after { + font-family: 'Font Awesome 6 Pro'; + margin-left: 5px; + } + + &[data-sort-type='ASC']:after { + content: '\f0d7'; + } + + &[data-sort-type='DESC']:after { + content: '\f0d8'; + } + } + } + + .item-list { + display: flex; + flex-direction: column; + gap: 5px; + + .item-container { + &:hover { + background: light-dark(@dark-blue-10, @golden-10); + } + } + + .item-desc .wrapper { + padding: 0 10px; + display: flex; + flex-direction: column; + gap: 5px; + font-family: @font-body; + + h1 { + font-size: 32px; + } + h2 { + font-size: 28px; + font-weight: 600; + } + h3 { + font-size: 20px; + font-weight: 600; + } + h4 { + font-size: 16px; + color: @beige; + font-weight: 600; + } + + ul, + ol { + margin: 1rem 0; + padding: 0 0 0 1.25rem; + + li { + font-family: @font-body; + margin-bottom: 0.25rem; + } + } + + ul { + list-style: disc; + } + } + + img { + border-radius: 5px; + } + } + + .filter-content { + padding: 0px; + } + + .filter-content, + .item-desc { + display: grid; + grid-template-rows: 0fr; + transition: all 0.3s ease-in-out; + .wrapper { + overflow: hidden; + display: grid; + grid-template-columns: repeat(4, 1fr); + padding: 0; + + .form-group { + label { + flex: 1; + font-family: @font-body; + } + .form-fields { + width: 100%; + flex: 2; + + input[type='number'] { + text-align: center; + color: light-dark(@dark, @beige); + } + } + } + } + } + + .expanded + .extensible { + grid-template-rows: 1fr; + padding-top: 10px; + } + + .welcome-message { + display: flex; + flex-direction: column; + gap: 5px; + align-items: center; + justify-content: center; + height: -webkit-fill-available; + margin: 0; + + .title { + font-family: @font-subtitle; + margin: 0; + text-align: center; + } + .hint { + font-family: @font-body; + } + } + + [disabled] { + opacity: 0.5; + pointer-events: none; + } + } + + &.lite, + &.no-folder { + .menu-path { + display: none; + } + } + + &.lite { + .filter-header { + display: none; + } + } + + &.no-filter { + .filter-header { + a[data-action='expandContent'] { + display: none; + } + } + } +} diff --git a/templates/sheets/actors/character/header.hbs b/templates/sheets/actors/character/header.hbs index 4ab817c2..b6f51bcf 100644 --- a/templates/sheets/actors/character/header.hbs +++ b/templates/sheets/actors/character/header.hbs @@ -31,25 +31,25 @@ {{#if document.system.class.value}} {{document.system.class.value.name}} {{else}} - {{localize 'TYPES.Item.class'}} + {{localize 'TYPES.Item.class'}} {{/if}} {{#if document.system.class.subclass}} {{document.system.class.subclass.name}} {{else}} - {{localize 'TYPES.Item.subclass'}} + {{localize 'TYPES.Item.subclass'}} {{/if}} {{#if document.system.community}} {{document.system.community.name}} {{else}} - {{localize 'TYPES.Item.community'}} + {{localize 'TYPES.Item.community'}} {{/if}} {{#if document.system.ancestry}} {{document.system.ancestry.name}} {{else}} - {{localize 'TYPES.Item.ancestry'}} + {{localize 'TYPES.Item.ancestry'}} {{/if}} @@ -58,13 +58,13 @@ {{#if document.system.multiclass.value}} {{document.system.multiclass.value.name}} {{else}} - {{localize 'DAGGERHEART.GENERAL.multiclass'}} + {{localize 'DAGGERHEART.GENERAL.multiclass'}} {{/if}} {{#if document.system.multiclass.subclass}} {{document.system.multiclass.subclass.name}} {{else}} - {{localize 'TYPES.Item.subclass'}} + {{localize 'TYPES.Item.subclass'}} {{/if}} {{/if}} diff --git a/templates/sheets/actors/character/inventory.hbs b/templates/sheets/actors/character/inventory.hbs index 017d37d9..ee5b6034 100644 --- a/templates/sheets/actors/character/inventory.hbs +++ b/templates/sheets/actors/character/inventory.hbs @@ -10,6 +10,9 @@ + + +
diff --git a/templates/sheets/global/partials/inventory-fieldset-items-V2.hbs b/templates/sheets/global/partials/inventory-fieldset-items-V2.hbs index cc030523..8fbd5800 100644 --- a/templates/sheets/global/partials/inventory-fieldset-items-V2.hbs +++ b/templates/sheets/global/partials/inventory-fieldset-items-V2.hbs @@ -26,7 +26,7 @@ Parameters: {{localize title}} {{#if canCreate}} - {{localize tabs.settings.label}} {{localize "DAGGERHEART.ITEMS.Subclass.spellcastingTrait"}} - {{formField systemFields.spellcastingTrait value=source.system.spellcastingTrait localize=true}} + {{formInput systemFields.spellcastingTrait value=source.system.spellcastingTrait localize=true}} \ No newline at end of file diff --git a/templates/sheets/items/weapon/settings.hbs b/templates/sheets/items/weapon/settings.hbs index fd8f16d9..021fc627 100644 --- a/templates/sheets/items/weapon/settings.hbs +++ b/templates/sheets/items/weapon/settings.hbs @@ -6,15 +6,15 @@
{{localize tabs.settings.label}} {{localize "DAGGERHEART.GENERAL.Tiers.singular"}} - {{formField systemFields.tier value=source.system.tier}} + {{formInput systemFields.tier value=source.system.tier}} {{localize "DAGGERHEART.ITEMS.Weapon.secondaryWeapon"}} - {{formField systemFields.secondary value=source.system.secondary}} + {{formInput systemFields.secondary value=source.system.secondary}} {{localize "DAGGERHEART.GENERAL.Trait.single"}} {{formInput systemFields.attack.fields.roll.fields.trait value=document.system.attack.roll.trait name="system.attack.roll.trait" label="DAGGERHEART.GENERAL.Trait.single" localize=true}} {{localize "DAGGERHEART.GENERAL.range"}} {{formInput systemFields.attack.fields.range value=document.system.attack.range label="Range" name="system.attack.range" localize=true}} {{localize "DAGGERHEART.GENERAL.burden"}} - {{formField systemFields.burden value=source.system.burden localize=true}} + {{formInput systemFields.burden value=source.system.burden localize=true}}
diff --git a/templates/ui/itemBrowser/itemBrowser.hbs b/templates/ui/itemBrowser/itemBrowser.hbs new file mode 100644 index 00000000..65c2121e --- /dev/null +++ b/templates/ui/itemBrowser/itemBrowser.hbs @@ -0,0 +1,89 @@ +
+ {{#if menu.data }} + +
+ +
+
+ {{#each fieldFilter}} + {{#if choices }} +
+ +
+ +
+
+ {{else}} + {{#if filtered }} + {{formField field localize=true blank="" name=name choices=(@root.formatChoices this) valueAttr="value" dataset=(object key=key) value=value}} + {{else}} + {{#if field.label}} + {{formField field localize=true blank="" name=name dataset=(object key=key) value=value}} + {{else}} + {{formField field localize=true blank="" name=name dataset=(object key=key) label=label value=value}} + {{/if}} + {{/if}} + {{/if}} + {{/each}} +
+
+
+ {{!--
--}} + {{#if menu.data.columns.length}} +
+
+
Name
+ {{#each menu.data.columns}} +
{{label}}
+ {{/each}} +
+ {{/if}} +
+ {{#each items}} +
+
+
+ +
{{name}}
+ {{#each ../menu.data.columns}} +
{{#with (@root.formatLabel ../this this) as | label |}}{{{label}}}{{/with}}
+ {{/each}} +
+
+
+
{{{system.description}}}
+
+
+ {{/each}} +
+ {{!--
--}} + {{else}} +
+

Daggerheart Compendium Browser

+ Select a Folder in sidebar to start browsing trought the compendium +
+ {{/if}} +
\ No newline at end of file diff --git a/templates/ui/itemBrowser/sidebar.hbs b/templates/ui/itemBrowser/sidebar.hbs new file mode 100644 index 00000000..6c395a4b --- /dev/null +++ b/templates/ui/itemBrowser/sidebar.hbs @@ -0,0 +1,28 @@ +
+ {{#each compendiums}} +
+ + {{label}} + + +
+ {{#each folders}} +
{{label}}
+ {{!--
{{label}}
--}} +
+ {{#each folders}} +
+ • {{label}} +
+ {{/each}} +
+ {{/each}} +
+ +
+ {{/each}} +