diff --git a/README.md b/README.md index ea95030..960e9d0 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,30 @@ # Compendium Browser -> This version only works for Foundry v0.4.4 and up. To use the previous version please visit https://github.com/syl3r86/compendium-browser/tree/pre-0.4.4 +Tired of scrolling compendia? Easily brows and filter for spells, feats, items, and NPCs using Compendium Browser. -A module to easily browse and filter spells as well as npcs loaded from compendie. +**NEW** Compendium Browser is faster and better-behaved; **it no longer loads all the compendia into memory on start-up** (which sometimes hung servers because of memory or CPU requirements). Instead, it filters and loads on-demand, as well as giving you a Module Setting to control how many rows are loaded at a time. + +## Summary +* **Authors**: Discord: Spetzel#0103; Felix (felix.mueller.86@web.de) +* **Version**: 0.4.2 +* **Foundry VTT Compatibility**: 0.7.2-0.7.9 +* **System Compatibility (If applicable)**: dnd5e +* **Translation Support**: en ## Installation -1. Copy this link and use it in Foundrys Module Manager to install the Module +1. Go to the Add-on Modules tab in Foundry Setup +2. Click Install Module and search for **Compendium Browser** OR paste this link: `https://github.com/League-of-Foundry-Developers/compendium-browser/releases/download/latest/module.json` +3. Open your world and go to Settings>Manage Modules and enable Compendium Browser - > https://raw.githubusercontent.com/syl3r86/compendium-browser/master/module.json - -2. Enable the Module in your Worlds Module Settings ![example](preview.jpg) ## Details -Only the Gamemaster has access to the Settings, where they can enable or disable player access to the spell or npc-browser. It is **highly** recommended to disable any compendie that do not contain spell or should not be used in the NPC Browser. This reduces the initial loading time, that happens after a client connects to foundry, immensly. +Only the Gamemaster has access to the Settings, where they can enable or disable player access to the spell or npc-browser. It is **highly** recommended to disable any compendia that do not contain spell or should not be used in the NPC Browser. . This application enables anyone to add their own custom spell or npc filters via the api. After initialization the app can be found under game.compendiumBrowser where either addSpellFilter or addNpcFilter can be used to add a filter. This does support any data that the spell or npc has, including flags. -All filters featured in the app are included in this manor and can be found in the compendium-browser.js at around line 726. +All filters featured in the app are included in this manner and can be found in the compendium-browser.js at around line 726. ## Contribution If you feel like supporting my work, feel free to leave a tip at my paypal felix.mueller.86@web.de diff --git a/compendium-browser.css b/compendium-browser.css index f9e8f8f..96021bc 100644 --- a/compendium-browser.css +++ b/compendium-browser.css @@ -53,12 +53,22 @@ position: sticky; display: block; min-width: 250px; - max-width: 400px; - width: 300px; + max-width: 45%; + width: 350px; height: 100%; padding-right: 5px; overflow: scroll; } +.compendium-browser .list-area { + position: sticky; + display: flex; + min-width: 250px; + max-width: 55%; + width: 400px; + height: 100%; + padding-right: 5px; + overflow-y: scroll; +} .compendium-browser .control-area button { background: rgba(0, 0, 0, 0.05); border: 1px solid #bbb; @@ -129,7 +139,6 @@ float: right; display: block; min-width: 335px; - width: 785px; margin: 0; height: 100%; overflow: auto; diff --git a/compendium-browser.js b/compendium-browser.js index 9c46b2a..9807685 100644 --- a/compendium-browser.js +++ b/compendium-browser.js @@ -1,32 +1,89 @@ -/** +/* eslint-disable valid-jsdoc */ +/* eslint-disable complexity */ +/** * @author Felix Müller aka syl3r86 * @version 0.2.0 */ +/** @author Jeffrey Pugh aka @spetzel2020 + * @version 0.4.0 + */ +/* +4-Feb-2020 0.4.0 Switch to not pre-loading the indexes, and instead do that at browsing time, to reduce server load and memory usage + Refactor some of the eslint warnings +5-Feb-2021 Don't do memory allocation - just browse compendia in real-time + After this, next step would be incremental (lazy) loading +7-Feb-2021 0.4.1 Move load back to "ready" hook, but limit number loaded +8-Feb-2021 0.4.1 Bug fix: initialize() was setting this.spells, not this.items so CB was using twice the memory (once loaded incorrectly into this.spells + and once loaded on first getData() into this.items) + 0.4.1b SpellBrowser -> CompendiumBrowser +9-Feb-2021 0.4.1b Call loadAndFilterItems instead of loadItems; filter as we go, limited by numToPreload + 0.4.1c Needed to pass specific spellFilters, itemFilters etc. + 0.4.1d: Fixed img observer on replaced spellData +11-Feb-2021 0.4.1e: Don't save the filter data (which is most of the memory) and remove the preload limit; instead just save the minimal amount of data + 0.4.1g: Generalize the spell list reload and confirm spells still working + 0.4.1h: Add the partials for npc, feat, item and the backing code +12-Feb-2021 0.4.1j: Correct compactItem for feats and items required display items + Rename itemType -> browserTab to differentiate candidate item's type from the tab it appears on (spell, feat/class, item, NPC) + Fixed: Was calling the wrong sort for feat and NPC + 0.4.1k: Don't call loadItems() during initalize; getData() just displays static elements + 0.4.1l: Display progress indicator for loading - for now just a static one +15-Feb-2021 0.4.2: Fix NPCs to use loadAndFilterNpcs + 0.4.2b: Add Loading... message for NPCs + 0.4.2c: Correct Loading... message on initial tab, but not on tab switch + 0.4.2d: Display the type of item being loaded +16-Dec-2021 0.4.2f: Change preload to maxLoaded and display a message to filter if you want more +10-Mar-2021 0.4.3: activateItemListListeners(): Remove spurious li.parents (wasn't being used anyway) +11-Mar-2021 0.4.3 Fixed: Reset Filters doesn't clear the on-screen filter fields (because it is not completely re-rendering like it used to) Issue #4 + Hack solution is to re-render whole dialog which unfortunately loses filter settings on other tabs as well + 0.4.3b: Clear all filters to match displayed +*/ -class SpellBrowser extends Application { - - async initializeContent() { +const CMPBrowser = { + MODULE_NAME : "compendium-browser", + MODULE_VERSION : "0.4.3", + MAXLOAD : 500, //Default for the maximum number to load before displaying a message that you need to filter to see more +} + +class CompendiumBrowser extends Application { + + static get defaultOptions() { + const options = super.defaultOptions; + mergeObject(options, { + title: "CMPBrowser.compendiumBrowser", + tabs: [{navSelector: ".tabs", contentSelector: ".content", initial: "spell"}], + classes: options.classes.concat('compendium-browser'), + template: "modules/compendium-browser/template/template.html", + width: 800, + height: 700, + resizable: true, + minimizable: true + }); + return options; + } + + async initialize() { // load settings if (this.settings === undefined) { this.initSettings(); - } - this.loadItems().then(obj => { - this.spells = obj; - }); - this.loadNpcs().then(obj => { - this.npcs = obj; - }); + } + await loadTemplates([ "modules/compendium-browser/template/spell-browser.html", + "modules/compendium-browser/template/spell-browser-list.html", "modules/compendium-browser/template/npc-browser.html", + "modules/compendium-browser/template/npc-browser-list.html", "modules/compendium-browser/template/feat-browser.html", + "modules/compendium-browser/template/feat-browser-list.html", "modules/compendium-browser/template/item-browser.html", + "modules/compendium-browser/template/item-browser-list.html", "modules/compendium-browser/template/filter-container.html", - "modules/compendium-browser/template/settings.html" + "modules/compendium-browser/template/settings.html", + "modules/compendium-browser/template/loading.html" ]); this.hookCompendiumList(); - + + //Reset the filters used in the dialog this.spellFilters = { registeredFilterCategorys: {}, activeFilters: {} @@ -45,74 +102,339 @@ class SpellBrowser extends Application { }; } - static get defaultOptions() { - const options = super.defaultOptions; - mergeObject(options, { - tabs: [{ navSelector: ".tabs", contentSelector: ".content", initial: "spell" }], - classes: options.classes.concat('compendium-browser'), - template: "modules/compendium-browser/template/template.html", - width: 800, - height: 700, - resizable: true, - minimizable: true, - title: "Compendium Browser" - }); - return options; + + /** override */ + _onChangeTab(event, tabs, active) { + super._onChangeTab(event, tabs, active); + const html = this.element; + this.replaceList(html, active, {reload : false}) } - hookCompendiumList() { - Hooks.on('renderCompendiumDirectory', (app, html, data) => { - this.hookCompendiumList(); - }); - let html = $('#compendium'); - if (this.settings === undefined) { - this.initSettings(); - } - if (game.user.isGM || this.settings.allowSpellBrowser || this.settings.allowNpcBrowser) { - const importButton = $(``); - html.find('.compendium-browser-btn').remove(); + /** override */ + async getData() { - // adding to directory-list since the footer doesn't exist if the user is not gm - html.find('.directory-footer').append(importButton); + //0.4.1 Filter as we load to support new way of filtering + //Previously loaded all data and filtered in place; now loads minimal (preload) amount, filtered as we go + //First time (when you press Compendium Browser button) is called with filters unset + + //0.4.1k: Don't do any item/npc loading until tab is visible + let data = { + items : [], + npcs: [], + spellFilters : this.spellFilters, + showSpellBrowser : (game.user.isGM || this.settings.allowSpellBrowser), + featFilters : this.featFilters, + showFeatBrowser : (game.user.isGM || this.settings.allowFeatBrowser), + itemFilters : this.itemFilters, + showItemBrowser : (game.user.isGM || this.settings.allowItemBrowser), + npcFilters : this.npcFilters, + showNpcBrowser : (game.user.isGM || this.settings.allowNpcBrowser), + settings : this.settings, + isGM : game.user.isGM + }; - // Handle button clicks - importButton.click(ev => { - ev.preventDefault(); - this.render(true); - }); - } - } - async getData() { - if (!this.spellsLoaded) { - // spells will be stored locally to not require full loading each time the browser is opened - this.items = await this.loadItems(); - this.spellsLoaded = true; - } - - let data = {}; - data.spells = this.items.spells; - data.spellFilters = this.spellFilters; - data.showSpellBrowser = (game.user.isGM || this.settings.allowSpellBrowser); - data.feats = this.items.feats; - data.featFilters = this.featFilters; - data.showFeatBrowser = (game.user.isGM || this.settings.allowFeatBrowser); - data.items = this.items.items; - data.itemFilters = this.itemFilters; - data.showItemBrowser = (game.user.isGM || this.settings.allowItemBrowser); - data.npcs = this.npcs; - data.npcFilters = this.npcFilters; - data.showNpcBrowser = (game.user.isGM || this.settings.allowNpcBrowser); - data.settings = this.settings; - data.isGM = game.user.isGM; return data; } - async loadItems() { - console.log('Spell Browser | Started loading items'); + activateItemListListeners(html) { + // show entity sheet + html.find('.item-edit').click(ev => { + let itemId = $(ev.currentTarget).parents("li").attr("data-entry-id"); + let compendium = $(ev.currentTarget).parents("li").attr("data-entry-compendium"); + let pack = game.packs.find(p => p.collection === compendium); + pack.getEntity(itemId).then(entity => { + entity.sheet.render(true); + }); + }); - if (this.classList === undefined) { + // make draggable + //0.4.1: Avoid the game.packs lookup + html.find('.draggable').each((i, li) => { + li.setAttribute("draggable", true); + li.addEventListener('dragstart', event => { + let packName = li.getAttribute("data-entry-compendium"); + let pack = game.packs.find(p => p.collection === packName); + if (!pack) { + event.preventDefault(); + return false; + } + event.dataTransfer.setData("text/plain", JSON.stringify({ + type: pack.entity, + pack: pack.collection, + id: li.getAttribute("data-entry-id") + })); + }, false); + }); + } + + /** override */ + activateListeners(html) { + super.activateListeners(html); + + this.observer = new IntersectionObserver((entries, observer) => { + for (let e of entries) { + if (!e.isIntersecting) continue; + const img = e.target; + // Avatar image + //const img = li.querySelector("img"); + if (img && img.dataset.src) { + img.src = img.dataset.src; + delete img.dataset.src; + } + + // No longer observe the target + observer.unobserve(e.target); + } + }); + + this.activateItemListListeners(html); + + // toggle visibility of filter containers + html.find('.filtercontainer h3, .multiselect label').click(async ev => { + await $(ev.target.nextElementSibling).toggle(100); + + }); + html.find('.multiselect label').trigger('click'); + + // sort spell list + html.find('.spell-browser select[name=sortorder]').on('change', ev => { + let spellList = html.find('.spell-browser li'); + let byName = (ev.target.value == 'true'); + let sortedList = this.sortSpells(spellList, byName); + let ol = $(html.find('.spell-browser ul')); + ol[0].innerHTML = []; + for (let element of sortedList) { + ol[0].append(element); + } + }); + this.triggerSort(html, "spell"); + + // sort feat list in place + html.find('.feat-browser select[name=sortorder]').on('change', ev => { + let featList = html.find('.feat-browser li'); + let byName = (ev.target.value == 'true'); + let sortedList = this.sortFeats(featList, byName); + let ol = $(html.find('.feat-browser ul')); + ol[0].innerHTML = []; + for (let element of sortedList) { + ol[0].append(element); + } + }); + this.triggerSort(html, "feat"); + + // sort item list in place + html.find('.item-browser select[name=sortorder]').on('change', ev => { + let itemList = html.find('.item-browser li'); + let byName = (ev.target.value == 'true'); + let sortedList = this.sortItems(itemList, byName); + let ol = $(html.find('.item-browser ul')); + ol[0].innerHTML = []; + for (let element of sortedList) { + ol[0].append(element); + } + }); + this.triggerSort(html, "item"); + + // sort npc list in place + html.find('.npc-browser select[name=sortorder]').on('change', ev => { + let npcList = html.find('.npc-browser li'); + let orderBy = ev.target.value; + let sortedList = this.sortNpcs(npcList, orderBy); + let ol = $(html.find('.npc-browser ul')); + ol[0].innerHTML = []; + for (let element of sortedList) { + ol[0].append(element); + } + }); + this.triggerSort(html, "npc"); + + // reset filters and re-render + //0.4.3: Reset ALL filters because when we do a re-render it affects all tabs + html.find('#reset-spell-filter').click(ev => { + this.resetFilters(); + //v0.4.3: Re-render so that we display the filters correctly + this.refreshList = "spell"; + this.render(); + }); + + html.find('#reset-feat-filter').click(ev => { + this.resetFilters(); + //v0.4.3: Re-render so that we display the filters correctly + this.refreshList = "feat"; + this.render(); + }); + + html.find('#reset-item-filter').click(ev => { + this.resetFilters(); + //v0.4.3: Re-render so that we display the filters correctly + this.refreshList = "item"; + this.render(); + + }); + + html.find('#reset-npc-filter').click(ev => { + this.resetFilters(); + //v0.4.3: Re-render so that we display the filters correctly + this.refreshList = "npc"; + this.render(); + }); + + // settings + html.find('.settings input').on('change', ev => { + let setting = ev.target.dataset.setting; + let value = ev.target.checked; + if (setting === 'spell-compendium-setting') { + let key = ev.target.dataset.key; + this.settings.loadedSpellCompendium[key].load = value; + this.render(); + ui.notifications.info("Settings Saved. Item Compendiums are being reloaded."); + } else if (setting === 'npc-compendium-setting') { + let key = ev.target.dataset.key; + this.settings.loadedNpcCompendium[key].load = value; + this.render(); + ui.notifications.info("Settings Saved. NPC Compendiums are being reloaded."); + } + if (setting === 'allow-spell-browser') { + this.settings.allowSpellBrowser = value; + } + if (setting === 'allow-feat-browser') { + this.settings.allowFeatBrowser = value; + } + if (setting === 'allow-item-browser') { + this.settings.allowItemBrowser = value; + } + if (setting === 'allow-npc-browser') { + this.settings.allowNpcBrowser = value; + } + this.saveSettings(); + }); + + + // activating or deactivating filters + //0.4.1: Now does a re-load and updates just the data side + // text filters + html.find('.filter[data-type=text] input, .filter[data-type=text] select').on('keyup change paste', ev => { + const path = $(ev.target).parents('.filter').data('path'); + const key = path.replace(/\./g, ''); + const value = ev.target.value; + const browserTab = $(ev.target).parents('.tab').data('tab'); + + const filterTarget = `${browserTab}Filters`; + + if (value === '' || value === undefined) { + delete this[filterTarget].activeFilters[key]; + } else { + this[filterTarget].activeFilters[key] = { + path: path, + type: 'text', + valIsArray: false, + value: ev.target.value + } + } + + this.replaceList(html, browserTab); + }); + + // select filters + html.find('.filter[data-type=select] select, .filter[data-type=bool] select').on('change', ev => { + const path = $(ev.target).parents('.filter').data('path'); + const key = path.replace(/\./g, ''); + const filterType = $(ev.target).parents('.filter').data('type'); + const browserTab = $(ev.target).parents('.tab').data('tab'); + let valIsArray = $(ev.target).parents('.filter').data('valisarray'); + if (valIsArray === 'true') valIsArray = true; + let value = ev.target.value; + if (value === 'false') value = false; + if (value === 'true') value = true; + + const filterTarget = `${browserTab}Filters`; + + if (value === "null") { + delete this[filterTarget].activeFilters[key] + } else { + this[filterTarget].activeFilters[key] = { + path: path, + type: filterType, + valIsArray: valIsArray, + value:value + } + } + this.replaceList(html, browserTab); + }); + + // multiselect filters + html.find('.filter[data-type=multiSelect] input').on('change', ev => { + const path = $(ev.target).parents('.filter').data('path'); + const key = path.replace(/\./g, ''); + const filterType = 'multiSelect'; + const browserTab = $(ev.target).parents('.tab').data('tab'); + let valIsArray = $(ev.target).parents('.filter').data('valisarray'); + if (valIsArray === 'true') valIsArray = true; + let value = $(ev.target).data('value'); + + const filterTarget = `${browserTab}Filters`; + const filter = this[filterTarget].activeFilters[key]; + + if (ev.target.checked === true) { + if (filter === undefined) { + this[filterTarget].activeFilters[key] = { + path: path, + type: filterType, + valIsArray: valIsArray, + values: [value] + } + } else { + this[filterTarget].activeFilters[key].values.push(value); + } + } else { + delete this[filterTarget].activeFilters[key].values.splice(this[filterTarget].activeFilters[key].values.indexOf(value),1); + if (this[filterTarget].activeFilters[key].values.length === 0) { + delete this[filterTarget].activeFilters[key]; + } + } + + this.replaceList(html, browserTab); + }); + + + html.find('.filter[data-type=numberCompare] select, .filter[data-type=numberCompare] input').on('change keyup paste', ev => { + const path = $(ev.target).parents('.filter').data('path'); + const key = path.replace(/\./g, ''); + const filterType = 'numberCompare'; + const browserTab = $(ev.target).parents('.tab').data('tab'); + let valIsArray = false; + + const operator = $(ev.target).parents('.filter').find('select').val(); + const value = $(ev.target).parents('.filter').find('input').val(); + + const filterTarget = `${browserTab}Filters`; + + if (value === '' || operator === 'null') { + delete this[filterTarget].activeFilters[key] + } else { + this[filterTarget].activeFilters[key] = { + path: path, + type: filterType, + valIsArray: valIsArray, + operator: operator, + value: value + } + } + + this.replaceList(html, browserTab); + }); + + //Just for the loading image + if (this.observer) { + html.find("img").each((i,img) => this.observer.observe(img)); + } + } + + async checkListsLoaded() { + //Provides extra info not in the standard SRD, like which classes can learn a spell + if (!this.classList) { this.classList = await fetch('modules/compendium-browser/spell-classes.json').then(result => { return result.json(); }).then(obj => { @@ -120,7 +442,7 @@ class SpellBrowser extends Application { }); } - if (this.packList === undefined) { + if (!this.packList) { this.packList = await fetch('modules/compendium-browser/item-packs.json').then(result => { return result.json(); }).then(obj => { @@ -128,19 +450,102 @@ class SpellBrowser extends Application { }); } - if (this.subClasses === undefined) { + if (!this.subClasses) { this.subClasses = await fetch('modules/compendium-browser/sub-classes.json').then(result => { return result.json(); }).then(obj => { return this.subClasses = obj; }); } + } - this.spellsLoaded = false; - this.spellsLoading = true; + async loadAndFilterItems(browserTab="spell",updateLoading=null) { + console.log(`Load and Filter Items | Started loading ${browserTab}s`); + console.time("loadAndFilterItems"); + await this.checkListsLoaded(); + + const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; + + //0.4.1: Load and filter just one of spells, feats, and items (specified by browserTab) + let unfoundSpells = ''; + let numItemsLoaded = 0; + let compactItems = {}; + + //Filter the full list, but only save the core compendium information + level + for (let pack of game.packs) { + if (pack['metadata']['entity'] === "Item" && this.settings.loadedSpellCompendium[pack.collection].load) { +//FIXME: How much could we do with the loaded index rather than all content? + await pack.getContent().then(content => { + for (let item5e of content) { + let compactItem = null; + const decoratedItem = this.decorateItem(item5e); + if (decoratedItem) { + if ((browserTab === "spell") && (decoratedItem.type === "spell")) { + if (this.getFilterResult(decoratedItem, this.spellFilters.activeFilters)) { + compactItem = { + compendium : pack.collection, + name : decoratedItem.name, + img: decoratedItem.img, + data : { + level : decoratedItem.data?.level, + components : decoratedItem.data?.components + } + } + } + } else if ((browserTab === "feat") && ((decoratedItem.type === "feat") || (decoratedItem.type === "class"))) { + if (this.getFilterResult(decoratedItem, this.featFilters.activeFilters)) { + compactItem = { + compendium : pack.collection, + name : decoratedItem.name, + img: decoratedItem.img, + classRequirementString : decoratedItem.classRequirementString + } + } + } else if ((browserTab === "item") && this.getFilterResult(decoratedItem, this.itemFilters.activeFilters)) { + compactItem = { + compendium : pack.collection, + name : decoratedItem.name, + img: decoratedItem.img, + type : decoratedItem.type + } + } + if (compactItem) { //Indicates it passed the filters + compactItems[decoratedItem._id] = compactItem; + if (numItemsLoaded++ >= maxLoad) break; + //0.4.2e: Update the UI (e.g. "Loading 142 spells") + if (updateLoading) {updateLoading(numItemsLoaded);} + } + } + }//for item5e of content + }); + }//end if pack entity === Item + if (numItemsLoaded >= maxLoad) break; + }//for packs + +/* + if (unfoundSpells !== '') { + console.log(`Load and Fliter Items | List of Spells that don't have a class associated to them:`); + console.log(unfoundSpells); + } +*/ + this.itemsLoaded = true; + console.timeEnd("loadAndFilterItems"); + console.log(`Load and Filter Items | Finished loading ${Object.keys(compactItems).length} ${browserTab}s`); + return compactItems; + } + + async loadItems(numToPreload=CMPBrowser.PRELOAD) { + console.log('Item Browser | Started loading items'); + console.time("loadItems"); + await this.checkListsLoaded(); + + this.itemsLoaded = false; + let unfoundSpells = ''; - + let numSpellsLoaded = 0; + let numFeatsLoaded = 0; + let numItemsLoaded = 0; let items = { spells: {}, feats: {}, @@ -148,20 +553,21 @@ class SpellBrowser extends Application { }; - for (let pack of game.packs) { - if (pack['metadata']['entity'] == "Item" && this.settings.loadedSpellCompendium[pack.collection].load) { + if (pack['metadata']['entity'] === "Item" && this.settings.loadedSpellCompendium[pack.collection].load) { await pack.getContent().then(content => { for (let item5e of content) { let item = item5e.data; - if (item.type == 'spell') { + if (item.type === 'spell') { + //0.4.1 Only preload a limited number and fill more in as needed + if (numSpellsLoaded++ > numToPreload) continue; item.compendium = pack.collection; // determining classes that can use the spell let cleanSpellName = item.name.toLowerCase().replace(/[^一-龠ぁ-ゔァ-ヴーa-zA-Z0-9a-zA-Z0-9々〆〤]/g, '').replace("'", '').replace(/ /g, ''); //let cleanSpellName = spell.name.toLowerCase().replace(/[^a-zA-Z0-9\s:]/g, '').replace("'", '').replace(/ /g, ''); - if (this.classList[cleanSpellName] !== undefined) { + if (this.classList[cleanSpellName]) { let classes = this.classList[cleanSpellName]; item.data.classes = classes.split(','); } else { @@ -178,9 +584,11 @@ class SpellBrowser extends Application { } } } - items.spells[(item._id)] = item; - } else if (item.type == 'feat' || item.type == 'class') { + } else if (item.type === 'feat' || item.type === 'class') { + //0.4.1 Only preload a limited number and fill more in as needed + if (numFeatsLoaded++ > numToPreload) continue; + item.compendium = pack.collection; // getting damage types item.damageTypes = []; @@ -216,13 +624,16 @@ class SpellBrowser extends Application { item.hasSave = item5e.hasSave; - items.feats[(item._id)] = item; + } else { + //0.4.1 Only preload a limited number and fill more in as needed + if (numItemsLoaded++ > numToPreload) continue; + item.compendium = pack.collection; // getting damage types item.damageTypes = []; - if (item.data.damage && item.data.damage.parts.length > 0) { + if (item.data.damage && item.data.damage.parts.size > 0) { for (let part of item.data.damage.parts) { let type = part[1]; if (item.damageTypes.indexOf(type) === -1) { @@ -233,7 +644,7 @@ class SpellBrowser extends Application { // getting pack let matchedPacks = []; - for (let pack in this.packList) { + for (let pack of Object.keys(this.packList)) { for (let packItem of this.packList[pack]) { if (item.name.toLowerCase() === packItem.toLowerCase()) { matchedPacks.push(pack); @@ -248,428 +659,215 @@ class SpellBrowser extends Application { item.usesRessources = item5e.hasLimitedUses items.items[(item._id)] = item; - } - } + } + + }//for item5e of content }); } - } + if ((numSpellsLoaded >= numToPreload) && (numFeatsLoaded >= numToPreload) && (numItemsLoaded >= numToPreload)) break; + }//for packs if (unfoundSpells !== '') { - console.log(`Spell Browser | List of Spells that don't have a class assosiated to them:`); + console.log(`Item Browser | List of Spells that don't have a class associated to them:`); console.log(unfoundSpells); - } - console.log('Spell Browser | Finished loading items'); + } + this.itemsLoaded = true; + console.timeEnd("loadItems"); + console.log(`Item Browser | Finished loading items: ${Object.keys(items.spells).length} spells, ${Object.keys(items.feats).length} features, ${Object.keys(items.items).length} items `); return items; } - async loadNpcs() { + async loadAndFilterNpcs(updateLoading=null) { console.log('NPC Browser | Started loading NPCs'); - + console.time("loadAndFilterNpcs"); let npcs = {}; + const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; + + let numNpcsLoaded = 0; + this.npcsLoaded = false; for (let pack of game.packs) { if (pack['metadata']['entity'] == "Actor" && this.settings.loadedNpcCompendium[pack.collection].load) { await pack.getContent().then(async content => { for (let npc of content) { - //console.log('%c '+npc.name, 'background: white; color: red') - npc = npc.data; - // add needed data - npc.compendium = pack.collection; - // cr display - let cr = npc.data.details.cr; - if (cr == undefined || cr == '') cr = 0; - else cr = Number(cr); - if (cr > 0 && cr < 1) cr = "1/" + (1 / cr); - npc.displayCR = cr; - npc.displaySize = 'unset'; - npc.filterSize = 2; - if (CONFIG.DND5E.actorSizes[npc.data.traits.size] !== undefined) { - npc.displaySize = CONFIG.DND5E.actorSizes[npc.data.traits.size]; - } - switch (npc.data.traits.size) { - case 'grg': npc.filterSize = 5; break; - case 'huge': npc.filterSize = 4; break; - case 'lg': npc.filterSize = 3; break; - case 'sm': npc.filterSize = 1; break; - case 'tiny': npc.filterSize = 0; break; - case 'med': - default: npc.filterSize = 2; break; - } - - // getting value for HasSpells and damage types - npc.hasSpells = false; - npc.damageDealt = []; - for (let item of npc.items) { - if (item.type == 'spell') { - npc.hasSpells = true; + let compactNpc = null; + const decoratedNpc = this.decorateNpc(npc); + if (decoratedNpc && this.getFilterResult(decoratedNpc, this.npcFilters.activeFilters)) { + //0.4.2: Don't store all the details - just the display elements + compactNpc = { + compendium : pack.collection, + name : decoratedNpc.name, + img: decoratedNpc.img, + displayCR : decoratedNpc.displayCR, + displaySize : decoratedNpc.displaySize, + displayType: decoratedNpc.data?.details?.type, + orderCR : decoratedNpc.data.details.cr, + orderSize : decoratedNpc.filterSize } - if (item.data.damage && item.data.damage.parts && item.data.damage.parts.length > 0) { - for (let part of item.data.damage.parts) { - let type = part[1]; - if (npc.damageDealt.indexOf(type) === -1) { - npc.damageDealt.push(type); - } - } + if (compactNpc) { + npcs[decoratedNpc._id] = compactNpc; + //0.4.2 Don't load more than maxLoad; display a message to filter + if (numNpcsLoaded++ > maxLoad) break; + //0.4.2e: Update the UI (e.g. "Loading 142 NPCs") + if (updateLoading) {updateLoading(numNpcsLoaded);} } } - - npcs[npc._id] = npc; } }); } + //0.4.1 Only preload a limited number and fill more in as needed + if (numNpcsLoaded >= maxLoad) break; } - console.log('NPC Browser | Finished loading NPCs'); + + this.npcsLoaded = true; + console.timeEnd("loadAndFilterNpcs"); + console.log(`NPC Browser | Finished loading NPCs: ${Object.keys(npcs).length} NPCs`); return npcs; } - activateListeners(html) { - super.activateListeners(html); - // localizing title - $(html).parents('.app').find('.window-title')[0].innerText = game.i18n.localize("CMPBrowser.compendiumBrowser"); - // show entity sheet - html.find('.item-edit').click(ev => { - let itemId = $(ev.currentTarget).parents("li").attr("data-entry-id"); - let compendium = $(ev.currentTarget).parents("li").attr("data-entry-compendium"); - let pack = game.packs.find(p => p.collection === compendium); - pack.getEntity(itemId).then(entity => { - entity.sheet.render(true); + + hookCompendiumList() { + Hooks.on('renderCompendiumDirectory', (app, html, data) => { + this.hookCompendiumList(); + }); + + let html = $('#compendium'); + if (this.settings === undefined) { + this.initSettings(); + } + if (game.user.isGM || this.settings.allowSpellBrowser || this.settings.allowNpcBrowser) { + const cbButton = $(``); + html.find('.compendium-browser-btn').remove(); + + // adding to directory-list since the footer doesn't exist if the user is not gm + html.find('.directory-footer').append(cbButton); + + // Handle button clicks + cbButton.click(ev => { + ev.preventDefault(); + //0.4.1: Reset filters when you click button + this.resetFilters(); + //0.4.3: Reset everything (including data) when you press the button - calls afterRender() hook + + if (game.user.isGM || this.settings.allowSpellBrowser) { + this.refreshList = "spell"; + } else if (this.settings.allowFeatBrowser) { + this.refreshList = "feat"; + } else if (this.settings.allowItemBrowser) { + this.refreshList = "item"; + } else if (this.settings.allowNPCBrowser) { + this.refreshList = "npc"; + } + this.render(true); }); - }); - - // make draggable - html.find('.draggable').each((i, li) => { - li.setAttribute("draggable", true); - li.addEventListener('dragstart', event => { - let packName = li.getAttribute("data-entry-compendium"); - let pack = game.packs.find(p => p.collection === packName); - if (!pack) { - event.preventDefault(); - return false; - } - event.dataTransfer.setData("text/plain", JSON.stringify({ - type: pack.entity, - pack: pack.collection, - id: li.getAttribute("data-entry-id") - })); - }, false); - }); - - // toggle visibility of filter containers - html.find('.filtercontainer h3, .multiselect label').click(async ev => { - await $(ev.target.nextElementSibling).toggle(100); - - }); - html.find('.multiselect label').trigger('click'); - - // sort spell list - html.find('.spell-browser select[name=sortorder]').on('change', ev => { - let spellList = html.find('.spell-browser li'); - let byName = (ev.target.value == 'true'); - let sortedList = this.sortSpells(spellList, byName); - let ol = $(html.find('.spell-browser ul')); - ol[0].innerHTML = []; - for (let element of sortedList) { - ol[0].append(element); - } - }); - html.find('.spell-browser select[name=sortorder]').trigger('change'); - - // sort feat list - html.find('.feat-browser select[name=sortorder]').on('change', ev => { - let featList = html.find('.feat-browser li'); - let byName = (ev.target.value == 'true'); - let sortedList = this.sortFeats(featList, byName); - let ol = $(html.find('.feat-browser ul')); - ol[0].innerHTML = []; - for (let element of sortedList) { - ol[0].append(element); - } - }); - html.find('.feat-browser select[name=sortorder]').trigger('change'); - - // sort item list - html.find('.item-browser select[name=sortorder]').on('change', ev => { - let itemList = html.find('.item-browser li'); - let byName = (ev.target.value == 'true'); - let sortedList = this.sortItems(itemList, byName); - let ol = $(html.find('.item-browser ul')); - ol[0].innerHTML = []; - for (let element of sortedList) { - ol[0].append(element); - } - }); - html.find('.item-browser select[name=sortorder]').trigger('change'); - - // sort npc list - html.find('.npc-browser select[name=sortorder]').on('change', ev => { - let npcList = html.find('.npc-browser li'); - let orderBy = ev.target.value; - let sortedList = this.sortNpcs(npcList, orderBy); - let ol = $(html.find('.npc-browser ul')); - ol[0].innerHTML = []; - for (let element of sortedList) { - ol[0].append(element); - } - }); - html.find('.npc-browser select[name=sortorder]').trigger('change') - - // reset filters - html.find('#reset-spell-filter').click(ev => { - this.spellFilters.activeFilters = {}; - this.render(); - }); - - html.find('#reset-feat-filter').click(ev => { - this.featFilters.activeFilters = {}; - this.render(); - }); - - html.find('#reset-item-filter').click(ev => { - this.itemFilters.activeFilters = {}; - this.render(); - }); - - html.find('#reset-npc-filter').click(ev => { - this.npcFilters.activeFilters = {}; - this.render(); - }); - - // settings - html.find('.settings input').on('change', ev => { - let setting = ev.target.dataset.setting; - let value = ev.target.checked; - if (setting === 'spell-compendium-setting') { - let key = ev.target.dataset.key; - this.settings.loadedSpellCompendium[key].load = value; - this.loadItems().then((spells) => { - this.spells = spells; - this.render(); - }); - ui.notifications.info("Settings Saved. Spell Compendiums are being reloaded."); - } else if (setting === 'npc-compendium-setting') { - let key = ev.target.dataset.key; - this.settings.loadedNpcCompendium[key].load = value; - this.loadNpcs().then((npcs) => { - this.npcs = npcs; - this.render(); - }); - ui.notifications.info("Settings Saved. NPC Compendiums are being reloaded."); - } - if (setting === 'allow-spell-browser') { - this.settings.allowSpellBrowser = value; - } - if (setting === 'allow-feat-browser') { - this.settings.allowFeatBrowser = value; - } - if (setting === 'allow-item-browser') { - this.settings.allowItemBrowser = value; - } - if (setting === 'allow-npc-browser') { - this.settings.allowNpcBrowser = value; - } - this.saveSettings(); - }); - - - // activating or deactivating filters - - // text filters - - html.find('.filter[data-type=text] input, .filter[data-type=text] select').on('keyup change paste', ev => { - let path = $(ev.target).parents('.filter').data('path'); - let key = path.replace(/\./g, ''); - let value = ev.target.value; - let itemType = $(ev.target).parents('.tab').data('tab'); - - let filterTarget = `${itemType}Filters`; - - if (value === '' || value === undefined) { - delete this[filterTarget].activeFilters[key]; - } else { - this[filterTarget].activeFilters[key] = { - path: path, - type: 'text', - valIsArray: false, - value: ev.target.value - } - } - - let list = null; - let subjects = null; - if (itemType === 'spell') { - list = html.find('.spell-browser li'); - subjects = this.items.spells; - } else if (itemType === 'npc') { - list = html.find('.npc-browser li'); - subjects = this.npcs; - } else if (itemType === 'feat') { - list = html.find('.feat-browser li'); - subjects = this.items.feats; - } else if (itemType === 'item') { - list = html.find('.item-browser li'); - subjects = this.items.items; - } - this.filterElements(list, subjects, this[filterTarget].activeFilters); - }); - - // select filters - html.find('.filter[data-type=select] select, .filter[data-type=bool] select').on('change', ev => { - let path = $(ev.target).parents('.filter').data('path'); - let key = path.replace(/\./g, ''); - let filterType = $(ev.target).parents('.filter').data('type'); - let itemType = $(ev.target).parents('.tab').data('tab'); - let valIsArray = $(ev.target).parents('.filter').data('valisarray'); - if (valIsArray === 'true') valIsArray = true; - let value = ev.target.value; - if (value === 'false') value = false; - if (value === 'true') value = true; - - let filterTarget = `${itemType}Filters`; - - if (value === "null") { - delete this[filterTarget].activeFilters[key] - } else { - this[filterTarget].activeFilters[key] = { - path: path, - type: filterType, - valIsArray: valIsArray, - value:value - } - } - - let list = null; - let subjects = null; - if (itemType === 'spell') { - list = html.find('.spell-browser li'); - subjects = this.items.spells; - } else if (itemType === 'npc') { - list = html.find('.npc-browser li'); - subjects = this.npcs; - } else if (itemType === 'feat') { - list = html.find('.feat-browser li'); - subjects = this.items.feats; - } else if (itemType === 'item') { - list = html.find('.item-browser li'); - subjects = this.items.items; - } - this.filterElements(list, subjects, this[filterTarget].activeFilters); - }); - - // multiselect filters - html.find('.filter[data-type=multiSelect] input').on('change', ev => { - let path = $(ev.target).parents('.filter').data('path'); - let key = path.replace(/\./g, ''); - let filterType = 'multiSelect'; - let itemType = $(ev.target).parents('.tab').data('tab'); - let valIsArray = $(ev.target).parents('.filter').data('valisarray'); - if (valIsArray === 'true') valIsArray = true; - let value = $(ev.target).data('value'); - - let filterTarget = `${itemType}Filters`; - let filter = this[filterTarget].activeFilters[key]; - - if (ev.target.checked === true) { - if (filter === undefined) { - this[filterTarget].activeFilters[key] = { - path: path, - type: filterType, - valIsArray: valIsArray, - values: [ value ] - } - } else { - this[filterTarget].activeFilters[key].values.push(value); - } - } else { - delete this[filterTarget].activeFilters[key].values.splice(this[filterTarget].activeFilters[key].values.indexOf(value),1); - if (this[filterTarget].activeFilters[key].values.length === 0) { - delete this[filterTarget].activeFilters[key]; - } - } - - let list = null; - let subjects = null; - if (itemType === 'spell') { - list = html.find('.spell-browser li'); - subjects = this.items.spells; - } else if (itemType === 'npc') { - list = html.find('.npc-browser li'); - subjects = this.npcs; - } else if (itemType === 'feat') { - list = html.find('.feat-browser li'); - subjects = this.items.feats; - } else if (itemType === 'item') { - list = html.find('.item-browser li'); - subjects = this.items.items; - } - this.filterElements(list, subjects, this[filterTarget].activeFilters); - }); - - - html.find('.filter[data-type=numberCompare] select, .filter[data-type=numberCompare] input').on('change keyup paste', ev => { - let path = $(ev.target).parents('.filter').data('path'); - let key = path.replace(/\./g, ''); - let filterType = 'numberCompare'; - let itemType = $(ev.target).parents('.tab').data('tab'); - let valIsArray = false; - - let operator = $(ev.target).parents('.filter').find('select').val(); - let value = $(ev.target).parents('.filter').find('input').val(); - - let filterTarget = `${itemType}Filters`; - - if (value === '' || operator === 'null') { - delete this[filterTarget].activeFilters[key] - } else { - this[filterTarget].activeFilters[key] = { - path: path, - type: filterType, - valIsArray: valIsArray, - operator: operator, - value: value - } - } - - let list = null; - let subjects = null; - if (itemType === 'spell') { - list = html.find('.spell-browser li'); - subjects = this.items.spells; - } else if (itemType === 'npc') { - list = html.find('.npc-browser li'); - subjects = this.npcs; - } else if (itemType === 'feat') { - list = html.find('.feat-browser li'); - subjects = this.items.feats; - } else if (itemType === 'item') { - list = html.find('.item-browser li'); - subjects = this.items.items; - } - this.filterElements(list, subjects, this[filterTarget].activeFilters); - }); - - - // lazy load images - const observer = new IntersectionObserver((entries, observer) => { - for (let e of entries) { - if (!e.isIntersecting) continue; - const img = e.target; - // Avatar image - //const img = li.querySelector("img"); - if (img && img.dataset.src) { - img.src = img.dataset.src; - delete img.dataset.src; - } - - // No longer observe the target - observer.unobserve(e.target); - } - }); - html.find("img").each((i, img) => observer.observe(img)); + } } + + /* Hook to load the first data */ + static afterRender(cb, html) { + //0.4.3: Because a render always resets ALL the displayed filters (on all tabs) to unselected , we have to blank all the lists as well + // (because the current HTML template doesn't set the selected filter values) + if (!cb?.refreshList) {return;} + + cb.replaceList(html, cb.refreshList); + + cb.refreshList = null; + } + + resetFilters() { + this.spellFilters.activeFilters = {}; + this.featFilters.activeFilters = {}; + this.itemFilters.activeFilters = {}; + this.npcFilters.activeFilters = {}; + } + + + + async replaceList(html, browserTab, options = {reload : true}) { + //After rendering the first time or re-rendering trigger the load/reload of visible data + + let elements = null; + //0.4.2 Display a Loading... message while the data is being loaded and filtered + let loadingMessage = null; + if (browserTab === 'spell') { + elements = html.find("ul#CBSpells"); + loadingMessage = html.find("#CBSpellsMessage"); + } else if (browserTab === 'npc') { + elements = html.find("ul#CBNPCs"); + loadingMessage = html.find("#CBNpcsMessage"); + } else if (browserTab === 'feat') { + elements = html.find("ul#CBFeats"); + loadingMessage = html.find("#CBFeatsMessage"); + } else if (browserTab === 'item') { + elements = html.find("ul#CBItems"); + loadingMessage = html.find("#CBItemsMessage"); + } + if (elements?.length) { + //0.4.2b: On a tab-switch, only reload if there isn't any data already + if (options?.reload || !elements[0].children.length) { + + const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; + const updateLoading = async numLoaded => { + if (loadingMessage.length) {this.renderLoading(loadingMessage[0], browserTab, numLoaded, numLoaded>=maxLoad);} + } + updateLoading(0); + + //Uses loadAndFilterItems to read compendia for items which pass the current filters and render on this tab + const newItemsHTML = await this.renderItemData(browserTab, updateLoading); + elements[0].innerHTML = newItemsHTML; + //Re-sort before setting up lazy loading + this.triggerSort(html, browserTab); + + //Lazy load images + if (this.observer) { + $(elements).find("img").each((i,img) => this.observer.observe(img)); + } + + //Reactivate listeners for clicking and dragging + this.activateItemListListeners($(elements)); + } + } + + } + + async renderLoading(messageElement, itemType, numLoaded, maxLoaded=false) { + if (!messageElement) return; + + let loadingHTML = await renderTemplate("modules/compendium-browser/template/loading.html", {numLoaded: numLoaded, itemType: itemType, maxLoaded: maxLoaded}); + messageElement.innerHTML = loadingHTML; + } + + async renderItemData(browserTab, updateLoading=null) { + let listItems; + if (browserTab === "npc") { + listItems = await this.loadAndFilterNpcs(updateLoading); + } else { + listItems = await this.loadAndFilterItems(browserTab, updateLoading); + } + const html = await renderTemplate(`modules/compendium-browser/template/${browserTab}-browser-list.html`, {listItems : listItems}) + + return html; + } + + //SORTING + triggerSort(html, browserTab) { + if (browserTab === 'spell') { + html.find('.spell-browser select[name=sortorder]').trigger('change'); + } else if (browserTab === 'feat') { + html.find('.feat-browser select[name=sortorder]').trigger('change'); + } else if (browserTab === 'npc') { + html.find('.npc-browser select[name=sortorder]').trigger('change') + } else if (browserTab === 'item') { + html.find('.item-browser select[name=sortorder]').trigger('change'); + } + } + + + sortSpells(list, byName) { - if(byName) { + if (byName) { list.sort((a, b) => { let aName = $(a).find('.item-name a')[0].innerHTML; let bName = $(b).find('.item-name a')[0].innerHTML; @@ -791,6 +989,120 @@ class SpellBrowser extends Application { return list; } + decorateItem(item5e) { + if (!item5e) return null; + //Decorate and then filter a compendium entry - returns null or the item + const item = item5e.data; + + // getting damage types (common to all Items, although some won't have any) + item.damageTypes = []; + if (item.data.damage && item.data.damage.parts.length > 0) { + for (let part of item.data.damage.parts) { + let type = part[1]; + if (item.damageTypes.indexOf(type) === -1) { + item.damageTypes.push(type); + } + } + } + + if (item.type === 'spell') { + // determining classes that can use the spell + let cleanSpellName = item.name.toLowerCase().replace(/[^一-龠ぁ-ゔァ-ヴーa-zA-Z0-9a-zA-Z0-9々〆〤]/g, '').replace("'", '').replace(/ /g, ''); + //let cleanSpellName = spell.name.toLowerCase().replace(/[^a-zA-Z0-9\s:]/g, '').replace("'", '').replace(/ /g, ''); + if (this.classList[cleanSpellName]) { + let classes = this.classList[cleanSpellName]; + item.data.classes = classes.split(','); + } else { +//FIXME: unfoundSpells += cleanSpellName + ','; + } + } else if (item.type === 'feat' || item.type === 'class') { + // getting class + let reqString = item.data.requirements?.replace(/[0-9]/g, '').trim(); + let matchedClass = []; + for (let c in this.subClasses) { + if (reqString && reqString.toLowerCase().indexOf(c) !== -1) { + matchedClass.push(c); + } else { + for (let subClass of this.subClasses[c]) { + if (reqString && reqString.indexOf(subClass) !== -1) { + matchedClass.push(c); + break; + } + } + } + } + item.classRequirement = matchedClass; + item.classRequirementString = matchedClass.join(', '); + + // getting uses/ressources status + item.usesRessources = item5e.hasLimitedUses; + item.hasSave = item5e.hasSave; + + } else { + // getting pack + let matchedPacks = []; + for (let pack of Object.keys(this.packList)) { + for (let packItem of this.packList[pack]) { + if (item.name.toLowerCase() === packItem.toLowerCase()) { + matchedPacks.push(pack); + break; + } + } + } + item.matchedPacks = matchedPacks; + item.matchedPacksString = matchedPacks.join(', '); + + // getting uses/ressources status + item.usesRessources = item5e.hasLimitedUses + } + return item; + } + + decorateNpc(npc) { + //console.log('%c '+npc.name, 'background: white; color: red') + const decoratedNpc = npc.data; + + // cr display + let cr = decoratedNpc.data.details.cr; + if (cr == undefined || cr == '') cr = 0; + else cr = Number(cr); + if (cr > 0 && cr < 1) cr = "1/" + (1 / cr); + decoratedNpc.displayCR = cr; + decoratedNpc.displaySize = 'unset'; + decoratedNpc.filterSize = 2; + if (CONFIG.DND5E.actorSizes[decoratedNpc.data.traits.size] !== undefined) { + decoratedNpc.displaySize = CONFIG.DND5E.actorSizes[decoratedNpc.data.traits.size]; + } + switch (decoratedNpc.data.traits.size) { + case 'grg': decoratedNpc.filterSize = 5; break; + case 'huge': decoratedNpc.filterSize = 4; break; + case 'lg': decoratedNpc.filterSize = 3; break; + case 'sm': decoratedNpc.filterSize = 1; break; + case 'tiny': decoratedNpc.filterSize = 0; break; + case 'med': + default: decoratedNpc.filterSize = 2; break; + } + + // getting value for HasSpells and damage types + decoratedNpc.hasSpells = false; + decoratedNpc.damageDealt = []; + for (let item of decoratedNpc.items) { + if (item.type == 'spell') { + decoratedNpc.hasSpells = true; + } + if (item.data.damage && item.data.damage.parts && item.data.damage.parts.length > 0) { + for (let part of item.data.damage.parts) { + let type = part[1]; + if (decoratedNpc.damageDealt.indexOf(type) === -1) { + decoratedNpc.damageDealt.push(type); + } + } + } + } + + return decoratedNpc; + } + filterElements(list, subjects, filters) { for (let element of list) { let subject = subjects[element.dataset.entryId]; @@ -803,8 +1115,7 @@ class SpellBrowser extends Application { } getFilterResult(subject, filters) { - for (let filterKey in filters) { - let filter = filters[filterKey]; + for (let filter of Object.values(filters)) { let prop = getProperty(subject, filter.path); if (filter.type === 'numberCompare') { @@ -837,7 +1148,7 @@ class SpellBrowser extends Application { if (prop.indexOf(filter.value) === -1) { return false; } - } else if(filter.values) { + } else if (filter.values) { for (let val of filter.values) { if (prop.indexOf(val) !== -1) { continue; @@ -875,13 +1186,13 @@ class SpellBrowser extends Application { loadedNpcCompendium: {}, }; for (let compendium of game.packs) { - if (compendium['metadata']['entity'] == "Item") { + if (compendium['metadata']['entity'] === "Item") { defaultSettings.loadedSpellCompendium[compendium.collection] = { load: true, name: `${compendium['metadata']['label']} (${compendium.collection})` }; } - if (compendium['metadata']['entity'] == "Actor") { + if (compendium['metadata']['entity'] === "Actor") { defaultSettings.loadedNpcCompendium[compendium.collection] = { load: true, name: `${compendium['metadata']['label']} (${compendium.collection})` @@ -889,7 +1200,7 @@ class SpellBrowser extends Application { } } // creating game setting container - game.settings.register("compendiumBrowser", "settings", { + game.settings.register(CMPBrowser.MODULE_NAME, "settings", { name: "Compendium Browser Settings", hint: "Settings to exclude packs from loading and visibility of the browser", default: defaultSettings, @@ -899,9 +1210,22 @@ class SpellBrowser extends Application { this.settings = settings; } }); + game.settings.register(CMPBrowser.MODULE_NAME, "maxload", { + name: game.i18n.localize("CMPBrowser.SETTING.Maxload.NAME"), + hint: game.i18n.localize("CMPBrowser.SETTING.Maxload.HINT"), + scope: "world", + config: true, + default: CMPBrowser.MAXLOAD, + type: Number, + range: { // If range is specified, the resulting setting will be a range slider + min: 200, + max: 5000, + step: 100 + } + }); // load settings from container and apply to default settings (available compendie might have changed) - let settings = game.settings.get('compendiumBrowser', 'settings'); + let settings = game.settings.get(CMPBrowser.MODULE_NAME, 'settings'); for (let compKey in defaultSettings.loadedSpellCompendium) { if (settings.loadedSpellCompendium[compKey] !== undefined) { defaultSettings.loadedSpellCompendium[compKey].load = settings.loadedSpellCompendium[compKey].load; @@ -918,7 +1242,7 @@ class SpellBrowser extends Application { defaultSettings.allowNpcBrowser = settings.allowNpcBrowser ? true : false; if (game.user.isGM) { - game.settings.set('compendiumBrowser', 'settings', defaultSettings); + game.settings.set(CMPBrowser.MODULE_NAME, 'settings', defaultSettings); console.log("New default settings set"); console.log(defaultSettings); } @@ -926,10 +1250,12 @@ class SpellBrowser extends Application { } saveSettings() { - game.settings.set('compendiumBrowser', 'settings', this.settings); + game.settings.set(CMPBrowser.MODULE_NAME, 'settings', this.settings); } - addFilter(entityType, category, label, path, type, possibleValues = null, valIsArray = false) { + //FILTERS - Added on the Ready hook + //0.4.0 Make this async so filters can be added all at once + async addFilter(entityType, category, label, path, type, possibleValues = null, valIsArray = false) { let target = `${entityType}Filters`; let filter = {}; filter.path = path; @@ -946,13 +1272,159 @@ class SpellBrowser extends Application { let catId = category.replace(/\W/g, ''); if (this[target].registeredFilterCategorys[catId] === undefined) { - this[target].registeredFilterCategorys[catId] = { label: category, filters: [] }; + this[target].registeredFilterCategorys[catId] = {label: category, filters: []}; } this[target].registeredFilterCategorys[catId].filters.push(filter); } - _lazyLoadImg(entries, observer) { + async addSpellFilters() { + // Spellfilters + this.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); + this.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.lvl"), 'data.level', 'multiSelect', [game.i18n.localize("CMPBrowser.cantip"), 1, 2, 3, 4, 5, 6, 7, 8, 9]); + this.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.school"), 'data.school', 'select', CONFIG.DND5E.spellSchools); + this.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.castingTime"), 'data.activation.type', 'select', + { + action: game.i18n.localize("DND5E.Action"), + bonus: game.i18n.localize("CMPBrowser.bonusAction"), + reaction: game.i18n.localize("CMPBrowser.reaction"), + minute: game.i18n.localize("DND5E.TimeMinute"), + hour: game.i18n.localize("DND5E.TimeHour"), + day: game.i18n.localize("DND5E.TimeDay") + } + ); + this.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.spellType"), 'data.actionType', 'select', CONFIG.DND5E.itemActionTypes); + this.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes); + this.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.class"), 'data.classes', 'select', + { + artificer: game.i18n.localize("CMPBrowser.artificer"), + bard: game.i18n.localize("CMPBrowser.bard"), + cleric: game.i18n.localize("CMPBrowser.cleric"), + druid: game.i18n.localize("CMPBrowser.druid"), + paladin: game.i18n.localize("CMPBrowser.paladin"), + ranger: game.i18n.localize("CMPBrowser.ranger"), + sorcerer: game.i18n.localize("CMPBrowser.sorcerer"), + warlock: game.i18n.localize("CMPBrowser.warlock"), + wizard: game.i18n.localize("CMPBrowser.wizard"), + }, true + ); + this.addSpellFilter(game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.ritual"), 'data.components.ritual', 'bool'); + this.addSpellFilter(game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.concentration"), 'data.components.concentration', 'bool'); + this.addSpellFilter(game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.verbal"), 'data.components.vocal', 'bool'); + this.addSpellFilter(game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.somatic"), 'data.components.somatic', 'bool'); + this.addSpellFilter(game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.material"), 'data.components.material', 'bool'); + } + + async addItemFilters() { + // Item Filters + + this.addItemFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); + this.addItemFilter(game.i18n.localize("CMPBrowser.general"), "Item Type", 'type', 'select', { + consumable: game.i18n.localize("DND5E.ItemTypeConsumable"), + backpack: game.i18n.localize("DND5E.ItemTypeContainer"), + equipment: game.i18n.localize("DND5E.ItemTypeEquipment"), + loot: game.i18n.localize("DND5E.ItemTypeLoot"), + tool: game.i18n.localize("DND5E.ItemTypeTool"), + weapon: game.i18n.localize("DND5E.ItemTypeWeapon") + }); + this.addItemFilter(game.i18n.localize("CMPBrowser.general"), "Packs", 'matchedPacks', 'select', + { + burglar: "Burglar's Pack", + diplomat: "Diplomat's Pack", + dungeoneer: "Dungeoneer's Pack", + entertainer: "Entertainer's Pack", + explorer: "Explorer's Pack", + monsterhunter: "Monster Hunter's Pack", + priest: "Priest's Pack", + scholar: "Scholar's Pack", + }, true + ); + this.addItemFilter("Game Mechanics", game.i18n.localize("DND5E.ItemActivationCost"), 'data.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes); + this.addItemFilter("Game Mechanics", game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes); + this.addItemFilter("Game Mechanics", "Uses Resources", 'usesRessources', 'bool'); + + this.addItemFilter("Item Subtype", "Weapon", 'data.weaponType', 'text', CONFIG.DND5E.weaponTypes); + this.addItemFilter("Item Subtype", "Equipment", 'data.armor.type', 'text', CONFIG.DND5E.equipmentTypes); + this.addItemFilter("Item Subtype", "Consumable", 'data.consumableType', 'text', CONFIG.DND5E.consumableTypes); + + this.addItemFilter("Magic Items", "Rarity", 'data.rarity', 'select', + { + Common: "Common", + Uncommon: "Uncommon", + Rare: "Rare", + "Very rare": "Very Rare", + Legendary: "Legendary" + }); + } + + async addFeatFilters() { + + // Feature Filters + + this.addFeatFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); + this.addFeatFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.class"), 'classRequirement', 'select', + { + artificer: game.i18n.localize("CMPBrowser.artificer"), + barbarian: "Barbarian", + bard: game.i18n.localize("CMPBrowser.bard"), + cleric: game.i18n.localize("CMPBrowser.cleric"), + druid: game.i18n.localize("CMPBrowser.druid"), + fighter: "Fighter", + monk: "Monk", + paladin: game.i18n.localize("CMPBrowser.paladin"), + ranger: game.i18n.localize("CMPBrowser.ranger"), + rogue: "Rogue", + sorcerer: game.i18n.localize("CMPBrowser.sorcerer"), + warlock: game.i18n.localize("CMPBrowser.warlock"), + wizard: game.i18n.localize("CMPBrowser.wizard") + }, true); + + this.addFeatFilter("Game Mechanics", game.i18n.localize("DND5E.ItemActivationCost"), 'data.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes); + this.addFeatFilter("Game Mechanics", game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes); + this.addFeatFilter("Game Mechanics", "Uses Resources", 'usesRessources', 'bool'); + + + } + + async addNpcFilters() { + // NPC Filters + + this.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.details.source', 'text'); + this.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.size"), 'data.traits.size', 'select', CONFIG.DND5E.actorSizes); + this.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasSpells"), 'hasSpells', 'bool'); + this.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegAct"), 'data.resources.legact.max', 'bool'); + this.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegRes"), 'data.resources.legres.max', 'bool'); + this.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.cr"), 'data.details.cr', 'numberCompare'); + this.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.creatureType"), 'data.details.type', 'text', { + aberration: game.i18n.localize("CMPBrowser.aberration"), + beast: game.i18n.localize("CMPBrowser.beast"), + celestial: game.i18n.localize("CMPBrowser.celestial"), + construct: game.i18n.localize("CMPBrowser.construct"), + dragon: game.i18n.localize("CMPBrowser.dragon"), + elemental: game.i18n.localize("CMPBrowser.elemental"), + fey: game.i18n.localize("CMPBrowser.fey"), + fiend: game.i18n.localize("CMPBrowser.fiend"), + giant: game.i18n.localize("CMPBrowser.giant"), + humanoid: game.i18n.localize("CMPBrowser.humanoid"), + monstrosity: game.i18n.localize("CMPBrowser.monstrosity"), + ooze: game.i18n.localize("CMPBrowser.ooze"), + plant: game.i18n.localize("CMPBrowser.plant"), + undead: game.i18n.localize("CMPBrowser.undead") + }); + + this.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityStr"), 'data.abilities.str.value', 'numberCompare'); + this.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityDex"), 'data.abilities.dex.value', 'numberCompare'); + this.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCon"), 'data.abilities.con.value', 'numberCompare'); + this.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityInt"), 'data.abilities.int.value', 'numberCompare'); + this.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityWis"), 'data.abilities.wis.value', 'numberCompare'); + this.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCha"), 'data.abilities.cha.value', 'numberCompare'); + + this.addNpcFilter(game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamImm"), 'data.traits.di.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); + this.addNpcFilter(game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamRes"), 'data.traits.dr.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); + this.addNpcFilter(game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamVuln"), 'data.traits.dv.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); + this.addNpcFilter(game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.ConImm"), 'data.traits.ci.value', 'multiSelect', CONFIG.DND5E.conditionTypes, true); + this.addNpcFilter(game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("CMPBrowser.dmgDealt"), 'damageDealt', 'multiSelect', CONFIG.DND5E.damageTypes, true); + } /** @@ -1002,147 +1474,20 @@ class SpellBrowser extends Application { } } -Hooks.on('ready', async function() { +Hooks.on('ready', async () => { if (game.compendiumBrowser === undefined) { - game.compendiumBrowser = new SpellBrowser(); - await game.compendiumBrowser.initializeContent(); + game.compendiumBrowser = new CompendiumBrowser(); +//0.4.0 Defer loading content until we actually use the Compendium Browser + //A compromise approach would be better (periodic loading) except would still create the memory use problem + await game.compendiumBrowser.initialize(); } - // Spellfilters + game.compendiumBrowser.addSpellFilters(); + game.compendiumBrowser.addFeatFilters(); + game.compendiumBrowser.addItemFilters(); + game.compendiumBrowser.addNpcFilters(); - game.compendiumBrowser.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); - game.compendiumBrowser.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.lvl"), 'data.level', 'multiSelect', [game.i18n.localize("CMPBrowser.cantip"), 1, 2, 3, 4, 5, 6, 7, 8, 9]); - game.compendiumBrowser.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.school"), 'data.school', 'select', CONFIG.DND5E.spellSchools); - game.compendiumBrowser.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.castingTime"), 'data.activation.type', 'select', - { - action: game.i18n.localize("DND5E.Action"), - bonus: game.i18n.localize("CMPBrowser.bonusAction"), - reaction: game.i18n.localize("CMPBrowser.reaction"), - minute: game.i18n.localize("DND5E.TimeMinute"), - hour: game.i18n.localize("DND5E.TimeHour"), - day: game.i18n.localize("DND5E.TimeDay") - }); - game.compendiumBrowser.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.spellType"), 'data.actionType', 'select', CONFIG.DND5E.itemActionTypes); - game.compendiumBrowser.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes); - game.compendiumBrowser.addSpellFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.class"), 'data.classes', 'select', - { - artificer: game.i18n.localize("CMPBrowser.artificer"), - bard: game.i18n.localize("CMPBrowser.bard"), - cleric: game.i18n.localize("CMPBrowser.cleric"), - druid: game.i18n.localize("CMPBrowser.druid"), - paladin: game.i18n.localize("CMPBrowser.paladin"), - ranger: game.i18n.localize("CMPBrowser.ranger"), - sorcerer: game.i18n.localize("CMPBrowser.sorcerer"), - warlock: game.i18n.localize("CMPBrowser.warlock"), - wizard: game.i18n.localize("CMPBrowser.wizard"), - }, true); +}); - game.compendiumBrowser.addSpellFilter(game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.ritual"), 'data.components.ritual', 'bool'); - game.compendiumBrowser.addSpellFilter(game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.concentration"), 'data.components.concentration', 'bool'); - game.compendiumBrowser.addSpellFilter(game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.verbal"), 'data.components.vocal', 'bool'); - game.compendiumBrowser.addSpellFilter(game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.somatic"), 'data.components.somatic', 'bool'); - game.compendiumBrowser.addSpellFilter(game.i18n.localize("CMPBrowser.components"), game.i18n.localize("CMPBrowser.material"), 'data.components.material', 'bool'); - - // Feature Filters - - game.compendiumBrowser.addFeatFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); - game.compendiumBrowser.addFeatFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.class"), 'classRequirement', 'select', - { - artificer: game.i18n.localize("CMPBrowser.artificer"), - barbarian: "Barbarian", - bard: game.i18n.localize("CMPBrowser.bard"), - cleric: game.i18n.localize("CMPBrowser.cleric"), - druid: game.i18n.localize("CMPBrowser.druid"), - fighter: "Fighter", - monk: "Monk", - paladin: game.i18n.localize("CMPBrowser.paladin"), - ranger: game.i18n.localize("CMPBrowser.ranger"), - rogue: "Rogue", - sorcerer: game.i18n.localize("CMPBrowser.sorcerer"), - warlock: game.i18n.localize("CMPBrowser.warlock"), - wizard: game.i18n.localize("CMPBrowser.wizard") - }, true); - - game.compendiumBrowser.addFeatFilter("Game Mechanics", game.i18n.localize("DND5E.ItemActivationCost"), 'data.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes); - game.compendiumBrowser.addFeatFilter("Game Mechanics", game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes); - game.compendiumBrowser.addFeatFilter("Game Mechanics", "Uses Resources", 'usesRessources', 'bool'); - - - // Item Filters - - game.compendiumBrowser.addItemFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.source', 'text'); - game.compendiumBrowser.addItemFilter(game.i18n.localize("CMPBrowser.general"), "Item Type", 'type', 'select', { - consumable: game.i18n.localize("DND5E.ItemTypeConsumable"), - backpack: game.i18n.localize("DND5E.ItemTypeContainer"), - equipment: game.i18n.localize("DND5E.ItemTypeEquipment"), - loot: game.i18n.localize("DND5E.ItemTypeLoot"), - tool: game.i18n.localize("DND5E.ItemTypeTool"), - weapon: game.i18n.localize("DND5E.ItemTypeWeapon") - }); - game.compendiumBrowser.addItemFilter(game.i18n.localize("CMPBrowser.general"), "Packs", 'matchedPacks', 'select', - { - burglar: "Burglar's Pack", - diplomat: "Diplomat's Pack", - dungeoneer: "Dungeoneer's Pack", - entertainer: "Entertainer's Pack", - explorer: "Explorer's Pack", - monsterhunter: "Monster Hunter's Pack", - priest: "Priest's Pack", - scholar: "Scholar's Pack", - }, true); - - game.compendiumBrowser.addItemFilter("Game Mechanics", game.i18n.localize("DND5E.ItemActivationCost"), 'data.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes); - game.compendiumBrowser.addItemFilter("Game Mechanics", game.i18n.localize("CMPBrowser.damageType"), 'damageTypes', 'select', CONFIG.DND5E.damageTypes); - game.compendiumBrowser.addItemFilter("Game Mechanics", "Uses Resources", 'usesRessources', 'bool'); - - game.compendiumBrowser.addItemFilter("Item Subtype", "Weapon", 'data.weaponType', 'text', CONFIG.DND5E.weaponTypes); - game.compendiumBrowser.addItemFilter("Item Subtype", "Equipment", 'data.armor.type', 'text', CONFIG.DND5E.equipmentTypes); - game.compendiumBrowser.addItemFilter("Item Subtype", "Consumable", 'data.consumableType', 'text', CONFIG.DND5E.consumableTypes); - - game.compendiumBrowser.addItemFilter("Magic Items", "Rarity", 'data.rarity', 'select', { - Common: "Common", - Uncommon: "Uncommon", - Rare: "Rare", - "Very rare": "Very Rare", - Legendary: "Legendary" - }); - - // NPC Filters - - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("DND5E.Source"), 'data.details.source', 'text'); - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.size"), 'data.traits.size', 'select', CONFIG.DND5E.actorSizes); - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasSpells"), 'hasSpells', 'bool'); - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegAct"), 'data.resources.legact.max', 'bool'); - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.hasLegRes"), 'data.resources.legres.max', 'bool'); - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.cr"), 'data.details.cr', 'numberCompare'); - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.general"), game.i18n.localize("CMPBrowser.creatureType"), 'data.details.type', 'text', { - aberration: game.i18n.localize("CMPBrowser.aberration"), - beast: game.i18n.localize("CMPBrowser.beast"), - celestial: game.i18n.localize("CMPBrowser.celestial"), - construct: game.i18n.localize("CMPBrowser.construct"), - dragon: game.i18n.localize("CMPBrowser.dragon"), - elemental: game.i18n.localize("CMPBrowser.elemental"), - fey: game.i18n.localize("CMPBrowser.fey"), - fiend: game.i18n.localize("CMPBrowser.fiend"), - giant: game.i18n.localize("CMPBrowser.giant"), - humanoid: game.i18n.localize("CMPBrowser.humanoid"), - monstrosity: game.i18n.localize("CMPBrowser.monstrosity"), - ooze: game.i18n.localize("CMPBrowser.ooze"), - plant: game.i18n.localize("CMPBrowser.plant"), - undead: game.i18n.localize("CMPBrowser.undead") - }); - - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityStr"), 'data.abilities.str.value', 'numberCompare'); - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityDex"), 'data.abilities.dex.value', 'numberCompare'); - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCon"), 'data.abilities.con.value', 'numberCompare'); - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityInt"), 'data.abilities.int.value', 'numberCompare'); - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityWis"), 'data.abilities.wis.value', 'numberCompare'); - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.abilities"), game.i18n.localize("DND5E.AbilityCha"), 'data.abilities.cha.value', 'numberCompare'); - - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamImm"), 'data.traits.di.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamRes"), 'data.traits.dr.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.DamVuln"), 'data.traits.dv.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("DND5E.ConImm"), 'data.traits.ci.value', 'multiSelect', CONFIG.DND5E.conditionTypes, true); - game.compendiumBrowser.addNpcFilter(game.i18n.localize("CMPBrowser.dmgInteraction"), game.i18n.localize("CMPBrowser.dmgDealt"), 'damageDealt', 'multiSelect', CONFIG.DND5E.damageTypes, true); -}); \ No newline at end of file +Hooks.on("renderCompendiumBrowser", CompendiumBrowser.afterRender); \ No newline at end of file diff --git a/lang/en.json b/lang/en.json index 5f4b998..501edcc 100644 --- a/lang/en.json +++ b/lang/en.json @@ -55,8 +55,15 @@ "CMPBrowser.dmgInteraction": "Damage Interaction", "CMPBrowser.dmgDealt": "Damage Dealt", "CMPBrowser.size": "Size", - "CMPBrowser.spellBrowser":"Spell Browser", - "CMPBrowser.npcBrowser":"NPC Browser", - "CMPBrowser.settings":"Settings" + "CMPBrowser.Tab.SpellBrowser":"Spell Browser", + "CMPBrowser.Tab.FeatBrowser": "Feat Browser", + "CMPBrowser.Tab.ItemBrowser": "Item Browser", + "CMPBrowser.Tab.NPCBrowser":"NPC Browser", + "CMPBrowser.Tab.Settings":"Settings", + "CMPBrowser.SETTING.Maxload.NAME" : "Maximum load", + "CMPBrowser.SETTING.Maxload.HINT" : "Maximum number of spells, feats, items, or NPCs to display; to see more use the filters. This setting is to allow manageing memory and server load.", + "CMPBrowser.LOADING.Message" : "Loaded...{numLoaded} {itemType}s", + "CMPBrowser.LOADING.MaxLoaded" : "(maximum displayed; to see more, use the filters)", + "CMPBrowser.Filters.ResetFilters" : "Reset Filters" } \ No newline at end of file diff --git a/module.json b/module.json index 3c045ba..004abd4 100644 --- a/module.json +++ b/module.json @@ -1,9 +1,18 @@ { "name": "compendium-browser", "title": "Compendium Browser", - "description": "A module to easily browse and filter spells as well as npcs loaded from compendie.", - "version": "0.3.1", + "description": "

Easily browse and filter spells, feats, items, and npcs loaded from compendia!

NEW! Compendium Browser is faster and better-behaved; it no longer loads all the compendia into memory on start-up (which sometimes hung servers because of memory or CPU requirements). Instead, it filters and loads on-demand, as well as giving you a Module Setting to control how many rows are loaded at a time.
Changes in v0.4.3:", + "version": "0.4.3", "author": "Felix#6196", + "authors": [ + { + "name": "Spetzel#0103", + "url": "https://github.com/spetzel2020" + }, + { + "name": "Felix#6196" + } + ], "systems": ["dnd5e"], "scripts": ["./compendium-browser.js"], "styles": ["./compendium-browser.css"], @@ -25,14 +34,18 @@ "path": "lang/fr.json" }, { - "lang": "pt-BR", - "name": "Português (Brasil)", - "path": "lang/pt-BR.json" - } + "lang": "pt-BR", + "name": "Português (Brasil)", + "path": "lang/pt-BR.json" + } ], - "url": "https://github.com/syl3r86/compendium-browser", - "manifest": "https://raw.githubusercontent.com/syl3r86/compendium-browser/master/module.json", - "download": "https://github.com/syl3r86/compendium-browser/archive/master.zip", + "url": "https://github.com/League-of-Foundry-Developers/compendium-browser", + "manifest": "https://github.com/League-of-Foundry-Developers/compendium-browser/releases/download/latest/module.json", + "download": "https://github.com/League-of-Foundry-Developers/compendium-browser/releases/download/latest/compendium-browser.zip", "minimumCoreVersion": "0.6.2", - "compatibleCoreVersion": "0.6.2" + "compatibleCoreVersion": "0.7.9", + "allowBugReporter": true, + "bugs": "https://github.com/League-of-Foundry-Developers/compendium-browser/issues", + "readme": "https://github.com/League-of-Foundry-Developers/compendium-browser/blob/master/README.md", + "changelog": "https://github.com/League-of-Foundry-Developers/compendium-browser/blob/master/CHANGELOG.md" } diff --git a/template/feat-browser-list.html b/template/feat-browser-list.html new file mode 100644 index 0000000..0fb8d35 --- /dev/null +++ b/template/feat-browser-list.html @@ -0,0 +1,17 @@ +{{#each listItems as |feat id|}} +
  • +
    + +
    +
    + {{feat.name}} +
    +
    + {{feat.classRequirementString}} + +
    +
    + +
    +
  • +{{/each}} diff --git a/template/feat-browser.html b/template/feat-browser.html index 34d8b94..0960e31 100644 --- a/template/feat-browser.html +++ b/template/feat-browser.html @@ -8,31 +8,18 @@
    {{localize "CMPBrowser.sortBy"}}:
    - + {{> "modules/compendium-browser/template/filter-container.html" filters=featFilters}} - +
    + + +
    \ No newline at end of file diff --git a/template/item-browser-list.html b/template/item-browser-list.html new file mode 100644 index 0000000..9beaf6b --- /dev/null +++ b/template/item-browser-list.html @@ -0,0 +1,16 @@ +{{#each listItems as |item id|}} +
  • +
    + +
    +
    + {{item.name}} +
    +
    + {{item.type}} +
    +
    + +
    +
  • +{{/each}} diff --git a/template/item-browser.html b/template/item-browser.html index 603fb56..7b8b217 100644 --- a/template/item-browser.html +++ b/template/item-browser.html @@ -8,30 +8,17 @@
    {{localize "CMPBrowser.sortBy"}}:
    - + {{> "modules/compendium-browser/template/filter-container.html" filters=itemFilters}} - - +
    + + +
    \ No newline at end of file diff --git a/template/loading.html b/template/loading.html new file mode 100644 index 0000000..d558fab --- /dev/null +++ b/template/loading.html @@ -0,0 +1,6 @@ + + + + {{localize "CMPBrowser.LOADING.Message" numLoaded=numLoaded itemType=itemType}} + {{#if maxLoaded}}{{localize "CMPBrowser.LOADING.MaxLoaded"}}{{/if}} + diff --git a/template/npc-browser-list.html b/template/npc-browser-list.html new file mode 100644 index 0000000..818e462 --- /dev/null +++ b/template/npc-browser-list.html @@ -0,0 +1,21 @@ +{{#each listItems as |npc id|}} +
  • +
    + +
    +
    + +
    + CR {{npc.displayCR}} + {{npc.displaySize}} + {{npc.displayType}} +
    +
    + + +
    +
    +
  • +{{/each}} diff --git a/template/npc-browser.html b/template/npc-browser.html index dc0af5f..16167a3 100644 --- a/template/npc-browser.html +++ b/template/npc-browser.html @@ -12,32 +12,14 @@ - + {{> "modules/compendium-browser/template/filter-container.html" filters=npcFilters}} - - +
    + + +
    \ No newline at end of file diff --git a/template/spell-browser-list.html b/template/spell-browser-list.html new file mode 100644 index 0000000..5eb0d56 --- /dev/null +++ b/template/spell-browser-list.html @@ -0,0 +1,25 @@ + +{{#each listItems as |spell id|}} +
  • +
    + +
    +
    + {{spell.name}} +
    +
    + {{#if spell.data.level}}{{spell.data.level}}{{else}}C{{/if}} +
    + R + C +
    + V + S + M +
    +
    + +
    +
  • +{{/each}} + \ No newline at end of file diff --git a/template/spell-browser.html b/template/spell-browser.html index 8005ce5..66c3613 100644 --- a/template/spell-browser.html +++ b/template/spell-browser.html @@ -11,34 +11,14 @@ - + {{> "modules/compendium-browser/template/filter-container.html" filters=spellFilters}} - - +
    + + +
    \ No newline at end of file diff --git a/template/template.html b/template/template.html index 9f331d2..ec5f6dc 100644 --- a/template/template.html +++ b/template/template.html @@ -1,16 +1,16 @@
    - {{#if showSpellBrowser}}{{localize "CMPBrowser.spellBrowser"}}{{/if}} - {{#if showFeatBrowser}}Feat Browser{{/if}} - {{#if showItemBrowser}}Item Browser{{/if}} - {{#if showNpcBrowser}}{{localize "CMPBrowser.npcBrowser"}}{{/if}} - {{#if isGM}}{{localize "CMPBrowser.settings"}}{{/if}} + {{#if showSpellBrowser}}{{localize "CMPBrowser.Tab.SpellBrowser"}}{{/if}} + {{#if showFeatBrowser}}{{localize "CMPBrowser.Tab.FeatBrowser"}}{{/if}} + {{#if showItemBrowser}}{{localize "CMPBrowser.Tab.ItemBrowser"}}{{/if}} + {{#if showNpcBrowser}}{{localize "CMPBrowser.Tab.NPCBrowser"}}{{/if}} + {{#if isGM}}{{localize "CMPBrowser.Tab.Settings"}}{{/if}}
    {{#if showSpellBrowser}}{{> "modules/compendium-browser/template/spell-browser.html"}}{{/if}}
    -
    {{#if showSpellBrowser}}{{> "modules/compendium-browser/template/feat-browser.html"}}{{/if}}
    -
    {{#if showSpellBrowser}}{{> "modules/compendium-browser/template/item-browser.html"}}{{/if}}
    +
    {{#if showFeatBrowser}}{{> "modules/compendium-browser/template/feat-browser.html"}}{{/if}}
    +
    {{#if showItemBrowser}}{{> "modules/compendium-browser/template/item-browser.html"}}{{/if}}
    {{#if showNpcBrowser}} {{> "modules/compendium-browser/template/npc-browser.html"}}{{/if}}
    {{#if isGM}} {{> "modules/compendium-browser/template/settings.html"}}{{/if}}