Item Browser v0.5

This commit is contained in:
Dapoolp 2025-08-05 01:15:16 +02:00
parent 8cfa88b1e3
commit 845d72e20c
16 changed files with 880 additions and 30 deletions

View file

@ -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
@ -707,6 +709,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}

View file

@ -9,49 +9,350 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
* @mixes HandlebarsApplication
*/
export default class ItemBrowser extends HandlebarsApplicationMixin(ApplicationV2) {
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;
}
/** @inheritDoc */
static DEFAULT_OPTIONS = {
id: 'itemBrowser',
classes: [],
classes: ['dh-style'],
tag: 'div',
// window: {
// frame: true,
// title: 'Item Browser',
// positioned: true,
// resizable: true
// },
actions: {
// setFear: FearTracker.setFear,
// increaseFear: FearTracker.increaseFear
// title: 'Item Browser',
window: {
frame: true,
title: 'Item Browser',
positioned: true,
resizable: true
},
/* position: {
width: 222,
height: 222
actions: {
selectFolder: this.selectFolder,
expandContent: this.expandContent,
resetFilters: this.resetFilters
},
position: {
width: 1000,
height: 800
// top: "200px",
// left: "120px"
} */
}
};
/** @override */
static PARTS = {
resources: {
root: true,
template: 'systems/daggerheart/templates/ui/itemBrowser.hbs'
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<string>,
* input: Set<string>
* }
* }}
*/
#filteredItems = {
browser: {
search: new Set(),
input: new Set()
}
};
/** @inheritDoc */
async _onRender(context, options) {
await super._onRender(context, options);
this._createSearchFilter();
this._createFilterInputs();
this._createDragProcess();
}
/* -------------------------------------------- */
/* Rendering */
/* -------------------------------------------- */
/** @override */
async _prepareContext(options) {
const data = await super._prepareContext(options);
return data;
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.selectedMenu.data?.filters ? this._createFieldFilter() : [];
context.items = this.items;
console.log(this.items)
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') : [];
// sortBy(Object.values(c.folders), 'label')
folders.push(folder)
})
// console.log(folders)
return folders;
}
static async selectFolder(_, target) {
const config = foundry.utils.deepClone(this.config),
compendium = target.closest('[data-compendium-id]').dataset.compendiumId,
folderId = target.dataset.folderId,
folderPath = `${compendium}.folders.${folderId}`,
folderData = foundry.utils.getProperty(config, folderPath);
this.selectedMenu = {
path: folderPath.split('.'),
data: 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 = [];
this.selectedMenu.data.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();
filters.push(f)
})
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-browser',
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),
// target: '.filter-button',
// filters: FilterMenu.invetoryFilters
}
];
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);
// li.hidden = !(matchesSearch);
}
}
/**
* Callback when filters change
* @param {PointerEvent} event
* @param {HTMLElement} html
*/
async _onInputFilterBrowser(event) {
this.#filteredItems.browser.input.clear();
console.log(event.target.name)
this.fieldFilter.find(f => f.key === event.target.name).value = event.target.value;
// console.log(_event, html, filters)
for (const li of event.target.closest('[data-application-part="list"]').querySelectorAll('.item-container')) {
const itemUUID = li.dataset.itemUuid,
item = this.items.find(i => i.uuid === itemUUID);
const matchesMenu =
this.fieldFilter.length === 0 || this.fieldFilter.every(f => {
return (!f.value && f.value !== false) || foundry.applications.ux.SearchFilter.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);
// li.hidden = !(matchesMenu);
}
}
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 });
}
/**
* Serialize salient information about this Document when dragging it.
* @returns {object} An object of drag data.
*/
// toDragData() {
// const dragData = {type: this.documentName};
// if ( this.id ) dragData.uuid = this.uuid;
// else dragData.data = this.toObject();
// return dragData;
// }
_createDragProcess() {
new foundry.applications.ux.DragDrop.implementation({
dragSelector: ".item-container",
// dropSelector: ".directory-list",
permissions: {
dragstart: this._canDragStart.bind(this),
// drop: this._canDragDrop.bind(this)
},
callbacks: {
// dragover: this._onDragOver.bind(this),
dragstart: this._onDragStart.bind(this),
// drop: this._onDrop.bind(this)
}
}).bind(this.element);
// this.element.querySelectorAll(".directory-item.folder").forEach(folder => {
// folder.addEventListener("dragenter", this._onDragHighlight.bind(this));
// folder.addEventListener("dragleave", this._onDragHighlight.bind(this));
// });
}
async _onDragStart(event) {
// console.log(event)
// ui.context?.close({ animate: false });
const { itemUuid } = event.target.closest('[data-item-uuid]').dataset;
const dragData = foundry.utils.fromUuidSync(itemUuid).toDragData();
// console.log(dragData)
// const dragData = { UUID: itemUuid };
event.dataTransfer.setData("text/plain", JSON.stringify(dragData));
}
_canDragStart() {
return true;
}
}

View file

@ -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';

View file

@ -0,0 +1,231 @@
export const compendiumConfig = {
"daggerheart": {
id: "daggerheart",
label: "DAGGERHEART",
folders: {
"adversaries": {
id: "adversaries",
keys: ["adversaries"],
label: "Adversaries",
type: ["adversary"],
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",
label: "Difficulty (Min)",
field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty',
operator: "gte"
},
{
key: "system.difficulty",
label: "Difficulty (Max)",
field: 'system.api.models.actors.DhAdversary.schema.fields.difficulty',
operator: "lte"
},
{
key: "system.resources.hitPoints.max",
label: "Hit Points (Min)",
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.hitPoints.fields.max',
operator: "gte"
},
{
key: "system.resources.hitPoints.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",
label: "Stress (Min)",
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max',
operator: "gte"
},
{
key: "system.resources.stress.max",
label: "Stress (Max)",
field: 'system.api.models.actors.DhAdversary.schema.fields.resources.fields.stress.fields.max',
operator: "lte"
},
]
},
"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"],
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",
// filtered: ["armor", "weapon", "consumable", "loot"],
// field: 'system.api.documents.DHItem.schema.fields.type',
// valueAttr: 'label'
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",
label: "Armor Score (Min)",
field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
operator: "gte"
},
{
key: "system.baseScore",
label: "Armor Score (Max)",
field: 'system.api.models.items.DHArmor.schema.fields.baseScore',
operator: "lte"
}
]
},
"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"]
}
}
},
"subclasses": {
id: "subclasses",
keys: ["subclasses"],
label: "Subclasses",
type: ["subclass"],
folders: {
"features": {
id: "features",
keys: ["subclasses"],
label: "Features",
type: ["feature"]
}
}
},
"domains": {
id: "domains",
keys: ["domains"],
label: "Domain Cards",
type: ["domainCard"]
},
"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"],
folders: {
"features": {
id: "features",
keys: ["beastforms"],
label: "Features",
type: ["feature"]
}
}
}
}
}
}

View file

@ -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
};

View file

@ -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);
}

View file

@ -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({

View file

@ -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({

View file

@ -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
});