From 2c267d0c86ff0d93e3d16f8314c30caf63962783 Mon Sep 17 00:00:00 2001 From: Matheus Clemente Date: Wed, 8 Nov 2023 00:03:55 -0300 Subject: [PATCH] Prettier formatting --- Patchnotes.md | 58 +- README.md | 15 +- compendium-browser.css | 16 +- compendium-browser.js | 4948 ++++++++++++++++++++++----------------- compendium-browser.less | 571 ++--- item-packs.json | 113 +- module.json | 144 +- spell-classes.json | 1001 ++++---- sub-classes.json | 109 +- 9 files changed, 3959 insertions(+), 3016 deletions(-) diff --git a/Patchnotes.md b/Patchnotes.md index 33c22e9..5cae1e9 100644 --- a/Patchnotes.md +++ b/Patchnotes.md @@ -1,4 +1,5 @@ #0.9.0 + - Button on character sheets for opening a search with class and spell level (works on default and TidySheet5e) - added filters for subclasses and backgrounds [League-of-Foundry-Developers/compendium-browser#48](https://github.com/League-of-Foundry-Developers/compendium-browser/issues/48) - Additional searches for sub-features @@ -7,64 +8,73 @@ - Fixed item rarity search in Foundry 10+ [League-of-Foundry-Developers/compendium-browser#54](https://github.com/League-of-Foundry-Developers/compendium-browser/issues/54) #0.8.2 + - Fixed Class searching for spells - issue: #43 -- Fixed Drag-and-Drop error in Foundry V10 - issue: #41 -#0.8 +- Fixed Drag-and-Drop error in Foundry V10 - issue: #41 + #0.8 - Merged PR [League-of-Foundry-Developers/compendium-browser#40](https://github.com/League-of-Foundry-Developers/compendium-browser/pull/40): Foundry v10 support -#0.7 -Works only with Foundry v0.8 and 9 -##0.7.2 + #0.7 + Works only with Foundry v0.8 and 9 + ##0.7.2 - Merged PR [League-of-Foundry-Developers/compendium-browser#33](https://github.com/League-of-Foundry-Developers/compendium-browser/pull/33) - Fixed: Issue [League-of-Foundry-Developers/compendium-browser#29](https://github.com/League-of-Foundry-Developers/compendium-browser/issues/29) - Fixed: Issue [League-of-Foundry-Developers/compendium-browser#30](https://github.com/League-of-Foundry-Developers/compendium-browser/issues/30) - Fixed: Issue [League-of-Foundry-Developers/compendium-browser#31](https://github.com/League-of-Foundry-Developers/compendium-browser/issues/31) -- Change message to "Loading..." until we're done, then "Loaded" (also when we hit the maxLoaded) +- Change message to "Loading..." until we're done, then "Loaded" (also when we hit the maxLoaded) ##0.7.1 + - Merged PR [League-of-Foundry-Developers/compendium-browser#26](https://github.com/League-of-Foundry-Developers/compendium-browser/pull/26) - Fixed: Issue [League-of-Foundry-Developers/compendium-browser#25](https://github.com/League-of-Foundry-Developers/compendium-browser/issues/25) ##v0.7.0 (only Foundry 0.8+) + - Merged PR #24 (performance speed ups) - Fixed: Issue [League-of-Foundry-Developers/compendium-browser#19](https://github.com/League-of-Foundry-Developers/compendium-browser/issues/19) - Fixed: Issue [League-of-Foundry-Developers/compendium-browser#7](https://github.com/League-of-Foundry-Developers/compendium-browser/issues/7) ##v0.6.0 + - Merged PR #14, #8 (classes for new spells and Optional Class Features in Tasha's) - Manually merged PR #3 (German and Spanish translations) ##v0.5.0 + - Fixed: Issue #17 (error in filtering NPCs by Creature Type) ##v0.4.5 + - Fixed: [Suggestion] Show compendium source in results; Issue #11 - Fixed: Spells from non-system compendium show up in items tab. Issue#10 ##v0.4.3 ##v0.3.1 - - fixed a bug that prevented loading when Class type items were loaded. - - fixed a bug that prevented disabled the scrollbar in the settings tab. + +- fixed a bug that prevented loading when Class type items were loaded. +- fixed a bug that prevented disabled the scrollbar in the settings tab. ##v0.3.0 - - improved load times by lazyloading images - - fixed some css problems - - new Feature: Feat Browser - - Compendium Browser now has a new section for Feats - - lets you filter by source, class (as set in the requirements field), activation cost, damage type and if it uses ressources - - new Feature: Item Browser - - Compendium Browser now has a new section for all inventory Items - - All item Packs (such as Explorer's Pack) are configured and you can browse a List of all Items contained in a pack! - - this list can be modified by editing the "item-packs.json" file to customize your packs - - many further filters available! + +- improved load times by lazyloading images +- fixed some css problems +- new Feature: Feat Browser + - Compendium Browser now has a new section for Feats + - lets you filter by source, class (as set in the requirements field), activation cost, damage type and if it uses ressources +- new Feature: Item Browser + - Compendium Browser now has a new section for all inventory Items + - All item Packs (such as Explorer's Pack) are configured and you can browse a List of all Items contained in a pack! + - this list can be modified by editing the "item-packs.json" file to customize your packs + - many further filters available! ##v0.2.1 - - fixed an issue that prevented the rendering of the Button to open the browser + +- fixed an issue that prevented the rendering of the Button to open the browser ##v0.2 - - fixed a bug that could prevent proper npc loading - - added a filter for "Source" for both spells and npcs - - migrated to the new TabsV2 - - added a Reset Filters button - - Added the Artificer class, thanks to Tielc#7191 for that +- fixed a bug that could prevent proper npc loading +- added a filter for "Source" for both spells and npcs +- migrated to the new TabsV2 +- added a Reset Filters button +- Added the Artificer class, thanks to Tielc#7191 for that diff --git a/README.md b/README.md index f09507e..6f30ae3 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,21 @@ # Compendium Browser + Tired of scrolling compendia? Easily browse and filter for spells, feats, items, and NPCs using Compendium Browser. 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); ZoltantheDM (Zoltan#8700); eduardopato41 -* **Version**: 0.9.0 -* **Foundry VTT Compatibility**: 9-10 -* **System Compatibility (If applicable)**: dnd5e -* **Translation Support**: en, de (thanks https://github.com/CarnVanBeck), es (thanks https://github.com/JJBocanegra), fr, ja, pt-BR + +- **Authors**: Discord: Spetzel#0103; Felix (felix.mueller.86@web.de); ZoltantheDM (Zoltan#8700); eduardopato41 +- **Version**: 0.9.0 +- **Foundry VTT Compatibility**: 9-10 +- **System Compatibility (If applicable)**: dnd5e +- **Translation Support**: en, de (thanks https://github.com/CarnVanBeck), es (thanks https://github.com/JJBocanegra), fr, ja, pt-BR [Patch Notes](https://github.com/ZoltanTheDM/compendium-browser/blob/master/Patchnotes.md) ## Installation + 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/latest/download/module.json` 3. Open your world and go to Settings>Manage Modules and enable Compendium Browser @@ -20,6 +23,7 @@ Compendium Browser is faster and better-behaved; **it no longer loads all the co ![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 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. @@ -27,6 +31,7 @@ This application enables anyone to add their own custom spell or npc filters via All filters featured in the app are included in this manner and can be found in the compendium-browser.js at around line 726. ## License + Creative Commons Licence
Compendium Browser - a module for Foundry VTT - by Felix Müller is licensed under a Creative Commons Attribution 4.0 International License. This work is licensed under Foundry Virtual Tabletop [EULA - Limited License Agreement for module development v 0.1.6](http://foundryvtt.com/pages/license.html). diff --git a/compendium-browser.css b/compendium-browser.css index 96021bc..ef37fee 100644 --- a/compendium-browser.css +++ b/compendium-browser.css @@ -5,28 +5,28 @@ display: block; } .compendium-browser { - overflow-y: hidden!important; + overflow-y: hidden !important; max-width: 1100px; max-height: 90vh; } .compendium-browser .window-content { - overflow-y: hidden!important; + overflow-y: hidden !important; height: 100%; } .compendium-browser .window-content .parent { - overflow-y: hidden!important; + overflow-y: hidden !important; height: 100%; } .compendium-browser .window-content .parent .content { - overflow-y: hidden!important; + overflow-y: hidden !important; height: calc(100% - 2em); } .compendium-browser .window-content .parent .content .tab { - overflow-y: hidden!important; + overflow-y: hidden !important; height: 100%; } .compendium-browser .window-content .parent .content .tab .browser { - overflow-y: hidden!important; + overflow-y: hidden !important; height: 100%; } .compendium-browser .window-content .parent .content .tab .browser ul { @@ -130,10 +130,10 @@ } .compendium-browser .browser { height: 100%; - overflow-y: hidden!important; + overflow-y: hidden !important; } .compendium-browser .browser .window-content { - overflow-y: hidden!important; + overflow-y: hidden !important; } .compendium-browser .browser ul { float: right; diff --git a/compendium-browser.js b/compendium-browser.js index 6e3879d..c11029f 100644 --- a/compendium-browser.js +++ b/compendium-browser.js @@ -1,2096 +1,2852 @@ -/* 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 - */ -/* -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 -15-Mar-2021 0.4.5: Fix: Spells from non-system compendium show up in items tab. Issue#10 - loadAndFilterItems(): Changed tests to switch + more explicit tests - 0.4.5b Show compendium source in results issue#11 - Try showing compendium in the image mouseover -12-Jun-2021 0.5.0 Test for Foundry 0.8.x in which creature type is now data.details.type.value -9-Spt-2021 CHANGES Removed functions that are disabled in Foundry 0.9.0 - Speed up on spells by using queries - Stops already in progress searches if a new one is started - Handles monster types from older revisions - Uses some built-ins for minor performance improvement -12-Sep-2021 0.7.1 Issue #25 Initialization fails because of corrupted settings - Fix: Check for settings.loadedSpellCompendium and settings.loadedNpcCompendium -1-Jan-2022 0.7.2 Switch to isFoundryV8Plus class variable -4-Jan-2022 0.7.2 Merge PR #33 (thanks kyleady) to improve NPC filtering performance - 0.7.2c Fix rarity encoding (uses camelcase names) (Issue #28) - Check for data.details?.cr in case you have NPCs without details (type=character) - Change message to "Loading..." until we're done, then "Loaded" -5-Jan-2022 0.7.2d decorateNpc(): NPCs without all details or weirdly formed ones should default damageDealt to [] not 0 -13-Sep-2022 0.8.0 Compatibility with Foundry V10 - Added check for Compendium Folders 'phantom' actors (#[tempEntity]) to filter out of NPC list - Fix to handle un-migrated compendiums (they get auto-excluded from the browser even if selected) -*/ - -const CMPBrowser = { - MODULE_NAME : "compendium-browser", - MODULE_VERSION : "0.8.0", - MAXLOAD : 500, //Default for the maximum number to load before displaying a message that you need to filter to see more -} - -const STOP_SEARCH = 'StopSearchException'; - -// JV-080 - Adding a 'not-migrated' exception for v10 if the compendiums are not migrated to the new format (breaks e.g. npc compendium browser) -const NOT_MIGRATED = 'NotMigratedException'; - -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(); - } - - 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/loading.html" - ]); - - - this.hookCompendiumList(); - - //Reset the filters used in the dialog - this.spellFilters = { - registeredFilterCategorys: {}, - activeFilters: {} - }; - this.npcFilters = { - registeredFilterCategorys: {}, - activeFilters: {} - }; - this.featFilters = { - registeredFilterCategorys: {}, - activeFilters: {} - }; - this.itemFilters = { - registeredFilterCategorys: {}, - activeFilters: {} - }; - } - - - /** override */ - _onChangeTab(event, tabs, active) { - super._onChangeTab(event, tabs, active); - const html = this.element; - this.replaceList(html, active, {reload : false}) - } - - - /** override */ - async getData() { - - //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 - }; - - - return data; - } - - 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.getDocument(itemId).then(entity => { - entity.sheet.render(true); - }); - }); - - // 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; - } - if (CompendiumBrowser.isFoundryV10Plus) { - event.dataTransfer.setData("text/plain", JSON.stringify({ - type: pack.documentName, - uuid: `Compendium.${pack.collection}.${li.getAttribute("data-entry-id")}` - })); - } else { - event.dataTransfer.setData("text/plain", JSON.stringify({ - type: pack.documentName, - 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"); - - for (let tab of ["spell", "feat", "item", "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-${tab}-filter`).click(ev => { - this.resetFilters(); - //v0.4.3: Re-render so that we display the filters correctly - this.refreshList = tab; - this.render(); - }); - - //copy Javascript seach to clipboard - html.find(`#copy-search-${tab}`).click(async ev => { - this.copySearchToClipboard(tab); - }); - } - - // 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 = stripDotCharacters(path); - 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 = stripDotCharacters(path); - 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 = stripDotCharacters(path); - 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 = stripDotCharacters(path); - 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 => { - return this.classList = obj; - }); - } - - if (!this.packList) { - this.packList = await fetch('modules/compendium-browser/item-packs.json').then(result => { - return result.json(); - }).then(obj => { - return this.packList = obj; - }); - } - - if (!this.subClasses) { - this.subClasses = await fetch('modules/compendium-browser/sub-classes.json').then(result => { - return result.json(); - }).then(obj => { - return this.subClasses = obj; - }); - } - } - - async loadAndFilterItems(browserTab="spell",updateLoading=null) { - console.log(`Load and Filter Items | Started loading ${browserTab}s`); - console.time("loadAndFilterItems"); - await this.checkListsLoaded(); - - const seachNumber = Date.now(); - - this.CurrentSeachNumber = seachNumber; - - 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 = {}; - - try{ - //Filter the full list, but only save the core compendium information + displayed info - for (let pack of game.packs) { - if (pack.documentName === "Item" && this.settings.loadedSpellCompendium[pack.collection].load) { - //can query just for spells since there is only 1 type - let query = {}; - if (browserTab === "spell") { - query = {type: "spell"}; - } - - //FIXME: How much could we do with the loaded index rather than all content? - //OR filter the content up front for the decoratedItem.type?? - await pack.getDocuments(query).then(content => { - - if (browserTab === "spell"){ - - content.reduce(function(itemsList, item5e) { - if (this.CurrentSeachNumber != seachNumber) throw STOP_SEARCH; - - numItemsLoaded = Object.keys(itemsList).length; - - if (maxLoad <= numItemsLoaded) { - if (updateLoading) {updateLoading(numItemsLoaded, true);} - throw STOP_SEARCH; - } - - const decoratedItem = this.decorateItem(item5e); - - if(decoratedItem && this.passesFilter(decoratedItem, this.spellFilters.activeFilters)){ - itemsList[item5e.id] = { - compendium : pack.collection, - name : decoratedItem.name, - img: decoratedItem.img, - data : { - level : decoratedItem.level, - components : decoratedItem.components - }, - id: item5e.id - }; - } - - return itemsList; - }.bind(this), compactItems); - - } - else if (browserTab === "feat"){ - - content.reduce(function(itemsList, item5e){ - if (this.CurrentSeachNumber != seachNumber) throw STOP_SEARCH; - - numItemsLoaded = Object.keys(itemsList).length; - - if (maxLoad <= numItemsLoaded) { - if (updateLoading) {updateLoading(numItemsLoaded, true);} - throw STOP_SEARCH; - } - - const decoratedItem = this.decorateItem(item5e); - - if(decoratedItem && ["feat","class","subclass", "background"].includes(decoratedItem.type) && this.passesFilter(decoratedItem, this.featFilters.activeFilters)){ - itemsList[item5e.id] = { - compendium : pack.collection, - name : decoratedItem.name, - img: decoratedItem.img, - classRequirementString : decoratedItem.classRequirementString - }; - } - - return itemsList; - }.bind(this), compactItems); - - } - else if (browserTab === "item"){ - - content.reduce(function(itemsList, item5e){ - if (this.CurrentSeachNumber != seachNumber) throw STOP_SEARCH; - - numItemsLoaded = Object.keys(itemsList).length; - - if (maxLoad <= numItemsLoaded) { - if (updateLoading) {updateLoading(numItemsLoaded, true);} - throw STOP_SEARCH; - } - - const decoratedItem = this.decorateItem(item5e); - - if(decoratedItem && !["spell","feat","class","subclass", "background"].includes(decoratedItem.type) && this.passesFilter(decoratedItem, this.itemFilters.activeFilters)){ - itemsList[item5e.id] = { - compendium : pack.collection, - name : decoratedItem.name, - img: decoratedItem.img, - type : decoratedItem.type - } - } - - return itemsList; - }.bind(this), compactItems); - - } - - numItemsLoaded = Object.keys(compactItems).length; - if (updateLoading) {updateLoading(numItemsLoaded, false);} - }); - }//end if pack entity === Item - }//for packs - } - catch(e){ - if (e === STOP_SEARCH){ - //stopping search early - } - else{ - throw e; - } - } - - // this.removeDuplicates(compactItems); -/* - - 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`); - updateLoading(numItemsLoaded, true) - return compactItems; - } - - async loadAndFilterNpcs(updateLoading=null) { - console.log('NPC Browser | Started loading NPCs'); - - const seachNumber = Date.now(); - this.CurrentSeachNumber = seachNumber; - - console.time("loadAndFilterNpcs"); - let npcs = {}; - - const maxLoad = game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? CMPBrowser.MAXLOAD; - - let numNpcsLoaded = 0; - this.npcsLoaded = false; - - - // fields required for displaying and decorating NPCs - let requiredIndexFields; - - if (CompendiumBrowser.isFoundryV11Plus){ - requiredIndexFields = [ - 'name', - 'img', - 'system.details.cr', - 'system.traits.size', - 'system.details.type.value', - ] - } - else if (CompendiumBrowser.isFoundryV10Plus) - { - requiredIndexFields = [ - 'name', - 'img', - 'system.details.cr', - 'system.traits.size', - 'system.details.type', - 'items.type', - 'items.system.damage.parts', - ] - - } - else{ - requiredIndexFields = [ - 'name', - 'img', - 'data.details.cr', - 'data.traits.size', - 'data.details.type', - 'items.type', - 'items.system.damage.parts', - ]; - } - // add any fields required for currently active filters - //also remove the duplicate fields for sanity - const indexFields = [...new Set(requiredIndexFields.concat( - Object.values(this.npcFilters.activeFilters).map(f => f.path) - ))]; - let collectionName = "unknown"; - try{ - for (let pack of game.packs) { - if (pack.documentName == "Actor" && this.settings.loadedNpcCompendium[pack.collection].load) { - await pack.getIndex({fields: indexFields}).then(async content => { - content.reduce(function(actorsList, npc5e){ - if (this.CurrentSeachNumber != seachNumber) {throw STOP_SEARCH;} - - // JV-080: We're in a v10 foundry but the data doesn't have Actor#system - this means index fields won't have populated. Can't 'browse' like this. - if (CompendiumBrowser.isFoundryV10Plus && npc5e.system == undefined) {collectionName = pack.collection; throw NOT_MIGRATED;} - - numNpcsLoaded = Object.keys(npcs).length; - - if (maxLoad <= numNpcsLoaded) { - if (updateLoading) {updateLoading(numNpcsLoaded, true);} - throw STOP_SEARCH; - } - // JV-080: Special case. Compendium Folders creates Actors called #[CF_tempEntity] as placeholders for it's functions. Avoid them - if (npc5e.name != "#[CF_tempEntity]") { - const decoratedNpc = this.decorateNpc(npc5e, indexFields); - if (decoratedNpc && this.passesFilter(decoratedNpc, this.npcFilters.activeFilters)){ - - actorsList[npc5e._id] = { - compendium : pack.collection, - name : decoratedNpc.name, - img: decoratedNpc.img, - displayCR : decoratedNpc.displayCR, - displaySize : decoratedNpc.displaySize, - displayType: decoratedNpc.displayType, - orderCR : decoratedNpc.orderCR, - orderSize : decoratedNpc.filterSize - }; - } - } - return actorsList; - }.bind(this), npcs); - - numNpcsLoaded = Object.keys(npcs).length; - if (updateLoading) {updateLoading(numNpcsLoaded, false);} - - }); - } - //0.4.1 Only preload a limited number and fill more in as needed - } - } - catch(e){ - if (e == STOP_SEARCH){ - //breaking out - } - else if (e == NOT_MIGRATED){ - console.log("Cannot browse compendium %s as it is not migrated to v10 format",collectionName); - } - else{ - console.timeEnd("loadAndFilterNpcs"); - throw e; - } - } - - this.npcsLoaded = true; - console.timeEnd("loadAndFilterNpcs"); - console.log(`NPC Browser | Finished loading NPCs: ${Object.keys(npcs).length} NPCs`); - updateLoading(numNpcsLoaded, true) - return npcs; - } - - - - 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); - }); - } - } - - - - - /* 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; - - if(CompendiumBrowser.postRender){ - CompendiumBrowser.postRender(); - } - } - - 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,doneLoading) => { - if (loadingMessage.length) {this.renderLoading(loadingMessage[0], browserTab, numLoaded, numLoaded>=maxLoad, doneLoading);} - } - updateLoading(0, false); - //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, doneLoading=false) { - if (!messageElement) return; - - let loadingHTML = await renderTemplate("modules/compendium-browser/template/loading.html", {numLoaded: numLoaded, itemType: itemType, maxLoaded: maxLoaded, doneLoading: doneLoading}); - 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) { - list.sort((a, b) => { - let aName = $(a).find('.item-name a')[0].innerHTML; - let bName = $(b).find('.item-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - }); - } else { - list.sort((a, b) => { - let aVal = $(a).find('input[name=level]').val(); - let bVal = $(b).find('input[name=level]').val(); - if (aVal < bVal) return -1; - if (aVal > bVal) return 1; - if (aVal == bVal) { - let aName = $(a).find('.item-name a')[0].innerHTML; - let bName = $(b).find('.item-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - } - }); - } - return list; - } - - sortFeats(list, 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; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - }); - } else { - list.sort((a, b) => { - let aVal = $(a).find('input[name=class]').val(); - let bVal = $(b).find('input[name=class]').val(); - if (aVal < bVal) return -1; - if (aVal > bVal) return 1; - if (aVal == bVal) { - let aName = $(a).find('.item-name a')[0].innerHTML; - let bName = $(b).find('.item-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - } - }); - } - return list; - } - - sortItems(list, 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; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - }); - } else { - list.sort((a, b) => { - let aVal = $(a).find('input[name=type]').val(); - let bVal = $(b).find('input[name=type]').val(); - if (aVal < bVal) return -1; - if (aVal > bVal) return 1; - if (aVal == bVal) { - let aName = $(a).find('.item-name a')[0].innerHTML; - let bName = $(b).find('.item-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - } - }); - } - return list; - } - - sortNpcs(list, orderBy) { - switch (orderBy) { - case 'name': - list.sort((a, b) => { - let aName = $(a).find('.npc-name a')[0].innerHTML; - let bName = $(b).find('.npc-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - }); break; - case 'cr': - list.sort((a, b) => { - let aVal = Number($(a).find('input[name="order.cr"]').val()); - let bVal = Number($(b).find('input[name="order.cr"]').val()); - if (aVal < bVal) return -1; - if (aVal > bVal) return 1; - if (aVal == bVal) { - let aName = $(a).find('.npc-name a')[0].innerHTML; - let bName = $(b).find('.npc-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - } - }); break; - case 'size': - list.sort((a, b) => { - let aVal = $(a).find('input[name="order.size"]').val(); - let bVal = $(b).find('input[name="order.size"]').val(); - if (aVal < bVal) return -1; - if (aVal > bVal) return 1; - if (aVal == bVal) { - let aName = $(a).find('.npc-name a')[0].innerHTML; - let bName = $(b).find('.npc-name a')[0].innerHTML; - if (aName < bName) return -1; - if (aName > bName) return 1; - return 0; - } - }); break; - } - return list; - } - - decorateItem(item5e) { - if (!item5e) return null; - //Decorate and then filter a compendium entry - returns null or the item - - //JV-080 - v10 does away with item.data and everything is under #system but we want to decorate the first level of the item for return - const item = {...item5e} - - //JV-080: Folding these down to base item.x level so we can have v10 Item#system coexist with v9- Item - if (CompendiumBrowser.isFoundryV10Plus) { - item.level = item5e.system?.level; - item.components = item5e.system?.components; - item.damage = item5e.system?.damage; - item.classes = item5e.system?.classes; - item.requirements = item5e.system?.requirements; - } - else { - item = item5e.data; - item.level = item5e.data?.level; - item.components = item5e.data?.level; - item.damage = item.data?.damage; // equivalent to: item5e.data.data.xxx - Ugh. The 'fold down' in v10 makes sense now. - item.classes = item.data?.classes; - item.requirements = item.data?.requirements; - } - // getting damage types (common to all Items, although some won't have any) - item.damageTypes = []; - - if (item.damage && item.damage.parts.length > 0) { - for (let part of item.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.classes = classes.split(','); - } else { - //FIXME: unfoundSpells += cleanSpellName + ','; - } - } else if (item.type === 'feat' || item.type === 'class') { - // getting class - let reqString = item.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; - - //JV-080: In v10 this is only a getter (and will already exist since item = item5e.system) - if (!CompendiumBrowser.isFoundryV10Plus) { - item.hasSave = item5e.hasSave; - } - } else if (item.type === 'subclass') { - //subclasses dont exist lower then version 10 - item.classRequirement = [item.system.classIdentifier]; - item.classRequirementString = item.system.classIdentifier - } 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, indexFields) { - try { - const decoratedNpc = indexFields.reduce((npcDict, item) => { - set(npcDict, item, getPropByString(npc, item)) - return npcDict - }, {}) - - //0.8.0: update for V10 to use actor.system instead of actor.data - let npcData; - - if (CompendiumBrowser.isFoundryV10Plus){ - npcData = npc.system; - } - else{ - npcData = npc.data; - } - - // cr display - let cr = npcData.details?.cr; //0.7.2c: Possibly because of getIndex() use we now have to check for existence of details (doesn't for Character-type NPCs) - if (cr === undefined || cr === '') cr = 0; - else cr = Number(cr); - - // JV-080: moved here because we want the OG number for orderCR but can't depend on .details.cr being present - decoratedNpc.orderCR = cr; - - if (cr > 0 && cr < 1) cr = "1/" + (1 / cr); - decoratedNpc.displayCR = cr; - - decoratedNpc.displaySize = 'unset'; - decoratedNpc.filterSize = 2; - if (npcData.details) { - decoratedNpc.displayType = this.getNPCType(npcData.details.type); - } - else { - decoratedNpc.displayType = game.i18n.localize("CMPBrowser.Unknown") ?? "Unknown"; - } - - if (CONFIG.DND5E.actorSizes[npcData.traits.size] !== undefined) { - decoratedNpc.displaySize = CONFIG.DND5E.actorSizes[npcData.traits.size]; - } - let npcSize; - if (CompendiumBrowser.isFoundryV10Plus) { - npcSize = npc.system.traits.size; - } else { - npcSize = npc.data.traits.size; - } - switch (npcSize) { - 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; - } - - if (CompendiumBrowser.isFoundryV10Minus){ - // getting value for HasSpells and damage types - decoratedNpc.hasSpells = npc.items?.type?.some(itemType => itemType === 'spell'); - let npcDamagePart; - if (CompendiumBrowser.isFoundryV10Plus) { - npcDamagePart = npc.items?.system?.damage?.parts; - } else { - npcDamagePart = npc.items?.data?.damage?.parts; - } - decoratedNpc.damageDealt = npcDamagePart ? npcDamagePart.filter(p => p?.length >= 2).map(p => p[1]) : []; - } - - // JV-080: Think we have covered this off above now. We're making no assumptions and assuring that all decoratedNpc fields are now not 'undef' - //handle poorly constructed npc - //if (npcData.details?.type && !(npcData.details?.type instanceof Object)){ - // npcData.details.type = {value: npcData.details?.type}; - //} - return decoratedNpc; - } - catch(e){ - console.log('%c Error loading NPC:'+npc.name, 'background: white; color: red') - throw e; - } - } - - getNPCType(type){ - if (type instanceof Object){ - return game.i18n.localize(CompendiumBrowser.CREATURE_TYPES[type.value]) ?? type.value; - } - - return type; - } - - filterElements(list, subjects, filters) { - for (let element of list) { - let subject = subjects[element.dataset.entryId]; - if (this.passesFilter(subject, filters) == false) { - $(element).hide(); - } else { - $(element).show(); - } - } - } - - passesFilter(subject, filters) { - for (let filter of Object.values(filters)) { - let prop = getProperty(subject, filter.path); - if (filter.type === 'numberCompare') { - - switch (filter.operator) { - case '=': if (prop != filter.value) { return false; } break; - case '<': if (prop >= filter.value) { return false; } break; - case '>': if (prop <= filter.value) { return false; } break; - } - - continue; - } - if (filter.valIsArray === false) { - if (filter.type === 'text') { - if (prop === undefined) return false; - if (prop.toLowerCase().indexOf(filter.value.toLowerCase()) === -1) { - return false; - } - } else { - if (prop === undefined) return false; - if (filter.value !== undefined && prop !== undefined && prop != filter.value && !(filter.value === true && prop)) { - return false; - } - if (filter.values && filter.values.indexOf(prop) === -1) { - return false; - } - } - } else { - if (prop === undefined) return false; - if (typeof prop === 'object') { - if (filter.value) { - if (prop.indexOf(filter.value) === -1) { - return false; - } - } else if (filter.values) { - for (let val of filter.values) { - if (prop.indexOf(val) !== -1) { - continue; - } - return false; - } - } - } else { - for (let val of filter.values) { - if (prop === val) { - continue; - } - } - return false; - } - } - } - - return true; - } - - //incomplete removal of duplicate items - removeDuplicates(spellList){ - //sort at n log n - let sortedList = Object.values(spellList).sort((a, b) => a.name.localeCompare(b.name)); - - //search through sorted list for duplicates - for (let index = 0; index < sortedList.length - 1;){ - - //all duplicates will be next to eachother - if (sortedList[index].name == sortedList[index + 1].name){ - //duplicate something is getting removed - //TODO choose what to remove rather then the second - let remove = index + 1; - - delete spellList[sortedList[remove].id]; - sortedList.splice(remove, 1); - } - else{ - index++; - } - } - } - - clearObject(obj) { - let newObj = {}; - for (let key in obj) { - if (obj[key] == true) { - newObj[key] = true; - } - } - return newObj; - } - - initSettings() { - let defaultSettings = { - loadedSpellCompendium: {}, - loadedNpcCompendium: {}, - }; - for (let compendium of game.packs) { - if (compendium.documentName === "Item") { - defaultSettings.loadedSpellCompendium[compendium.collection] = { - load: true, - name: `${compendium['metadata']['label']} (${compendium.collection})` - }; - } - if (compendium.documentName === "Actor") { - defaultSettings.loadedNpcCompendium[compendium.collection] = { - load: true, - name: `${compendium['metadata']['label']} (${compendium.collection})` - }; - } - } - // creating game setting container - 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, - type: Object, - scope: 'world', - onChange: settings => { - 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(CMPBrowser.MODULE_NAME, 'settings'); - for (let compKey in defaultSettings.loadedSpellCompendium) { - //v0.7.1 Check for settings.loadedSpellCompendium - if (settings.loadedSpellCompendium && (settings.loadedSpellCompendium[compKey] !== undefined)) { - defaultSettings.loadedSpellCompendium[compKey].load = settings.loadedSpellCompendium[compKey].load; - } - } - for (let compKey in defaultSettings.loadedNpcCompendium) { - //v0.7.1 Check for settings.loadedNpcCompendium - if (settings.loadedNpcCompendium && (settings.loadedNpcCompendium[compKey] !== undefined)) { - defaultSettings.loadedNpcCompendium[compKey].load = settings.loadedNpcCompendium[compKey].load; - } - } - defaultSettings.allowSpellBrowser = settings.allowSpellBrowser ? true : false; - defaultSettings.allowFeatBrowser = settings.allowFeatBrowser ? true : false; - defaultSettings.allowItemBrowser = settings.allowItemBrowser ? true : false; - defaultSettings.allowNpcBrowser = settings.allowNpcBrowser ? true : false; - - if (game.user.isGM) { - game.settings.set(CMPBrowser.MODULE_NAME, 'settings', defaultSettings); - console.log("New default settings set"); - console.log(defaultSettings); - } - this.settings = defaultSettings; - - //0.9.5 Set the CompendiumBrowser.isFoundryV8Plus variable for different code-paths - //If v9, then game.data.version will throw a deprecation warning so test for v9 first - CompendiumBrowser.isFoundryV8Plus = (game.release?.generation >= 10) || (game.data.release?.generation >= 9) || (game.data.version?.startsWith("0.8")); - - // If V10, we need to know this because in v10(+) Item5e#data and Actor#data have changed to Item5e#system and Actor#system - CompendiumBrowser.isFoundryV10Plus = (game.release?.generation >= 10); - CompendiumBrowser.isFoundryV10Minus = ((game.release?.generation <= 10) || (game.data.release?.generation <= 9) || (game.data.version?.startsWith("0.8"))); - - CompendiumBrowser.isFoundryV11Plus = (game.release?.generation >= 11); - } - - saveSettings() { - game.settings.set(CMPBrowser.MODULE_NAME, 'settings', this.settings); - } - - //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; - filter.labelId = stripSpecialCharacters(label); - filter.label = game.i18n.localize(label) ?? label; - filter.type = 'text'; - if (['text', 'bool', 'select', 'multiSelect', 'numberCompare'].indexOf(type) !== -1) { - filter[`is${type}`] = true; - filter.type = type; - } - - if (possibleValues !== null) { - filter.possibleValueIds = possibleValues; - - filter.possibleValues = Object.keys(possibleValues).reduce(function (acc, current) { - acc[current] = game.i18n.localize(possibleValues[current]) ?? possibleValues[current]; - return acc; - }.bind(this), - {}) - } - filter.valIsArray = valIsArray; - - let catId = stripSpecialCharacters(category); - if (this[target].registeredFilterCategorys[catId] === undefined) { - this[target].registeredFilterCategorys[catId] = { - label: game.i18n.localize(category) ?? category, - labelId: catId, - filters: [] - }; - } - this[target].registeredFilterCategorys[catId].filters.push(filter); - - } - - async addSpellFilters() { - // Spellfilters - //Foundry v10+ Item#data is now Item#system - if (CompendiumBrowser.isFoundryV10Plus) { - - this.addSpellFilter("CMPBrowser.general", "DND5E.Source", 'system.source', 'text'); - this.addSpellFilter("CMPBrowser.general", "DND5E.Level", 'system.level', 'multiSelect', {0:"DND5E.SpellCantrip", 1:"1", 2:"2", 3:"3", 4:"4", 5:"5", 6:"6", 7:"7", 8:"8", 9:"9"}); - this.addSpellFilter("CMPBrowser.general", "DND5E.SpellSchool", 'system.school', 'select', CONFIG.DND5E.spellSchools); - this.addSpellFilter("CMPBrowser.general", "CMPBrowser.castingTime", 'system.activation.type', 'select', - { - action: "DND5E.Action", - bonus: "DND5E.BonusAction", - reaction: "DND5E.Reaction", - minute: "DND5E.TimeMinute", - hour: "DND5E.TimeHour", - day: "DND5E.TimeDay" - } - ); - this.addSpellFilter("CMPBrowser.general", "CMPBrowser.spellType", 'system.actionType', 'select', CONFIG.DND5E.itemActionTypes); - this.addSpellFilter("CMPBrowser.general", "CMPBrowser.damageType", 'damageTypes', 'select', CONFIG.DND5E.damageTypes); - //JV-082: Fix for missing "Class" search feature - this.addSpellFilter("CMPBrowser.general", "ITEM.TypeClass", 'classes', 'select', - { - artificer: "CMPBrowser.artificer", - bard: "CMPBrowser.bard", - cleric: "CMPBrowser.cleric", - druid: "CMPBrowser.druid", - paladin: "CMPBrowser.paladin", - ranger: "CMPBrowser.ranger", - sorcerer: "CMPBrowser.sorcerer", - warlock: "CMPBrowser.warlock", - wizard: "CMPBrowser.wizard", - }, true - ); - this.addSpellFilter("DND5E.SpellComponents", "DND5E.Ritual", 'system.components.ritual', 'bool'); - this.addSpellFilter("DND5E.SpellComponents", "DND5E.Concentration", 'system.components.concentration', 'bool'); - this.addSpellFilter("DND5E.SpellComponents", "DND5E.ComponentVerbal", 'system.components.vocal', 'bool'); - this.addSpellFilter("DND5E.SpellComponents", "DND5E.ComponentSomatic", 'system.components.somatic', 'bool'); - this.addSpellFilter("DND5E.SpellComponents", "DND5E.ComponentMaterial", 'system.components.material', 'bool'); - } - else { - this.addSpellFilter("CMPBrowser.general", "DND5E.Source", 'data.source', 'text'); - this.addSpellFilter("CMPBrowser.general", "DND5E.Level", 'data.level', 'multiSelect', {0:"DND5E.SpellCantrip", 1:"1", 2:"2", 3:"3", 4:"4", 5:"5", 6:"6", 7:"7", 8:"8", 9:"9"}); - this.addSpellFilter("CMPBrowser.general", "DND5E.SpellSchool", 'data.school', 'select', CONFIG.DND5E.spellSchools); - this.addSpellFilter("CMPBrowser.general", "CMPBrowser.castingTime", 'data.activation.type', 'select', - { - action: "DND5E.Action", - bonus: "DND5E.BonusAction", - reaction: "DND5E.Reaction", - minute: "DND5E.TimeMinute", - hour: "DND5E.TimeHour", - day: "DND5E.TimeDay" - } - ); - this.addSpellFilter("CMPBrowser.general", "CMPBrowser.spellType", 'data.actionType', 'select', CONFIG.DND5E.itemActionTypes); - this.addSpellFilter("CMPBrowser.general", "CMPBrowser.damageType", 'damageTypes', 'select', CONFIG.DND5E.damageTypes); - this.addSpellFilter("CMPBrowser.general", "ITEM.TypeClass", 'data.classes', 'select', - { - artificer: "CMPBrowser.artificer", - bard: "CMPBrowser.bard", - cleric: "CMPBrowser.cleric", - druid: "CMPBrowser.druid", - paladin: "CMPBrowser.paladin", - ranger: "CMPBrowser.ranger", - sorcerer: "CMPBrowser.sorcerer", - warlock: "CMPBrowser.warlock", - wizard: "CMPBrowser.wizard", - }, true - ); - this.addSpellFilter("DND5E.SpellComponents", "DND5E.Ritual", 'data.components.ritual', 'bool'); - this.addSpellFilter("DND5E.SpellComponents", "DND5E.Concentration", 'data.components.concentration', 'bool'); - this.addSpellFilter("DND5E.SpellComponents", "DND5E.ComponentVerbal", 'data.components.vocal', 'bool'); - this.addSpellFilter("DND5E.SpellComponents", "DND5E.ComponentSomatic", 'data.components.somatic', 'bool'); - this.addSpellFilter("DND5E.SpellComponents", "DND5E.ComponentMaterial", 'data.components.material', 'bool'); - } - } - - async addItemFilters() { - // Item Filters - - // Feature Filters - //Foundry v10+ Item#data is now Item#system - if (CompendiumBrowser.isFoundryV10Plus) { - this.addItemFilter("CMPBrowser.general", "DND5E.Source", 'system.source', 'text'); - } - else - { - this.addItemFilter("CMPBrowser.general", "DND5E.Source", 'data.source', 'text'); - } - - this.addItemFilter("CMPBrowser.general", "Item Type", 'type', 'select', { - consumable: "ITEM.TypeConsumable", - backpack: "ITEM.TypeContainer", - equipment: "ITEM.TypeEquipment", - loot: "ITEM.TypeLoot", - tool: "ITEM.TypeTool", - weapon: "ITEM.TypeWeapon" - }); - this.addItemFilter("CMPBrowser.general", "CMPBrowser.ItemsPacks", 'matchedPacks', 'select', - { - burglar: "CMPBrowser.ItemsPacksBurglar", - diplomat: "CMPBrowser.ItemsPacksDiplomat", - dungeoneer: "CMPBrowser.ItemsPacksDungeoneer", - entertainer: "CMPBrowser.ItemsPacksEntertainer", - explorer: "CMPBrowser.ItemsPacksExplorer", - monsterhunter: "CMPBrowser.ItemsPacksMonsterHunter", - priest: "CMPBrowser.ItemsPacksPriest", - scholar: "CMPBrowser.ItemsPacksScholar", - }, true - ); - if (CompendiumBrowser.isFoundryV10Plus) { - this.addItemFilter("CMPBrowser.GameMechanics", "DND5E.ItemActivationCost", 'system.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes); - } - else { - this.addItemFilter("CMPBrowser.GameMechanics", "DND5E.ItemActivationCost", 'data.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes); - } - - this.addItemFilter("CMPBrowser.GameMechanics", "CMPBrowser.damageType", 'damageTypes', 'select', CONFIG.DND5E.damageTypes); - this.addItemFilter("CMPBrowser.GameMechanics", "CMPBrowser.UsesResources", 'usesRessources', 'bool'); - - if (CompendiumBrowser.isFoundryV10Plus) { - this.addItemFilter("CMPBrowser.ItemSubtype", "ITEM.TypeWeapon", 'system.weaponType', 'text', CONFIG.DND5E.weaponTypes); - this.addItemFilter("CMPBrowser.ItemSubtype", "ITEM.TypeEquipment", 'system.armor.type', 'text', CONFIG.DND5E.equipmentTypes); - this.addItemFilter("CMPBrowser.ItemSubtype", "ITEM.TypeConsumable", 'system.consumableType', 'text', CONFIG.DND5E.consumableTypes); - } - else { - this.addItemFilter("CMPBrowser.ItemSubtype", "ITEM.TypeWeapon", 'data.weaponType', 'text', CONFIG.DND5E.weaponTypes); - this.addItemFilter("CMPBrowser.ItemSubtype", "ITEM.TypeEquipment", 'data.armor.type', 'text', CONFIG.DND5E.equipmentTypes); - this.addItemFilter("CMPBrowser.ItemSubtype", "ITEM.TypeConsumable", 'data.consumableType', 'text', CONFIG.DND5E.consumableTypes); - } - - - if (CompendiumBrowser.isFoundryV10Plus) { - this.addItemFilter("CMPBrowser.MagicItems", "DND5E.Rarity", 'system.rarity', 'select', CONFIG.DND5E.itemRarity); - } - else { - //0.7.2c: Fix rarity encoding (uses camelcase names) - this.addItemFilter("CMPBrowser.MagicItems", "DND5E.Rarity", 'data.rarity', 'select', - { - common: "DND5E.ItemRarityCommon", - uncommon: "DND5E.ItemRarityUncommon", - rare: "DND5E.ItemRarityRare", - veryRare: "DND5E.ItemRarityVeryRare", - legendary: "DND5E.ItemRarityLegendary", - }); - } - } - - async addFeatFilters() { - - // Feature Filters - //Foundry v10+ Item#data is now Item#system - if (CompendiumBrowser.isFoundryV10Plus) { - this.addFeatFilter("CMPBrowser.general", "DND5E.Source", 'system.source', 'text'); - } - else - { - this.addFeatFilter("CMPBrowser.general", "DND5E.Source", 'data.source', 'text'); - } - this.addFeatFilter("CMPBrowser.general", "ITEM.TypeClass", 'classRequirement', 'select', - { - artificer: "CMPBrowser.artificer", - barbarian: "CMPBrowser.barbarian", - bard: "CMPBrowser.bard", - cleric: "CMPBrowser.cleric", - druid: "CMPBrowser.druid", - fighter: "CMPBrowser.fighter", - monk: "CMPBrowser.monk", - paladin: "CMPBrowser.paladin", - ranger: "CMPBrowser.ranger", - rogue: "CMPBrowser.rogue", - sorcerer: "CMPBrowser.sorcerer", - warlock: "CMPBrowser.warlock", - wizard: "CMPBrowser.wizard" - }, true); - - let featureTypes = { - class: "ITEM.TypeClass", - feat: "ITEM.TypeFeat", - }; - - //subclasses don't exist lower then version 10 - if (CompendiumBrowser.isFoundryV10Plus) { - featureTypes.subclass = "ITEM.TypeSubclass"; - featureTypes.background = "DND5E.Background"; - } - - this.addFeatFilter("CMPBrowser.general", "CMPBrowser.overall", 'type', 'select', - featureTypes - , false); - - if (CompendiumBrowser.isFoundryV10Plus) { - this.addFeatFilter("CMPBrowser.general", "DND5E.ItemFeatureType", 'system.type.value', 'select', - Object.keys(dnd5e.config.featureTypes).reduce(function(acc, current) { - acc[current] = dnd5e.config.featureTypes[current].label; - return acc; - }, {}) - , false); - } - - if (CompendiumBrowser.isFoundryV10Plus) { - this.addFeatFilter("CMPBrowser.general", "CMPBrowser.subfeature", 'system.type.subtype', 'select', - dnd5e.config.featureTypes.class.subtypes); - } - - if (CompendiumBrowser.isFoundryV10Plus) { - this.addFeatFilter("CMPBrowser.GameMechanics", "DND5E.ItemActivationCost", 'system.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes); - } - else { - this.addFeatFilter("CMPBrowser.GameMechanics", "DND5E.ItemActivationCost", 'data.activation.type', 'select', CONFIG.DND5E.abilityActivationTypes); - } - this.addFeatFilter("CMPBrowser.GameMechanics", "CMPBrowser.damageType", 'damageTypes', 'select', CONFIG.DND5E.damageTypes); - this.addFeatFilter("CMPBrowser.GameMechanics", "CMPBrowser.UsesResources", 'usesRessources', 'bool'); - - - } - - static CREATURE_TYPES = { - aberration: "DND5E.CreatureAberration", - beast: "DND5E.CreatureBeast", - celestial: "DND5E.CreatureCelestial", - construct: "DND5E.CreatureConstruct", - dragon: "DND5E.CreatureDragon", - elemental: "DND5E.CreatureElemental", - fey: "DND5E.CreatureFey", - fiend: "DND5E.CreatureFiend", - giant: "DND5E.CreatureGiant", - humanoid: "DND5E.CreatureHumanoid", - monstrosity: "DND5E.CreatureMonstrosity", - ooze: "DND5E.CreatureOoze", - plant: "DND5E.CreaturePlant", - undead: "DND5E.CreatureUndead" - } - - async addNpcFilters() { - // NPC Filters - - //Foundry v10+ Actor#data is now Actor#system - if (CompendiumBrowser.isFoundryV10Plus) { - this.addNpcFilter("CMPBrowser.general", "DND5E.Source", 'system.details.source', 'text'); - this.addNpcFilter("CMPBrowser.general", "DND5E.Size", 'system.traits.size', 'select', CONFIG.DND5E.actorSizes); - - if (CompendiumBrowser.isFoundryV10Minus){ - this.addNpcFilter("CMPBrowser.general", "CMPBrowser.hasSpells", 'hasSpells', 'bool'); - } - - this.addNpcFilter("CMPBrowser.general", "CMPBrowser.hasLegAct", 'system.resources.legact.max', 'bool'); - this.addNpcFilter("CMPBrowser.general", "CMPBrowser.hasLegRes", 'system.resources.legres.max', 'bool'); - this.addNpcFilter("CMPBrowser.general", "DND5E.ChallengeRating", 'system.details.cr', 'numberCompare'); - } - else { - this.addNpcFilter("CMPBrowser.general", "DND5E.Source", 'data.details.source', 'text'); - this.addNpcFilter("CMPBrowser.general", "DND5E.Size", 'data.traits.size', 'select', CONFIG.DND5E.actorSizes); - this.addNpcFilter("CMPBrowser.general", "CMPBrowser.hasSpells", 'hasSpells', 'bool'); - this.addNpcFilter("CMPBrowser.general", "CMPBrowser.hasLegAct", 'data.resources.legact.max', 'bool'); - this.addNpcFilter("CMPBrowser.general", "CMPBrowser.hasLegRes", 'data.resources.legres.max', 'bool'); - this.addNpcFilter("CMPBrowser.general", "DND5E.ChallengeRating", 'data.details.cr', 'numberCompare'); - } - - let npcDetailsPath; - //Foundry v10+ Actor#data is now Actor#system - if (CompendiumBrowser.isFoundryV10Plus) { - npcDetailsPath = "system.details.type.value"; - } - //Foundry 0.8.x: Creature type (data.details.type) is now a structure, so we check data.details.types.value instead - else if (CompendiumBrowser.isFoundryV8Plus) { - npcDetailsPath = "data.details.type.value"; - } - else {//0.7.x - npcDetailsPath = "data.details.type"; - } - - this.addNpcFilter("CMPBrowser.general", "DND5E.CreatureType", npcDetailsPath, 'select', CompendiumBrowser.CREATURE_TYPES); - //Foundry v10+ Actor#data is now Actor#system - if (CompendiumBrowser.isFoundryV10Plus) { - this.addNpcFilter("DND5E.Abilities", "DND5E.AbilityStr", 'system.abilities.str.value', 'numberCompare'); - this.addNpcFilter("DND5E.Abilities", "DND5E.AbilityDex", 'system.abilities.dex.value', 'numberCompare'); - this.addNpcFilter("DND5E.Abilities", "DND5E.AbilityCon", 'system.abilities.con.value', 'numberCompare'); - this.addNpcFilter("DND5E.Abilities", "DND5E.AbilityInt", 'system.abilities.int.value', 'numberCompare'); - this.addNpcFilter("DND5E.Abilities", "DND5E.AbilityWis", 'system.abilities.wis.value', 'numberCompare'); - this.addNpcFilter("DND5E.Abilities", "DND5E.AbilityCha", 'system.abilities.cha.value', 'numberCompare'); - - this.addNpcFilter("CMPBrowser.dmgInteraction", "DND5E.DamImm", 'system.traits.di.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); - this.addNpcFilter("CMPBrowser.dmgInteraction", "DND5E.DamRes", 'system.traits.dr.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); - this.addNpcFilter("CMPBrowser.dmgInteraction", "DND5E.DamVuln", 'system.traits.dv.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); - this.addNpcFilter("CMPBrowser.dmgInteraction", "DND5E.ConImm", 'system.traits.ci.value', 'multiSelect', CONFIG.DND5E.conditionTypes, true); - } - else - { - this.addNpcFilter("DND5E.Abilities", "DND5E.AbilityStr", 'data.abilities.str.value', 'numberCompare'); - this.addNpcFilter("DND5E.Abilities", "DND5E.AbilityDex", 'data.abilities.dex.value', 'numberCompare'); - this.addNpcFilter("DND5E.Abilities", "DND5E.AbilityCon", 'data.abilities.con.value', 'numberCompare'); - this.addNpcFilter("DND5E.Abilities", "DND5E.AbilityInt", 'data.abilities.int.value', 'numberCompare'); - this.addNpcFilter("DND5E.Abilities", "DND5E.AbilityWis", 'data.abilities.wis.value', 'numberCompare'); - this.addNpcFilter("DND5E.Abilities", "DND5E.AbilityCha", 'data.abilities.cha.value', 'numberCompare'); - - this.addNpcFilter("CMPBrowser.dmgInteraction", "DND5E.DamImm", 'data.traits.di.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); - this.addNpcFilter("CMPBrowser.dmgInteraction", "DND5E.DamRes", 'data.traits.dr.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); - this.addNpcFilter("CMPBrowser.dmgInteraction", "DND5E.DamVuln", 'data.traits.dv.value', 'multiSelect', CONFIG.DND5E.damageTypes, true); - this.addNpcFilter("CMPBrowser.dmgInteraction", "DND5E.ConImm", 'data.traits.ci.value', 'multiSelect', CONFIG.DND5E.conditionTypes, true); - - } - - if (CompendiumBrowser.isFoundryV10Minus){ - this.addNpcFilter("CMPBrowser.dmgInteraction", "CMPBrowser.dmgDealt", 'damageDealt', 'multiSelect', CONFIG.DND5E.damageTypes, true); - } - } - - /** - * Used to add custom filters to the Spell-Browser - * @param {String} category - Title of the category - * @param {String} label - Title of the filter - * @param {String} path - path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value - * @param {String} type - type of filter - * possible filter: - * text: will give a textinput (or use a select if possibleValues has values) to compare with the data. will use objectData.indexOf(searchedText) to enable partial matching - * bool: will see if the data at the path exists and not false. - * select: exactly matches the data with the chosen selector from possibleValues - * multiSelect: enables selecting multiple values from possibleValues, any of witch has to match the objects data - * numberCompare: gives the option to compare numerical values, either with =, < or the > operator - * @param {Boolean} possibleValues - predetermined values to choose from. needed for select and multiSelect, can be used in text filters - * @param {Boolean} valIsArray - if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden - */ - addSpellFilter(category, label, path, type, possibleValues = null, valIsArray = false) { - this.addFilter('spell', category, label, path, type, possibleValues, valIsArray); - } - - /** - * Used to add custom filters to the Spell-Browser - * @param {String} category - Title of the category - * @param {String} label - Title of the filter - * @param {String} path - path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value - * @param {String} type - type of filter - * possible filter: - * text: will give a textinput (or use a select if possibleValues has values) to compare with the data. will use objectData.indexOf(searchedText) to enable partial matching - * bool: will see if the data at the path exists and not false. - * select: exactly matches the data with the chosen selector from possibleValues - * multiSelect: enables selecting multiple values from possibleValues, any of witch has to match the objects data - * numberCompare: gives the option to compare numerical values, either with =, < or the > operator - * @param {Boolean} possibleValues - predetermined values to choose from. needed for select and multiSelect, can be used in text filters - * @param {Boolean} valIsArray - if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden - */ - addNpcFilter(category, label, path, type, possibleValues = null, valIsArray = false) { - this.addFilter('npc', category, label, path, type, possibleValues, valIsArray); - } - - addFeatFilter(category, label, path, type, possibleValues = null, valIsArray = false) { - this.addFilter('feat', category, label, path, type, possibleValues, valIsArray); - } - - addItemFilter(category, label, path, type, possibleValues = null, valIsArray = false) { - this.addFilter('item', category, label, path, type, possibleValues, valIsArray); - } - - async renderWith(tab="spell", filters=[]){ - - //if there isn't a tab error out - if(!this[`${tab}Filters`]){ - ui.notifications.warn(`no tab by name ${tab}`); - return - } - - this.resetFilters(); - - this.refreshList = tab; - - let html = await this.render(); - - let activateFilters = filters.reduce((acc, input) => { - let filter = this.findFilter(tab, input.section, input.label); - - if (filter){ - if (input.value){ - filter.value = input.value; - } - else if(input.values){ - filter.values = input.values; - } - else{ - ui.notifications.warn(`no value(s) in filter:${tab} ${input.section}, ${input.label}`); - } - - acc[stripSpecialCharacters(filter.path)] = filter; - } - else{ - ui.notifications.warn(`filter not found: tab:${tab} ${input.section}, ${input.label}.`); - } - - return acc; - }, - {}) - - this[`${tab}Filters`].activeFilters = activateFilters; - - //wait for after the afterRender function to change tabs - //this avoids some errors when initially opening the window - CompendiumBrowser.postRender = async ()=>{ - - CompendiumBrowser.postRender = ()=>{}; - - await html.activateTab(tab); - - for (let input of filters){ - let filter = this.findFilter(tab, input.section, input.label); - - if (!filter){ - continue; - } - - const typeMap = { - select:"select", - bool: "select", - text: "input", - } - - if (filter.type in typeMap){ - let component = html.element.find(`div.tab.active #${input.section}-${input.label} ${typeMap[filter.type]}`) - - component[0].value = input.value; - } - else if (filter.type == "multiSelect"){ - let components = html.element.find(`div.tab.active #${input.section}-${input.label}`) - - for (let v of input.values){ - let c = components.find(`input[data-value=${v}]`) - c.prop( "checked", true ); - } - } - else{ - ui.notifications.warn(`Unknown filter type?`); - console.log(filter) - } - - } - - }; - - this.render(true); - - return this - } - - findFilter(type, category, label){ - let target = `${type}Filters`; - let catId = stripSpecialCharacters(category); - - if (!this[target].registeredFilterCategorys[catId]){ - return; - } - - let filter = this[target].registeredFilterCategorys[catId].filters. - find(x => x.labelId == label) - - if (!filter){ - return; - } - - return { - path: filter.path, - type: filter.type, - valIsArray: filter.valIsArray, - } - } - - async copySearchToClipboard(tab){ - const text = this.getSearchText(tab) - - try { - await navigator.clipboard.writeText(text); - ui.notifications.info("Javascript Copied to clipboard") - } catch (err) { - ui.notifications.warn("failed to copy javascript to clipboard, check logs for string") - console.error('Failed to copy: ', err); - console.log(text); - } - } - - getSearchText(tab){ - const target = `${tab}Filters` - - //map active filters to their labels - let output = Object.values(this[target].activeFilters).map(filter => { - //find Filters from paths - let out = this.findFilterR(target, filter) - - if(filter.value){ - out.value = filter.value; - } - else if(filter.values){ - out.values = filter.values; - } - - return out; - }) - - const strOut = `game.compendiumBrowser.renderWith("${tab}", ${JSON.stringify(output)})` - - return strOut; - } - - findFilterR(target, filterTarget){ - for (let cat of Object.keys(this[target].registeredFilterCategorys)){ - for (let filter of this[target].registeredFilterCategorys[cat].filters){ - if (filterTarget.path == filter.path){ - return {section:`${cat}`, label:`${filter.labelId}`} - } - } - } - - ui.notifications.warn("Could not find the filter!!") - console.warn(filterTarget) - return; - } - - static async addTidySheetButton(cb, html, actor){ - - await html.find('.spell-browser-btn').remove(); - - let tabBar = html.find("div.tab.spellbook .spellcasting-ability") - const cbButton = $(`
`); - - tabBar.append(cbButton) - - CompendiumBrowser.addSpellsButton(cbButton, actor.actor) - } - - static async addDefaultSheetButton(cb, html, actor){ - - await html.find('.spell-browser-btn').remove(); - - let tabBar = html.find("div.spellbook-filters") - const cbButton = $(`
`); - console.log(tabBar) - - tabBar.append(cbButton) - - CompendiumBrowser.addSpellsButton(cbButton, actor.actor) - } - - static addSpellsButton(cbButton, character){ - - cbButton.click(async ev => { - ev.preventDefault(); - - let target = []; - - target = target.concat(CompendiumBrowser.findCasterClass(character)); - target = target.concat(CompendiumBrowser.findMaxCasterLevel(character)); - - game.compendiumBrowser.renderWith("spell", target); - }); - - } - - static async addASISheetButton(cb, html){ - - await html.find('.feat-browser-btn').remove(); - - let dropArea = html.find("div.drop-area") - const cbButton = $(`
`); - - dropArea.append(cbButton) - - cbButton.click(async ev => { - ev.preventDefault(); - - game.compendiumBrowser.renderWith("feat", [{"section":"CMPBrowsergeneral","label":"DND5EItemFeatureType", value: "feat" }]); - }); - } - - //find the first caster class of the character - static findCasterClass(character){ - const options = ["artificer", "bard", "cleric", "druid", "paladin", "ranger", "sorcerer", "warlock", "wizard"] - - for (let cls of Object.keys(character.classes)){ - if (options.includes(cls)){ - return [{"section":"CMPBrowsergeneral","label":"ITEMTypeClass","value":cls}]; - } - } - - return []; - } - - static findMaxCasterLevel(character){ - - //find max spell level - let maxLevel = Object.keys(character.system.spells).reduce((acc, spell)=>{ - //special case for pact magic - if (spell == "pact"){ - return Math.max(character.system.spells[spell].level, acc) - } - else{ - let spellObject = character.system.spells[spell]; - if ((spellObject.override ?? spellObject.max) > 0){ - let match = spell.match(/spell(?\d+)/); - return Math.max(parseInt(match.groups.lvl), acc) - } - } - - return acc - }, 0) - - if (maxLevel > 0){ - return [{"section":"CMPBrowsergeneral","label":"DND5ELevel", values: [...Array(maxLevel + 1).keys()]}]; - } - - return []; - } -} - -Hooks.on('ready', async () => { - - if (game.compendiumBrowser === undefined) { - 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(); - } - - game.compendiumBrowser.addSpellFilters(); - game.compendiumBrowser.addFeatFilters(); - game.compendiumBrowser.addItemFilters(); - game.compendiumBrowser.addNpcFilters(); - -}); - -function stripSpecialCharacters(str){ - return str.replace(/\W/g, ''); -} - -function stripDotCharacters(str){ - return str.replace(/\./g, ''); -} - -function set(obj, path, value) { - var schema = obj; // a moving reference to internal objects within obj - var pList = path.split('.'); - var len = pList.length; - for(var i = 0; i < len-1; i++) { - var elem = pList[i]; - if( !schema[elem] ) schema[elem] = {} - schema = schema[elem]; - } - - schema[pList[len-1]] = value; -} - -function getPropByString(obj, propString) { - if (!propString) - return obj; - - var prop, props = propString.split('.'); - - for (var i = 0, iLen = props.length - 1; i < iLen; i++) { - prop = props[i]; - - var candidate = obj[prop]; - if (candidate !== undefined) { - obj = candidate; - } else { - break; - } - } - return obj[props[i]]; -} - -Hooks.on("renderActorSheet5eCharacter", CompendiumBrowser.addDefaultSheetButton); -Hooks.on("renderTidy5eSheet", CompendiumBrowser.addTidySheetButton); -Hooks.on("renderAbilityScoreImprovementFlow", CompendiumBrowser.addASISheetButton); - -Hooks.on("renderCompendiumBrowser", CompendiumBrowser.afterRender); +/* 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 + */ +/* +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 +15-Mar-2021 0.4.5: Fix: Spells from non-system compendium show up in items tab. Issue#10 + loadAndFilterItems(): Changed tests to switch + more explicit tests + 0.4.5b Show compendium source in results issue#11 + Try showing compendium in the image mouseover +12-Jun-2021 0.5.0 Test for Foundry 0.8.x in which creature type is now data.details.type.value +9-Spt-2021 CHANGES Removed functions that are disabled in Foundry 0.9.0 + Speed up on spells by using queries + Stops already in progress searches if a new one is started + Handles monster types from older revisions + Uses some built-ins for minor performance improvement +12-Sep-2021 0.7.1 Issue #25 Initialization fails because of corrupted settings + Fix: Check for settings.loadedSpellCompendium and settings.loadedNpcCompendium +1-Jan-2022 0.7.2 Switch to isFoundryV8Plus class variable +4-Jan-2022 0.7.2 Merge PR #33 (thanks kyleady) to improve NPC filtering performance + 0.7.2c Fix rarity encoding (uses camelcase names) (Issue #28) + Check for data.details?.cr in case you have NPCs without details (type=character) + Change message to "Loading..." until we're done, then "Loaded" +5-Jan-2022 0.7.2d decorateNpc(): NPCs without all details or weirdly formed ones should default damageDealt to [] not 0 +13-Sep-2022 0.8.0 Compatibility with Foundry V10 + Added check for Compendium Folders 'phantom' actors (#[tempEntity]) to filter out of NPC list + Fix to handle un-migrated compendiums (they get auto-excluded from the browser even if selected) +*/ + +const CMPBrowser = { + MODULE_NAME: "compendium-browser", + MODULE_VERSION: "0.8.0", + MAXLOAD: 500, //Default for the maximum number to load before displaying a message that you need to filter to see more +}; + +const STOP_SEARCH = "StopSearchException"; + +// JV-080 - Adding a 'not-migrated' exception for v10 if the compendiums are not migrated to the new format (breaks e.g. npc compendium browser) +const NOT_MIGRATED = "NotMigratedException"; + +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(); + } + + 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/loading.html", + ]); + + this.hookCompendiumList(); + + //Reset the filters used in the dialog + this.spellFilters = { + registeredFilterCategorys: {}, + activeFilters: {}, + }; + this.npcFilters = { + registeredFilterCategorys: {}, + activeFilters: {}, + }; + this.featFilters = { + registeredFilterCategorys: {}, + activeFilters: {}, + }; + this.itemFilters = { + registeredFilterCategorys: {}, + activeFilters: {}, + }; + } + + /** override */ + _onChangeTab(event, tabs, active) { + super._onChangeTab(event, tabs, active); + const html = this.element; + this.replaceList(html, active, { reload: false }); + } + + /** override */ + async getData() { + //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, + }; + + return data; + } + + 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.getDocument(itemId).then((entity) => { + entity.sheet.render(true); + }); + }); + + // 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; + } + if (CompendiumBrowser.isFoundryV10Plus) { + event.dataTransfer.setData( + "text/plain", + JSON.stringify({ + type: pack.documentName, + uuid: `Compendium.${pack.collection}.${li.getAttribute( + "data-entry-id" + )}`, + }) + ); + } else { + event.dataTransfer.setData( + "text/plain", + JSON.stringify({ + type: pack.documentName, + 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"); + + for (let tab of ["spell", "feat", "item", "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-${tab}-filter`).click((ev) => { + this.resetFilters(); + //v0.4.3: Re-render so that we display the filters correctly + this.refreshList = tab; + this.render(); + }); + + //copy Javascript seach to clipboard + html.find(`#copy-search-${tab}`).click(async (ev) => { + this.copySearchToClipboard(tab); + }); + } + + // 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 = stripDotCharacters(path); + 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 = stripDotCharacters(path); + 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 = stripDotCharacters(path); + 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 = stripDotCharacters(path); + 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) => { + return (this.classList = obj); + }); + } + + if (!this.packList) { + this.packList = await fetch("modules/compendium-browser/item-packs.json") + .then((result) => { + return result.json(); + }) + .then((obj) => { + return (this.packList = obj); + }); + } + + if (!this.subClasses) { + this.subClasses = await fetch( + "modules/compendium-browser/sub-classes.json" + ) + .then((result) => { + return result.json(); + }) + .then((obj) => { + return (this.subClasses = obj); + }); + } + } + + async loadAndFilterItems(browserTab = "spell", updateLoading = null) { + console.log(`Load and Filter Items | Started loading ${browserTab}s`); + console.time("loadAndFilterItems"); + await this.checkListsLoaded(); + + const seachNumber = Date.now(); + + this.CurrentSeachNumber = seachNumber; + + 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 = {}; + + try { + //Filter the full list, but only save the core compendium information + displayed info + for (let pack of game.packs) { + if ( + pack.documentName === "Item" && + this.settings.loadedSpellCompendium[pack.collection].load + ) { + //can query just for spells since there is only 1 type + let query = {}; + if (browserTab === "spell") { + query = { type: "spell" }; + } + + //FIXME: How much could we do with the loaded index rather than all content? + //OR filter the content up front for the decoratedItem.type?? + await pack.getDocuments(query).then((content) => { + if (browserTab === "spell") { + content.reduce( + function (itemsList, item5e) { + if (this.CurrentSeachNumber != seachNumber) throw STOP_SEARCH; + + numItemsLoaded = Object.keys(itemsList).length; + + if (maxLoad <= numItemsLoaded) { + if (updateLoading) { + updateLoading(numItemsLoaded, true); + } + throw STOP_SEARCH; + } + + const decoratedItem = this.decorateItem(item5e); + + if ( + decoratedItem && + this.passesFilter( + decoratedItem, + this.spellFilters.activeFilters + ) + ) { + itemsList[item5e.id] = { + compendium: pack.collection, + name: decoratedItem.name, + img: decoratedItem.img, + data: { + level: decoratedItem.level, + components: decoratedItem.components, + }, + id: item5e.id, + }; + } + + return itemsList; + }.bind(this), + compactItems + ); + } else if (browserTab === "feat") { + content.reduce( + function (itemsList, item5e) { + if (this.CurrentSeachNumber != seachNumber) throw STOP_SEARCH; + + numItemsLoaded = Object.keys(itemsList).length; + + if (maxLoad <= numItemsLoaded) { + if (updateLoading) { + updateLoading(numItemsLoaded, true); + } + throw STOP_SEARCH; + } + + const decoratedItem = this.decorateItem(item5e); + + if ( + decoratedItem && + ["feat", "class", "subclass", "background"].includes( + decoratedItem.type + ) && + this.passesFilter( + decoratedItem, + this.featFilters.activeFilters + ) + ) { + itemsList[item5e.id] = { + compendium: pack.collection, + name: decoratedItem.name, + img: decoratedItem.img, + classRequirementString: + decoratedItem.classRequirementString, + }; + } + + return itemsList; + }.bind(this), + compactItems + ); + } else if (browserTab === "item") { + content.reduce( + function (itemsList, item5e) { + if (this.CurrentSeachNumber != seachNumber) throw STOP_SEARCH; + + numItemsLoaded = Object.keys(itemsList).length; + + if (maxLoad <= numItemsLoaded) { + if (updateLoading) { + updateLoading(numItemsLoaded, true); + } + throw STOP_SEARCH; + } + + const decoratedItem = this.decorateItem(item5e); + + if ( + decoratedItem && + ![ + "spell", + "feat", + "class", + "subclass", + "background", + ].includes(decoratedItem.type) && + this.passesFilter( + decoratedItem, + this.itemFilters.activeFilters + ) + ) { + itemsList[item5e.id] = { + compendium: pack.collection, + name: decoratedItem.name, + img: decoratedItem.img, + type: decoratedItem.type, + }; + } + + return itemsList; + }.bind(this), + compactItems + ); + } + + numItemsLoaded = Object.keys(compactItems).length; + if (updateLoading) { + updateLoading(numItemsLoaded, false); + } + }); + } //end if pack entity === Item + } //for packs + } catch (e) { + if (e === STOP_SEARCH) { + //stopping search early + } else { + throw e; + } + } + + // this.removeDuplicates(compactItems); + /* + + 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` + ); + updateLoading(numItemsLoaded, true); + return compactItems; + } + + async loadAndFilterNpcs(updateLoading = null) { + console.log("NPC Browser | Started loading NPCs"); + + const seachNumber = Date.now(); + this.CurrentSeachNumber = seachNumber; + + console.time("loadAndFilterNpcs"); + let npcs = {}; + + const maxLoad = + game.settings.get(CMPBrowser.MODULE_NAME, "maxload") ?? + CMPBrowser.MAXLOAD; + + let numNpcsLoaded = 0; + this.npcsLoaded = false; + + // fields required for displaying and decorating NPCs + let requiredIndexFields; + + if (CompendiumBrowser.isFoundryV11Plus) { + requiredIndexFields = [ + "name", + "img", + "system.details.cr", + "system.traits.size", + "system.details.type.value", + ]; + } else if (CompendiumBrowser.isFoundryV10Plus) { + requiredIndexFields = [ + "name", + "img", + "system.details.cr", + "system.traits.size", + "system.details.type", + "items.type", + "items.system.damage.parts", + ]; + } else { + requiredIndexFields = [ + "name", + "img", + "data.details.cr", + "data.traits.size", + "data.details.type", + "items.type", + "items.system.damage.parts", + ]; + } + // add any fields required for currently active filters + //also remove the duplicate fields for sanity + const indexFields = [ + ...new Set( + requiredIndexFields.concat( + Object.values(this.npcFilters.activeFilters).map((f) => f.path) + ) + ), + ]; + let collectionName = "unknown"; + try { + for (let pack of game.packs) { + if ( + pack.documentName == "Actor" && + this.settings.loadedNpcCompendium[pack.collection].load + ) { + await pack.getIndex({ fields: indexFields }).then(async (content) => { + content.reduce( + function (actorsList, npc5e) { + if (this.CurrentSeachNumber != seachNumber) { + throw STOP_SEARCH; + } + + // JV-080: We're in a v10 foundry but the data doesn't have Actor#system - this means index fields won't have populated. Can't 'browse' like this. + if ( + CompendiumBrowser.isFoundryV10Plus && + npc5e.system == undefined + ) { + collectionName = pack.collection; + throw NOT_MIGRATED; + } + + numNpcsLoaded = Object.keys(npcs).length; + + if (maxLoad <= numNpcsLoaded) { + if (updateLoading) { + updateLoading(numNpcsLoaded, true); + } + throw STOP_SEARCH; + } + // JV-080: Special case. Compendium Folders creates Actors called #[CF_tempEntity] as placeholders for it's functions. Avoid them + if (npc5e.name != "#[CF_tempEntity]") { + const decoratedNpc = this.decorateNpc(npc5e, indexFields); + if ( + decoratedNpc && + this.passesFilter( + decoratedNpc, + this.npcFilters.activeFilters + ) + ) { + actorsList[npc5e._id] = { + compendium: pack.collection, + name: decoratedNpc.name, + img: decoratedNpc.img, + displayCR: decoratedNpc.displayCR, + displaySize: decoratedNpc.displaySize, + displayType: decoratedNpc.displayType, + orderCR: decoratedNpc.orderCR, + orderSize: decoratedNpc.filterSize, + }; + } + } + return actorsList; + }.bind(this), + npcs + ); + + numNpcsLoaded = Object.keys(npcs).length; + if (updateLoading) { + updateLoading(numNpcsLoaded, false); + } + }); + } + //0.4.1 Only preload a limited number and fill more in as needed + } + } catch (e) { + if (e == STOP_SEARCH) { + //breaking out + } else if (e == NOT_MIGRATED) { + console.log( + "Cannot browse compendium %s as it is not migrated to v10 format", + collectionName + ); + } else { + console.timeEnd("loadAndFilterNpcs"); + throw e; + } + } + + this.npcsLoaded = true; + console.timeEnd("loadAndFilterNpcs"); + console.log( + `NPC Browser | Finished loading NPCs: ${Object.keys(npcs).length} NPCs` + ); + updateLoading(numNpcsLoaded, true); + return npcs; + } + + 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); + }); + } + } + + /* 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; + + if (CompendiumBrowser.postRender) { + CompendiumBrowser.postRender(); + } + } + + 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, doneLoading) => { + if (loadingMessage.length) { + this.renderLoading( + loadingMessage[0], + browserTab, + numLoaded, + numLoaded >= maxLoad, + doneLoading + ); + } + }; + updateLoading(0, false); + //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, + doneLoading = false + ) { + if (!messageElement) return; + + let loadingHTML = await renderTemplate( + "modules/compendium-browser/template/loading.html", + { + numLoaded: numLoaded, + itemType: itemType, + maxLoaded: maxLoaded, + doneLoading: doneLoading, + } + ); + 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) { + list.sort((a, b) => { + let aName = $(a).find(".item-name a")[0].innerHTML; + let bName = $(b).find(".item-name a")[0].innerHTML; + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + }); + } else { + list.sort((a, b) => { + let aVal = $(a).find("input[name=level]").val(); + let bVal = $(b).find("input[name=level]").val(); + if (aVal < bVal) return -1; + if (aVal > bVal) return 1; + if (aVal == bVal) { + let aName = $(a).find(".item-name a")[0].innerHTML; + let bName = $(b).find(".item-name a")[0].innerHTML; + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + } + }); + } + return list; + } + + sortFeats(list, 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; + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + }); + } else { + list.sort((a, b) => { + let aVal = $(a).find("input[name=class]").val(); + let bVal = $(b).find("input[name=class]").val(); + if (aVal < bVal) return -1; + if (aVal > bVal) return 1; + if (aVal == bVal) { + let aName = $(a).find(".item-name a")[0].innerHTML; + let bName = $(b).find(".item-name a")[0].innerHTML; + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + } + }); + } + return list; + } + + sortItems(list, 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; + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + }); + } else { + list.sort((a, b) => { + let aVal = $(a).find("input[name=type]").val(); + let bVal = $(b).find("input[name=type]").val(); + if (aVal < bVal) return -1; + if (aVal > bVal) return 1; + if (aVal == bVal) { + let aName = $(a).find(".item-name a")[0].innerHTML; + let bName = $(b).find(".item-name a")[0].innerHTML; + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + } + }); + } + return list; + } + + sortNpcs(list, orderBy) { + switch (orderBy) { + case "name": + list.sort((a, b) => { + let aName = $(a).find(".npc-name a")[0].innerHTML; + let bName = $(b).find(".npc-name a")[0].innerHTML; + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + }); + break; + case "cr": + list.sort((a, b) => { + let aVal = Number($(a).find('input[name="order.cr"]').val()); + let bVal = Number($(b).find('input[name="order.cr"]').val()); + if (aVal < bVal) return -1; + if (aVal > bVal) return 1; + if (aVal == bVal) { + let aName = $(a).find(".npc-name a")[0].innerHTML; + let bName = $(b).find(".npc-name a")[0].innerHTML; + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + } + }); + break; + case "size": + list.sort((a, b) => { + let aVal = $(a).find('input[name="order.size"]').val(); + let bVal = $(b).find('input[name="order.size"]').val(); + if (aVal < bVal) return -1; + if (aVal > bVal) return 1; + if (aVal == bVal) { + let aName = $(a).find(".npc-name a")[0].innerHTML; + let bName = $(b).find(".npc-name a")[0].innerHTML; + if (aName < bName) return -1; + if (aName > bName) return 1; + return 0; + } + }); + break; + } + return list; + } + + decorateItem(item5e) { + if (!item5e) return null; + //Decorate and then filter a compendium entry - returns null or the item + + //JV-080 - v10 does away with item.data and everything is under #system but we want to decorate the first level of the item for return + const item = { ...item5e }; + + //JV-080: Folding these down to base item.x level so we can have v10 Item#system coexist with v9- Item + if (CompendiumBrowser.isFoundryV10Plus) { + item.level = item5e.system?.level; + item.components = item5e.system?.components; + item.damage = item5e.system?.damage; + item.classes = item5e.system?.classes; + item.requirements = item5e.system?.requirements; + } else { + item = item5e.data; + item.level = item5e.data?.level; + item.components = item5e.data?.level; + item.damage = item.data?.damage; // equivalent to: item5e.data.data.xxx - Ugh. The 'fold down' in v10 makes sense now. + item.classes = item.data?.classes; + item.requirements = item.data?.requirements; + } + // getting damage types (common to all Items, although some won't have any) + item.damageTypes = []; + + if (item.damage && item.damage.parts.length > 0) { + for (let part of item.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.classes = classes.split(","); + } else { + //FIXME: unfoundSpells += cleanSpellName + ','; + } + } else if (item.type === "feat" || item.type === "class") { + // getting class + let reqString = item.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; + + //JV-080: In v10 this is only a getter (and will already exist since item = item5e.system) + if (!CompendiumBrowser.isFoundryV10Plus) { + item.hasSave = item5e.hasSave; + } + } else if (item.type === "subclass") { + //subclasses dont exist lower then version 10 + item.classRequirement = [item.system.classIdentifier]; + item.classRequirementString = item.system.classIdentifier; + } 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, indexFields) { + try { + const decoratedNpc = indexFields.reduce((npcDict, item) => { + set(npcDict, item, getPropByString(npc, item)); + return npcDict; + }, {}); + + //0.8.0: update for V10 to use actor.system instead of actor.data + let npcData; + + if (CompendiumBrowser.isFoundryV10Plus) { + npcData = npc.system; + } else { + npcData = npc.data; + } + + // cr display + let cr = npcData.details?.cr; //0.7.2c: Possibly because of getIndex() use we now have to check for existence of details (doesn't for Character-type NPCs) + if (cr === undefined || cr === "") cr = 0; + else cr = Number(cr); + + // JV-080: moved here because we want the OG number for orderCR but can't depend on .details.cr being present + decoratedNpc.orderCR = cr; + + if (cr > 0 && cr < 1) cr = "1/" + 1 / cr; + decoratedNpc.displayCR = cr; + + decoratedNpc.displaySize = "unset"; + decoratedNpc.filterSize = 2; + if (npcData.details) { + decoratedNpc.displayType = this.getNPCType(npcData.details.type); + } else { + decoratedNpc.displayType = + game.i18n.localize("CMPBrowser.Unknown") ?? "Unknown"; + } + + if (CONFIG.DND5E.actorSizes[npcData.traits.size] !== undefined) { + decoratedNpc.displaySize = CONFIG.DND5E.actorSizes[npcData.traits.size]; + } + let npcSize; + if (CompendiumBrowser.isFoundryV10Plus) { + npcSize = npc.system.traits.size; + } else { + npcSize = npc.data.traits.size; + } + switch (npcSize) { + 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; + } + + if (CompendiumBrowser.isFoundryV10Minus) { + // getting value for HasSpells and damage types + decoratedNpc.hasSpells = npc.items?.type?.some( + (itemType) => itemType === "spell" + ); + let npcDamagePart; + if (CompendiumBrowser.isFoundryV10Plus) { + npcDamagePart = npc.items?.system?.damage?.parts; + } else { + npcDamagePart = npc.items?.data?.damage?.parts; + } + decoratedNpc.damageDealt = npcDamagePart + ? npcDamagePart.filter((p) => p?.length >= 2).map((p) => p[1]) + : []; + } + + // JV-080: Think we have covered this off above now. We're making no assumptions and assuring that all decoratedNpc fields are now not 'undef' + //handle poorly constructed npc + //if (npcData.details?.type && !(npcData.details?.type instanceof Object)){ + // npcData.details.type = {value: npcData.details?.type}; + //} + return decoratedNpc; + } catch (e) { + console.log( + "%c Error loading NPC:" + npc.name, + "background: white; color: red" + ); + throw e; + } + } + + getNPCType(type) { + if (type instanceof Object) { + return ( + game.i18n.localize(CompendiumBrowser.CREATURE_TYPES[type.value]) ?? + type.value + ); + } + + return type; + } + + filterElements(list, subjects, filters) { + for (let element of list) { + let subject = subjects[element.dataset.entryId]; + if (this.passesFilter(subject, filters) == false) { + $(element).hide(); + } else { + $(element).show(); + } + } + } + + passesFilter(subject, filters) { + for (let filter of Object.values(filters)) { + let prop = getProperty(subject, filter.path); + if (filter.type === "numberCompare") { + switch (filter.operator) { + case "=": + if (prop != filter.value) { + return false; + } + break; + case "<": + if (prop >= filter.value) { + return false; + } + break; + case ">": + if (prop <= filter.value) { + return false; + } + break; + } + + continue; + } + if (filter.valIsArray === false) { + if (filter.type === "text") { + if (prop === undefined) return false; + if (prop.toLowerCase().indexOf(filter.value.toLowerCase()) === -1) { + return false; + } + } else { + if (prop === undefined) return false; + if ( + filter.value !== undefined && + prop !== undefined && + prop != filter.value && + !(filter.value === true && prop) + ) { + return false; + } + if (filter.values && filter.values.indexOf(prop) === -1) { + return false; + } + } + } else { + if (prop === undefined) return false; + if (typeof prop === "object") { + if (filter.value) { + if (prop.indexOf(filter.value) === -1) { + return false; + } + } else if (filter.values) { + for (let val of filter.values) { + if (prop.indexOf(val) !== -1) { + continue; + } + return false; + } + } + } else { + for (let val of filter.values) { + if (prop === val) { + continue; + } + } + return false; + } + } + } + + return true; + } + + //incomplete removal of duplicate items + removeDuplicates(spellList) { + //sort at n log n + let sortedList = Object.values(spellList).sort((a, b) => + a.name.localeCompare(b.name) + ); + + //search through sorted list for duplicates + for (let index = 0; index < sortedList.length - 1; ) { + //all duplicates will be next to eachother + if (sortedList[index].name == sortedList[index + 1].name) { + //duplicate something is getting removed + //TODO choose what to remove rather then the second + let remove = index + 1; + + delete spellList[sortedList[remove].id]; + sortedList.splice(remove, 1); + } else { + index++; + } + } + } + + clearObject(obj) { + let newObj = {}; + for (let key in obj) { + if (obj[key] == true) { + newObj[key] = true; + } + } + return newObj; + } + + initSettings() { + let defaultSettings = { + loadedSpellCompendium: {}, + loadedNpcCompendium: {}, + }; + for (let compendium of game.packs) { + if (compendium.documentName === "Item") { + defaultSettings.loadedSpellCompendium[compendium.collection] = { + load: true, + name: `${compendium["metadata"]["label"]} (${compendium.collection})`, + }; + } + if (compendium.documentName === "Actor") { + defaultSettings.loadedNpcCompendium[compendium.collection] = { + load: true, + name: `${compendium["metadata"]["label"]} (${compendium.collection})`, + }; + } + } + // creating game setting container + 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, + type: Object, + scope: "world", + onChange: (settings) => { + 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(CMPBrowser.MODULE_NAME, "settings"); + for (let compKey in defaultSettings.loadedSpellCompendium) { + //v0.7.1 Check for settings.loadedSpellCompendium + if ( + settings.loadedSpellCompendium && + settings.loadedSpellCompendium[compKey] !== undefined + ) { + defaultSettings.loadedSpellCompendium[compKey].load = + settings.loadedSpellCompendium[compKey].load; + } + } + for (let compKey in defaultSettings.loadedNpcCompendium) { + //v0.7.1 Check for settings.loadedNpcCompendium + if ( + settings.loadedNpcCompendium && + settings.loadedNpcCompendium[compKey] !== undefined + ) { + defaultSettings.loadedNpcCompendium[compKey].load = + settings.loadedNpcCompendium[compKey].load; + } + } + defaultSettings.allowSpellBrowser = settings.allowSpellBrowser + ? true + : false; + defaultSettings.allowFeatBrowser = settings.allowFeatBrowser ? true : false; + defaultSettings.allowItemBrowser = settings.allowItemBrowser ? true : false; + defaultSettings.allowNpcBrowser = settings.allowNpcBrowser ? true : false; + + if (game.user.isGM) { + game.settings.set(CMPBrowser.MODULE_NAME, "settings", defaultSettings); + console.log("New default settings set"); + console.log(defaultSettings); + } + this.settings = defaultSettings; + + //0.9.5 Set the CompendiumBrowser.isFoundryV8Plus variable for different code-paths + //If v9, then game.data.version will throw a deprecation warning so test for v9 first + CompendiumBrowser.isFoundryV8Plus = + game.release?.generation >= 10 || + game.data.release?.generation >= 9 || + game.data.version?.startsWith("0.8"); + + // If V10, we need to know this because in v10(+) Item5e#data and Actor#data have changed to Item5e#system and Actor#system + CompendiumBrowser.isFoundryV10Plus = game.release?.generation >= 10; + CompendiumBrowser.isFoundryV10Minus = + game.release?.generation <= 10 || + game.data.release?.generation <= 9 || + game.data.version?.startsWith("0.8"); + + CompendiumBrowser.isFoundryV11Plus = game.release?.generation >= 11; + } + + saveSettings() { + game.settings.set(CMPBrowser.MODULE_NAME, "settings", this.settings); + } + + //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; + filter.labelId = stripSpecialCharacters(label); + filter.label = game.i18n.localize(label) ?? label; + filter.type = "text"; + if ( + ["text", "bool", "select", "multiSelect", "numberCompare"].indexOf( + type + ) !== -1 + ) { + filter[`is${type}`] = true; + filter.type = type; + } + + if (possibleValues !== null) { + filter.possibleValueIds = possibleValues; + + filter.possibleValues = Object.keys(possibleValues).reduce( + function (acc, current) { + acc[current] = + game.i18n.localize(possibleValues[current]) ?? + possibleValues[current]; + return acc; + }.bind(this), + {} + ); + } + filter.valIsArray = valIsArray; + + let catId = stripSpecialCharacters(category); + if (this[target].registeredFilterCategorys[catId] === undefined) { + this[target].registeredFilterCategorys[catId] = { + label: game.i18n.localize(category) ?? category, + labelId: catId, + filters: [], + }; + } + this[target].registeredFilterCategorys[catId].filters.push(filter); + } + + async addSpellFilters() { + // Spellfilters + //Foundry v10+ Item#data is now Item#system + if (CompendiumBrowser.isFoundryV10Plus) { + this.addSpellFilter( + "CMPBrowser.general", + "DND5E.Source", + "system.source", + "text" + ); + this.addSpellFilter( + "CMPBrowser.general", + "DND5E.Level", + "system.level", + "multiSelect", + { + 0: "DND5E.SpellCantrip", + 1: "1", + 2: "2", + 3: "3", + 4: "4", + 5: "5", + 6: "6", + 7: "7", + 8: "8", + 9: "9", + } + ); + this.addSpellFilter( + "CMPBrowser.general", + "DND5E.SpellSchool", + "system.school", + "select", + CONFIG.DND5E.spellSchools + ); + this.addSpellFilter( + "CMPBrowser.general", + "CMPBrowser.castingTime", + "system.activation.type", + "select", + { + action: "DND5E.Action", + bonus: "DND5E.BonusAction", + reaction: "DND5E.Reaction", + minute: "DND5E.TimeMinute", + hour: "DND5E.TimeHour", + day: "DND5E.TimeDay", + } + ); + this.addSpellFilter( + "CMPBrowser.general", + "CMPBrowser.spellType", + "system.actionType", + "select", + CONFIG.DND5E.itemActionTypes + ); + this.addSpellFilter( + "CMPBrowser.general", + "CMPBrowser.damageType", + "damageTypes", + "select", + CONFIG.DND5E.damageTypes + ); + //JV-082: Fix for missing "Class" search feature + this.addSpellFilter( + "CMPBrowser.general", + "ITEM.TypeClass", + "classes", + "select", + { + artificer: "CMPBrowser.artificer", + bard: "CMPBrowser.bard", + cleric: "CMPBrowser.cleric", + druid: "CMPBrowser.druid", + paladin: "CMPBrowser.paladin", + ranger: "CMPBrowser.ranger", + sorcerer: "CMPBrowser.sorcerer", + warlock: "CMPBrowser.warlock", + wizard: "CMPBrowser.wizard", + }, + true + ); + this.addSpellFilter( + "DND5E.SpellComponents", + "DND5E.Ritual", + "system.components.ritual", + "bool" + ); + this.addSpellFilter( + "DND5E.SpellComponents", + "DND5E.Concentration", + "system.components.concentration", + "bool" + ); + this.addSpellFilter( + "DND5E.SpellComponents", + "DND5E.ComponentVerbal", + "system.components.vocal", + "bool" + ); + this.addSpellFilter( + "DND5E.SpellComponents", + "DND5E.ComponentSomatic", + "system.components.somatic", + "bool" + ); + this.addSpellFilter( + "DND5E.SpellComponents", + "DND5E.ComponentMaterial", + "system.components.material", + "bool" + ); + } else { + this.addSpellFilter( + "CMPBrowser.general", + "DND5E.Source", + "data.source", + "text" + ); + this.addSpellFilter( + "CMPBrowser.general", + "DND5E.Level", + "data.level", + "multiSelect", + { + 0: "DND5E.SpellCantrip", + 1: "1", + 2: "2", + 3: "3", + 4: "4", + 5: "5", + 6: "6", + 7: "7", + 8: "8", + 9: "9", + } + ); + this.addSpellFilter( + "CMPBrowser.general", + "DND5E.SpellSchool", + "data.school", + "select", + CONFIG.DND5E.spellSchools + ); + this.addSpellFilter( + "CMPBrowser.general", + "CMPBrowser.castingTime", + "data.activation.type", + "select", + { + action: "DND5E.Action", + bonus: "DND5E.BonusAction", + reaction: "DND5E.Reaction", + minute: "DND5E.TimeMinute", + hour: "DND5E.TimeHour", + day: "DND5E.TimeDay", + } + ); + this.addSpellFilter( + "CMPBrowser.general", + "CMPBrowser.spellType", + "data.actionType", + "select", + CONFIG.DND5E.itemActionTypes + ); + this.addSpellFilter( + "CMPBrowser.general", + "CMPBrowser.damageType", + "damageTypes", + "select", + CONFIG.DND5E.damageTypes + ); + this.addSpellFilter( + "CMPBrowser.general", + "ITEM.TypeClass", + "data.classes", + "select", + { + artificer: "CMPBrowser.artificer", + bard: "CMPBrowser.bard", + cleric: "CMPBrowser.cleric", + druid: "CMPBrowser.druid", + paladin: "CMPBrowser.paladin", + ranger: "CMPBrowser.ranger", + sorcerer: "CMPBrowser.sorcerer", + warlock: "CMPBrowser.warlock", + wizard: "CMPBrowser.wizard", + }, + true + ); + this.addSpellFilter( + "DND5E.SpellComponents", + "DND5E.Ritual", + "data.components.ritual", + "bool" + ); + this.addSpellFilter( + "DND5E.SpellComponents", + "DND5E.Concentration", + "data.components.concentration", + "bool" + ); + this.addSpellFilter( + "DND5E.SpellComponents", + "DND5E.ComponentVerbal", + "data.components.vocal", + "bool" + ); + this.addSpellFilter( + "DND5E.SpellComponents", + "DND5E.ComponentSomatic", + "data.components.somatic", + "bool" + ); + this.addSpellFilter( + "DND5E.SpellComponents", + "DND5E.ComponentMaterial", + "data.components.material", + "bool" + ); + } + } + + async addItemFilters() { + // Item Filters + + // Feature Filters + //Foundry v10+ Item#data is now Item#system + if (CompendiumBrowser.isFoundryV10Plus) { + this.addItemFilter( + "CMPBrowser.general", + "DND5E.Source", + "system.source", + "text" + ); + } else { + this.addItemFilter( + "CMPBrowser.general", + "DND5E.Source", + "data.source", + "text" + ); + } + + this.addItemFilter("CMPBrowser.general", "Item Type", "type", "select", { + consumable: "ITEM.TypeConsumable", + backpack: "ITEM.TypeContainer", + equipment: "ITEM.TypeEquipment", + loot: "ITEM.TypeLoot", + tool: "ITEM.TypeTool", + weapon: "ITEM.TypeWeapon", + }); + this.addItemFilter( + "CMPBrowser.general", + "CMPBrowser.ItemsPacks", + "matchedPacks", + "select", + { + burglar: "CMPBrowser.ItemsPacksBurglar", + diplomat: "CMPBrowser.ItemsPacksDiplomat", + dungeoneer: "CMPBrowser.ItemsPacksDungeoneer", + entertainer: "CMPBrowser.ItemsPacksEntertainer", + explorer: "CMPBrowser.ItemsPacksExplorer", + monsterhunter: "CMPBrowser.ItemsPacksMonsterHunter", + priest: "CMPBrowser.ItemsPacksPriest", + scholar: "CMPBrowser.ItemsPacksScholar", + }, + true + ); + if (CompendiumBrowser.isFoundryV10Plus) { + this.addItemFilter( + "CMPBrowser.GameMechanics", + "DND5E.ItemActivationCost", + "system.activation.type", + "select", + CONFIG.DND5E.abilityActivationTypes + ); + } else { + this.addItemFilter( + "CMPBrowser.GameMechanics", + "DND5E.ItemActivationCost", + "data.activation.type", + "select", + CONFIG.DND5E.abilityActivationTypes + ); + } + + this.addItemFilter( + "CMPBrowser.GameMechanics", + "CMPBrowser.damageType", + "damageTypes", + "select", + CONFIG.DND5E.damageTypes + ); + this.addItemFilter( + "CMPBrowser.GameMechanics", + "CMPBrowser.UsesResources", + "usesRessources", + "bool" + ); + + if (CompendiumBrowser.isFoundryV10Plus) { + this.addItemFilter( + "CMPBrowser.ItemSubtype", + "ITEM.TypeWeapon", + "system.weaponType", + "text", + CONFIG.DND5E.weaponTypes + ); + this.addItemFilter( + "CMPBrowser.ItemSubtype", + "ITEM.TypeEquipment", + "system.armor.type", + "text", + CONFIG.DND5E.equipmentTypes + ); + this.addItemFilter( + "CMPBrowser.ItemSubtype", + "ITEM.TypeConsumable", + "system.consumableType", + "text", + CONFIG.DND5E.consumableTypes + ); + } else { + this.addItemFilter( + "CMPBrowser.ItemSubtype", + "ITEM.TypeWeapon", + "data.weaponType", + "text", + CONFIG.DND5E.weaponTypes + ); + this.addItemFilter( + "CMPBrowser.ItemSubtype", + "ITEM.TypeEquipment", + "data.armor.type", + "text", + CONFIG.DND5E.equipmentTypes + ); + this.addItemFilter( + "CMPBrowser.ItemSubtype", + "ITEM.TypeConsumable", + "data.consumableType", + "text", + CONFIG.DND5E.consumableTypes + ); + } + + if (CompendiumBrowser.isFoundryV10Plus) { + this.addItemFilter( + "CMPBrowser.MagicItems", + "DND5E.Rarity", + "system.rarity", + "select", + CONFIG.DND5E.itemRarity + ); + } else { + //0.7.2c: Fix rarity encoding (uses camelcase names) + this.addItemFilter( + "CMPBrowser.MagicItems", + "DND5E.Rarity", + "data.rarity", + "select", + { + common: "DND5E.ItemRarityCommon", + uncommon: "DND5E.ItemRarityUncommon", + rare: "DND5E.ItemRarityRare", + veryRare: "DND5E.ItemRarityVeryRare", + legendary: "DND5E.ItemRarityLegendary", + } + ); + } + } + + async addFeatFilters() { + // Feature Filters + //Foundry v10+ Item#data is now Item#system + if (CompendiumBrowser.isFoundryV10Plus) { + this.addFeatFilter( + "CMPBrowser.general", + "DND5E.Source", + "system.source", + "text" + ); + } else { + this.addFeatFilter( + "CMPBrowser.general", + "DND5E.Source", + "data.source", + "text" + ); + } + this.addFeatFilter( + "CMPBrowser.general", + "ITEM.TypeClass", + "classRequirement", + "select", + { + artificer: "CMPBrowser.artificer", + barbarian: "CMPBrowser.barbarian", + bard: "CMPBrowser.bard", + cleric: "CMPBrowser.cleric", + druid: "CMPBrowser.druid", + fighter: "CMPBrowser.fighter", + monk: "CMPBrowser.monk", + paladin: "CMPBrowser.paladin", + ranger: "CMPBrowser.ranger", + rogue: "CMPBrowser.rogue", + sorcerer: "CMPBrowser.sorcerer", + warlock: "CMPBrowser.warlock", + wizard: "CMPBrowser.wizard", + }, + true + ); + + let featureTypes = { + class: "ITEM.TypeClass", + feat: "ITEM.TypeFeat", + }; + + //subclasses don't exist lower then version 10 + if (CompendiumBrowser.isFoundryV10Plus) { + featureTypes.subclass = "ITEM.TypeSubclass"; + featureTypes.background = "DND5E.Background"; + } + + this.addFeatFilter( + "CMPBrowser.general", + "CMPBrowser.overall", + "type", + "select", + featureTypes, + false + ); + + if (CompendiumBrowser.isFoundryV10Plus) { + this.addFeatFilter( + "CMPBrowser.general", + "DND5E.ItemFeatureType", + "system.type.value", + "select", + Object.keys(dnd5e.config.featureTypes).reduce(function (acc, current) { + acc[current] = dnd5e.config.featureTypes[current].label; + return acc; + }, {}), + false + ); + } + + if (CompendiumBrowser.isFoundryV10Plus) { + this.addFeatFilter( + "CMPBrowser.general", + "CMPBrowser.subfeature", + "system.type.subtype", + "select", + dnd5e.config.featureTypes.class.subtypes + ); + } + + if (CompendiumBrowser.isFoundryV10Plus) { + this.addFeatFilter( + "CMPBrowser.GameMechanics", + "DND5E.ItemActivationCost", + "system.activation.type", + "select", + CONFIG.DND5E.abilityActivationTypes + ); + } else { + this.addFeatFilter( + "CMPBrowser.GameMechanics", + "DND5E.ItemActivationCost", + "data.activation.type", + "select", + CONFIG.DND5E.abilityActivationTypes + ); + } + this.addFeatFilter( + "CMPBrowser.GameMechanics", + "CMPBrowser.damageType", + "damageTypes", + "select", + CONFIG.DND5E.damageTypes + ); + this.addFeatFilter( + "CMPBrowser.GameMechanics", + "CMPBrowser.UsesResources", + "usesRessources", + "bool" + ); + } + + static CREATURE_TYPES = { + aberration: "DND5E.CreatureAberration", + beast: "DND5E.CreatureBeast", + celestial: "DND5E.CreatureCelestial", + construct: "DND5E.CreatureConstruct", + dragon: "DND5E.CreatureDragon", + elemental: "DND5E.CreatureElemental", + fey: "DND5E.CreatureFey", + fiend: "DND5E.CreatureFiend", + giant: "DND5E.CreatureGiant", + humanoid: "DND5E.CreatureHumanoid", + monstrosity: "DND5E.CreatureMonstrosity", + ooze: "DND5E.CreatureOoze", + plant: "DND5E.CreaturePlant", + undead: "DND5E.CreatureUndead", + }; + + async addNpcFilters() { + // NPC Filters + + //Foundry v10+ Actor#data is now Actor#system + if (CompendiumBrowser.isFoundryV10Plus) { + this.addNpcFilter( + "CMPBrowser.general", + "DND5E.Source", + "system.details.source", + "text" + ); + this.addNpcFilter( + "CMPBrowser.general", + "DND5E.Size", + "system.traits.size", + "select", + CONFIG.DND5E.actorSizes + ); + + if (CompendiumBrowser.isFoundryV10Minus) { + this.addNpcFilter( + "CMPBrowser.general", + "CMPBrowser.hasSpells", + "hasSpells", + "bool" + ); + } + + this.addNpcFilter( + "CMPBrowser.general", + "CMPBrowser.hasLegAct", + "system.resources.legact.max", + "bool" + ); + this.addNpcFilter( + "CMPBrowser.general", + "CMPBrowser.hasLegRes", + "system.resources.legres.max", + "bool" + ); + this.addNpcFilter( + "CMPBrowser.general", + "DND5E.ChallengeRating", + "system.details.cr", + "numberCompare" + ); + } else { + this.addNpcFilter( + "CMPBrowser.general", + "DND5E.Source", + "data.details.source", + "text" + ); + this.addNpcFilter( + "CMPBrowser.general", + "DND5E.Size", + "data.traits.size", + "select", + CONFIG.DND5E.actorSizes + ); + this.addNpcFilter( + "CMPBrowser.general", + "CMPBrowser.hasSpells", + "hasSpells", + "bool" + ); + this.addNpcFilter( + "CMPBrowser.general", + "CMPBrowser.hasLegAct", + "data.resources.legact.max", + "bool" + ); + this.addNpcFilter( + "CMPBrowser.general", + "CMPBrowser.hasLegRes", + "data.resources.legres.max", + "bool" + ); + this.addNpcFilter( + "CMPBrowser.general", + "DND5E.ChallengeRating", + "data.details.cr", + "numberCompare" + ); + } + + let npcDetailsPath; + //Foundry v10+ Actor#data is now Actor#system + if (CompendiumBrowser.isFoundryV10Plus) { + npcDetailsPath = "system.details.type.value"; + } + //Foundry 0.8.x: Creature type (data.details.type) is now a structure, so we check data.details.types.value instead + else if (CompendiumBrowser.isFoundryV8Plus) { + npcDetailsPath = "data.details.type.value"; + } else { + //0.7.x + npcDetailsPath = "data.details.type"; + } + + this.addNpcFilter( + "CMPBrowser.general", + "DND5E.CreatureType", + npcDetailsPath, + "select", + CompendiumBrowser.CREATURE_TYPES + ); + //Foundry v10+ Actor#data is now Actor#system + if (CompendiumBrowser.isFoundryV10Plus) { + this.addNpcFilter( + "DND5E.Abilities", + "DND5E.AbilityStr", + "system.abilities.str.value", + "numberCompare" + ); + this.addNpcFilter( + "DND5E.Abilities", + "DND5E.AbilityDex", + "system.abilities.dex.value", + "numberCompare" + ); + this.addNpcFilter( + "DND5E.Abilities", + "DND5E.AbilityCon", + "system.abilities.con.value", + "numberCompare" + ); + this.addNpcFilter( + "DND5E.Abilities", + "DND5E.AbilityInt", + "system.abilities.int.value", + "numberCompare" + ); + this.addNpcFilter( + "DND5E.Abilities", + "DND5E.AbilityWis", + "system.abilities.wis.value", + "numberCompare" + ); + this.addNpcFilter( + "DND5E.Abilities", + "DND5E.AbilityCha", + "system.abilities.cha.value", + "numberCompare" + ); + + this.addNpcFilter( + "CMPBrowser.dmgInteraction", + "DND5E.DamImm", + "system.traits.di.value", + "multiSelect", + CONFIG.DND5E.damageTypes, + true + ); + this.addNpcFilter( + "CMPBrowser.dmgInteraction", + "DND5E.DamRes", + "system.traits.dr.value", + "multiSelect", + CONFIG.DND5E.damageTypes, + true + ); + this.addNpcFilter( + "CMPBrowser.dmgInteraction", + "DND5E.DamVuln", + "system.traits.dv.value", + "multiSelect", + CONFIG.DND5E.damageTypes, + true + ); + this.addNpcFilter( + "CMPBrowser.dmgInteraction", + "DND5E.ConImm", + "system.traits.ci.value", + "multiSelect", + CONFIG.DND5E.conditionTypes, + true + ); + } else { + this.addNpcFilter( + "DND5E.Abilities", + "DND5E.AbilityStr", + "data.abilities.str.value", + "numberCompare" + ); + this.addNpcFilter( + "DND5E.Abilities", + "DND5E.AbilityDex", + "data.abilities.dex.value", + "numberCompare" + ); + this.addNpcFilter( + "DND5E.Abilities", + "DND5E.AbilityCon", + "data.abilities.con.value", + "numberCompare" + ); + this.addNpcFilter( + "DND5E.Abilities", + "DND5E.AbilityInt", + "data.abilities.int.value", + "numberCompare" + ); + this.addNpcFilter( + "DND5E.Abilities", + "DND5E.AbilityWis", + "data.abilities.wis.value", + "numberCompare" + ); + this.addNpcFilter( + "DND5E.Abilities", + "DND5E.AbilityCha", + "data.abilities.cha.value", + "numberCompare" + ); + + this.addNpcFilter( + "CMPBrowser.dmgInteraction", + "DND5E.DamImm", + "data.traits.di.value", + "multiSelect", + CONFIG.DND5E.damageTypes, + true + ); + this.addNpcFilter( + "CMPBrowser.dmgInteraction", + "DND5E.DamRes", + "data.traits.dr.value", + "multiSelect", + CONFIG.DND5E.damageTypes, + true + ); + this.addNpcFilter( + "CMPBrowser.dmgInteraction", + "DND5E.DamVuln", + "data.traits.dv.value", + "multiSelect", + CONFIG.DND5E.damageTypes, + true + ); + this.addNpcFilter( + "CMPBrowser.dmgInteraction", + "DND5E.ConImm", + "data.traits.ci.value", + "multiSelect", + CONFIG.DND5E.conditionTypes, + true + ); + } + + if (CompendiumBrowser.isFoundryV10Minus) { + this.addNpcFilter( + "CMPBrowser.dmgInteraction", + "CMPBrowser.dmgDealt", + "damageDealt", + "multiSelect", + CONFIG.DND5E.damageTypes, + true + ); + } + } + + /** + * Used to add custom filters to the Spell-Browser + * @param {String} category - Title of the category + * @param {String} label - Title of the filter + * @param {String} path - path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value + * @param {String} type - type of filter + * possible filter: + * text: will give a textinput (or use a select if possibleValues has values) to compare with the data. will use objectData.indexOf(searchedText) to enable partial matching + * bool: will see if the data at the path exists and not false. + * select: exactly matches the data with the chosen selector from possibleValues + * multiSelect: enables selecting multiple values from possibleValues, any of witch has to match the objects data + * numberCompare: gives the option to compare numerical values, either with =, < or the > operator + * @param {Boolean} possibleValues - predetermined values to choose from. needed for select and multiSelect, can be used in text filters + * @param {Boolean} valIsArray - if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden + */ + addSpellFilter( + category, + label, + path, + type, + possibleValues = null, + valIsArray = false + ) { + this.addFilter( + "spell", + category, + label, + path, + type, + possibleValues, + valIsArray + ); + } + + /** + * Used to add custom filters to the Spell-Browser + * @param {String} category - Title of the category + * @param {String} label - Title of the filter + * @param {String} path - path to the data that the filter uses. uses dotnotation. example: data.abilities.dex.value + * @param {String} type - type of filter + * possible filter: + * text: will give a textinput (or use a select if possibleValues has values) to compare with the data. will use objectData.indexOf(searchedText) to enable partial matching + * bool: will see if the data at the path exists and not false. + * select: exactly matches the data with the chosen selector from possibleValues + * multiSelect: enables selecting multiple values from possibleValues, any of witch has to match the objects data + * numberCompare: gives the option to compare numerical values, either with =, < or the > operator + * @param {Boolean} possibleValues - predetermined values to choose from. needed for select and multiSelect, can be used in text filters + * @param {Boolean} valIsArray - if the objects data is an object use this. the filter will check each property in the object (not recursive). if no match is found, the object will be hidden + */ + addNpcFilter( + category, + label, + path, + type, + possibleValues = null, + valIsArray = false + ) { + this.addFilter( + "npc", + category, + label, + path, + type, + possibleValues, + valIsArray + ); + } + + addFeatFilter( + category, + label, + path, + type, + possibleValues = null, + valIsArray = false + ) { + this.addFilter( + "feat", + category, + label, + path, + type, + possibleValues, + valIsArray + ); + } + + addItemFilter( + category, + label, + path, + type, + possibleValues = null, + valIsArray = false + ) { + this.addFilter( + "item", + category, + label, + path, + type, + possibleValues, + valIsArray + ); + } + + async renderWith(tab = "spell", filters = []) { + //if there isn't a tab error out + if (!this[`${tab}Filters`]) { + ui.notifications.warn(`no tab by name ${tab}`); + return; + } + + this.resetFilters(); + + this.refreshList = tab; + + let html = await this.render(); + + let activateFilters = filters.reduce((acc, input) => { + let filter = this.findFilter(tab, input.section, input.label); + + if (filter) { + if (input.value) { + filter.value = input.value; + } else if (input.values) { + filter.values = input.values; + } else { + ui.notifications.warn( + `no value(s) in filter:${tab} ${input.section}, ${input.label}` + ); + } + + acc[stripSpecialCharacters(filter.path)] = filter; + } else { + ui.notifications.warn( + `filter not found: tab:${tab} ${input.section}, ${input.label}.` + ); + } + + return acc; + }, {}); + + this[`${tab}Filters`].activeFilters = activateFilters; + + //wait for after the afterRender function to change tabs + //this avoids some errors when initially opening the window + CompendiumBrowser.postRender = async () => { + CompendiumBrowser.postRender = () => {}; + + await html.activateTab(tab); + + for (let input of filters) { + let filter = this.findFilter(tab, input.section, input.label); + + if (!filter) { + continue; + } + + const typeMap = { + select: "select", + bool: "select", + text: "input", + }; + + if (filter.type in typeMap) { + let component = html.element.find( + `div.tab.active #${input.section}-${input.label} ${ + typeMap[filter.type] + }` + ); + + component[0].value = input.value; + } else if (filter.type == "multiSelect") { + let components = html.element.find( + `div.tab.active #${input.section}-${input.label}` + ); + + for (let v of input.values) { + let c = components.find(`input[data-value=${v}]`); + c.prop("checked", true); + } + } else { + ui.notifications.warn(`Unknown filter type?`); + console.log(filter); + } + } + }; + + this.render(true); + + return this; + } + + findFilter(type, category, label) { + let target = `${type}Filters`; + let catId = stripSpecialCharacters(category); + + if (!this[target].registeredFilterCategorys[catId]) { + return; + } + + let filter = this[target].registeredFilterCategorys[catId].filters.find( + (x) => x.labelId == label + ); + + if (!filter) { + return; + } + + return { + path: filter.path, + type: filter.type, + valIsArray: filter.valIsArray, + }; + } + + async copySearchToClipboard(tab) { + const text = this.getSearchText(tab); + + try { + await navigator.clipboard.writeText(text); + ui.notifications.info("Javascript Copied to clipboard"); + } catch (err) { + ui.notifications.warn( + "failed to copy javascript to clipboard, check logs for string" + ); + console.error("Failed to copy: ", err); + console.log(text); + } + } + + getSearchText(tab) { + const target = `${tab}Filters`; + + //map active filters to their labels + let output = Object.values(this[target].activeFilters).map((filter) => { + //find Filters from paths + let out = this.findFilterR(target, filter); + + if (filter.value) { + out.value = filter.value; + } else if (filter.values) { + out.values = filter.values; + } + + return out; + }); + + const strOut = `game.compendiumBrowser.renderWith("${tab}", ${JSON.stringify( + output + )})`; + + return strOut; + } + + findFilterR(target, filterTarget) { + for (let cat of Object.keys(this[target].registeredFilterCategorys)) { + for (let filter of this[target].registeredFilterCategorys[cat].filters) { + if (filterTarget.path == filter.path) { + return { section: `${cat}`, label: `${filter.labelId}` }; + } + } + } + + ui.notifications.warn("Could not find the filter!!"); + console.warn(filterTarget); + return; + } + + static async addTidySheetButton(cb, html, actor) { + await html.find(".spell-browser-btn").remove(); + + let tabBar = html.find("div.tab.spellbook .spellcasting-ability"); + const cbButton = $( + `
` + ); + + tabBar.append(cbButton); + + CompendiumBrowser.addSpellsButton(cbButton, actor.actor); + } + + static async addDefaultSheetButton(cb, html, actor) { + await html.find(".spell-browser-btn").remove(); + + let tabBar = html.find("div.spellbook-filters"); + const cbButton = $( + `
` + ); + console.log(tabBar); + + tabBar.append(cbButton); + + CompendiumBrowser.addSpellsButton(cbButton, actor.actor); + } + + static addSpellsButton(cbButton, character) { + cbButton.click(async (ev) => { + ev.preventDefault(); + + let target = []; + + target = target.concat(CompendiumBrowser.findCasterClass(character)); + target = target.concat(CompendiumBrowser.findMaxCasterLevel(character)); + + game.compendiumBrowser.renderWith("spell", target); + }); + } + + static async addASISheetButton(cb, html) { + await html.find(".feat-browser-btn").remove(); + + let dropArea = html.find("div.drop-area"); + const cbButton = $( + `
` + ); + + dropArea.append(cbButton); + + cbButton.click(async (ev) => { + ev.preventDefault(); + + game.compendiumBrowser.renderWith("feat", [ + { + section: "CMPBrowsergeneral", + label: "DND5EItemFeatureType", + value: "feat", + }, + ]); + }); + } + + //find the first caster class of the character + static findCasterClass(character) { + const options = [ + "artificer", + "bard", + "cleric", + "druid", + "paladin", + "ranger", + "sorcerer", + "warlock", + "wizard", + ]; + + for (let cls of Object.keys(character.classes)) { + if (options.includes(cls)) { + return [ + { section: "CMPBrowsergeneral", label: "ITEMTypeClass", value: cls }, + ]; + } + } + + return []; + } + + static findMaxCasterLevel(character) { + //find max spell level + let maxLevel = Object.keys(character.system.spells).reduce((acc, spell) => { + //special case for pact magic + if (spell == "pact") { + return Math.max(character.system.spells[spell].level, acc); + } else { + let spellObject = character.system.spells[spell]; + if ((spellObject.override ?? spellObject.max) > 0) { + let match = spell.match(/spell(?\d+)/); + return Math.max(parseInt(match.groups.lvl), acc); + } + } + + return acc; + }, 0); + + if (maxLevel > 0) { + return [ + { + section: "CMPBrowsergeneral", + label: "DND5ELevel", + values: [...Array(maxLevel + 1).keys()], + }, + ]; + } + + return []; + } +} + +Hooks.on("ready", async () => { + if (game.compendiumBrowser === undefined) { + 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(); + } + + game.compendiumBrowser.addSpellFilters(); + game.compendiumBrowser.addFeatFilters(); + game.compendiumBrowser.addItemFilters(); + game.compendiumBrowser.addNpcFilters(); +}); + +function stripSpecialCharacters(str) { + return str.replace(/\W/g, ""); +} + +function stripDotCharacters(str) { + return str.replace(/\./g, ""); +} + +function set(obj, path, value) { + var schema = obj; // a moving reference to internal objects within obj + var pList = path.split("."); + var len = pList.length; + for (var i = 0; i < len - 1; i++) { + var elem = pList[i]; + if (!schema[elem]) schema[elem] = {}; + schema = schema[elem]; + } + + schema[pList[len - 1]] = value; +} + +function getPropByString(obj, propString) { + if (!propString) return obj; + + var prop, + props = propString.split("."); + + for (var i = 0, iLen = props.length - 1; i < iLen; i++) { + prop = props[i]; + + var candidate = obj[prop]; + if (candidate !== undefined) { + obj = candidate; + } else { + break; + } + } + return obj[props[i]]; +} + +Hooks.on( + "renderActorSheet5eCharacter", + CompendiumBrowser.addDefaultSheetButton +); +Hooks.on("renderTidy5eSheet", CompendiumBrowser.addTidySheetButton); +Hooks.on( + "renderAbilityScoreImprovementFlow", + CompendiumBrowser.addASISheetButton +); + +Hooks.on("renderCompendiumBrowser", CompendiumBrowser.afterRender); diff --git a/compendium-browser.less b/compendium-browser.less index 4b690fb..a263f58 100644 --- a/compendium-browser.less +++ b/compendium-browser.less @@ -1,285 +1,286 @@ -#compendium .directory-footer .compendium-browser-btn { - margin-top:5px; -} - -#compendium .directory-footer { - display:block; -} - -.compendium-browser { - overflow-y: hidden!important; - max-width:1100px; - max-height:90vh; - - .window-content { - overflow-y: hidden!important; - height: 100%; - .parent { - overflow-y: hidden!important; - height: 100%; - .content { - overflow-y: hidden!important; - height: calc(100% - 2em); - .tab { - overflow-y: hidden!important; - height: 100%; - .browser { - overflow-y: hidden!important; - height: 100%; - ul { - overflow-y:auto; - height:100%; - } - } - .settings { - overflow-y:auto; - height:100%; - } - } - } - } - } - - .tabs { - max-height:2em; - border-bottom: solid #782e22; - a { - } - } - .tabContainer { - height:calc(100% - 2em); - .tab { - width: 100%; - height: 100%; - overflow:scroll; - } - } - - .control-area { - position:sticky; - display: block; - min-width: 250px; - max-width: 400px; - width: 300px; - height:100%; - padding-right:5px; - overflow:scroll; - - button { - background: rgba(0, 0, 0, 0.05); - border: 1px solid #bbb; - border-radius: 5px; - margin-top: 5px; - padding:2px; - } - - .filtercontainer { - border: 1px solid #bbb; - border-radius: 5px; - margin-top: 5px; - padding:2px; - - h3 { - margin:0; - cursor:pointer; - } - - dl, div { - margin: 5px 0; - } - - dt { - display:inline-block; - width:40%; - padding-left:5px; - } - - dd { - display:inline-block; - width:58%; - margin-left:0; - - select { - width:100%; - } - } - .multiselect { - border: 1px solid #bbb; - border-radius: 3px; - vertical-align: middle; - line-height:32px; - margin:2px 0; - - label { - padding:5px; - } - input { - vertical-align: middle; - } - } - .small-input { - width: calc(100% - 44px); - height: 27px; - background: rgba(0, 0, 0, 0.05); - border: 1px solid #444; - border-radius: 3px; - padding: 0 3px; - text-overflow: ellipsis; - } - .small-select { - width: 40px; - } - } - } - - .browser { - height: 100%; - overflow-y: hidden!important; - - .window-content { - overflow-y: hidden!important; - } - - ul { - float:right; - display: block; - min-width: 335px; - width: 785px; - margin:0; - height: 100%; - overflow:auto; - padding-left: 5px; - - .filter-tags { - display:none; - } - - li { - span { - white-space: nowrap; - overflow:hidden; - } - } - - } - - .spacer { - display:inline-block; - min-width:5px; - } - .spacer-large { - display:inline-block; - min-width:15px; - } - } - - .item-browser, .feat-browser, .spell-browser { - li { - cursor:default; - vertical-align: middle; - line-height:32px; - margin:2px 0; - - .item-image { - max-width:32px; - height:32px; - } - .item-name { - height:32px; - padding-left:5px; - } - - .feat-tags, .item-tags { - text-align:right; - margin-right:3px; - margin-left: 3px; - text-transform:capitalize; - height:32px; - } - } - } - - .spell-browser { - .spell { - - .spell-level { - text-align:center; - font-weight:900; - max-width:18px; - height:32px; - } - .spell-tags { - text-align:right; - margin-right:3px; - font-weight:900; - max-width:100px; - height:32px; - } - } - } - - .npc-browser { - .npc { - cursor:default; - vertical-align: middle; - line-height:64px; - margin:4px 0; - .npc-image { - max-width: 64px; - height: 64px; - } - .npc-image img { - width: 64px; - height: 64px; - border: none; - object-fit: contain; - } - - .npc-line { - line-height: 25px; - padding: 9px 0 5px 5px; - } - .npc-name { - font-weight:bold; - font-size:16px; - } - .cr { - display: inline-block; - width: 55px; - } - .size { - display: inline-block; - width: 75px; - } - .type { - display: inline-block; - } - } - } - - .settings { - .settings-group { - border: 1px solid #bbb; - border-radius: 5px; - margin-top: 5px; - padding:2px; - - h3 { - margin:0; - cursor:pointer; - } - - label { - display:block; - } - input { - - } - h4 { - display:inline-block; - vertical-align: middle; - height:100%; - } - } - } -} \ No newline at end of file +#compendium .directory-footer .compendium-browser-btn { + margin-top: 5px; +} + +#compendium .directory-footer { + display: block; +} + +.compendium-browser { + overflow-y: hidden !important; + max-width: 1100px; + max-height: 90vh; + + .window-content { + overflow-y: hidden !important; + height: 100%; + .parent { + overflow-y: hidden !important; + height: 100%; + .content { + overflow-y: hidden !important; + height: calc(100% - 2em); + .tab { + overflow-y: hidden !important; + height: 100%; + .browser { + overflow-y: hidden !important; + height: 100%; + ul { + overflow-y: auto; + height: 100%; + } + } + .settings { + overflow-y: auto; + height: 100%; + } + } + } + } + } + + .tabs { + max-height: 2em; + border-bottom: solid #782e22; + a { + } + } + .tabContainer { + height: calc(100% - 2em); + .tab { + width: 100%; + height: 100%; + overflow: scroll; + } + } + + .control-area { + position: sticky; + display: block; + min-width: 250px; + max-width: 400px; + width: 300px; + height: 100%; + padding-right: 5px; + overflow: scroll; + + button { + background: rgba(0, 0, 0, 0.05); + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding: 2px; + } + + .filtercontainer { + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding: 2px; + + h3 { + margin: 0; + cursor: pointer; + } + + dl, + div { + margin: 5px 0; + } + + dt { + display: inline-block; + width: 40%; + padding-left: 5px; + } + + dd { + display: inline-block; + width: 58%; + margin-left: 0; + + select { + width: 100%; + } + } + .multiselect { + border: 1px solid #bbb; + border-radius: 3px; + vertical-align: middle; + line-height: 32px; + margin: 2px 0; + + label { + padding: 5px; + } + input { + vertical-align: middle; + } + } + .small-input { + width: calc(100% - 44px); + height: 27px; + background: rgba(0, 0, 0, 0.05); + border: 1px solid #444; + border-radius: 3px; + padding: 0 3px; + text-overflow: ellipsis; + } + .small-select { + width: 40px; + } + } + } + + .browser { + height: 100%; + overflow-y: hidden !important; + + .window-content { + overflow-y: hidden !important; + } + + ul { + float: right; + display: block; + min-width: 335px; + width: 785px; + margin: 0; + height: 100%; + overflow: auto; + padding-left: 5px; + + .filter-tags { + display: none; + } + + li { + span { + white-space: nowrap; + overflow: hidden; + } + } + } + + .spacer { + display: inline-block; + min-width: 5px; + } + .spacer-large { + display: inline-block; + min-width: 15px; + } + } + + .item-browser, + .feat-browser, + .spell-browser { + li { + cursor: default; + vertical-align: middle; + line-height: 32px; + margin: 2px 0; + + .item-image { + max-width: 32px; + height: 32px; + } + .item-name { + height: 32px; + padding-left: 5px; + } + + .feat-tags, + .item-tags { + text-align: right; + margin-right: 3px; + margin-left: 3px; + text-transform: capitalize; + height: 32px; + } + } + } + + .spell-browser { + .spell { + .spell-level { + text-align: center; + font-weight: 900; + max-width: 18px; + height: 32px; + } + .spell-tags { + text-align: right; + margin-right: 3px; + font-weight: 900; + max-width: 100px; + height: 32px; + } + } + } + + .npc-browser { + .npc { + cursor: default; + vertical-align: middle; + line-height: 64px; + margin: 4px 0; + .npc-image { + max-width: 64px; + height: 64px; + } + .npc-image img { + width: 64px; + height: 64px; + border: none; + object-fit: contain; + } + + .npc-line { + line-height: 25px; + padding: 9px 0 5px 5px; + } + .npc-name { + font-weight: bold; + font-size: 16px; + } + .cr { + display: inline-block; + width: 55px; + } + .size { + display: inline-block; + width: 75px; + } + .type { + display: inline-block; + } + } + } + + .settings { + .settings-group { + border: 1px solid #bbb; + border-radius: 5px; + margin-top: 5px; + padding: 2px; + + h3 { + margin: 0; + cursor: pointer; + } + + label { + display: block; + } + input { + } + h4 { + display: inline-block; + vertical-align: middle; + height: 100%; + } + } + } +} diff --git a/item-packs.json b/item-packs.json index 6ef94ac..954ed9d 100644 --- a/item-packs.json +++ b/item-packs.json @@ -1,10 +1,103 @@ -{ - "burglar": ["Burglar's Pack", "Backpack","Ball Bearings", "String", "Bell", "Candle", "Crowbar", "Hammer", "Piton", "Hooded Lantern", "Oil Flask", "Rations", "Tinderbox","Waterskin","Hempen Rope (50 ft.)"], - "diplomat": ["Diplomat's Pack", "Chest", "Map or Scroll Case", "Fine Clothes", "Ink Bottle", "Ink Pen", "Lamp", "Oil Flask", "Paper", "Perfume", "Sealing Wax", "Soap"], - "dungeoneer": ["Dungeoneer's Pack", "Backpack", "Crowbar", "Hammer", "Piton", "Torch", "Tinderbox", "Rations", "Waterskin", "Hempen Rope (50 ft.)"], - "entertainer": ["Entertainer's Pack", "Backpack", "Bedroll", "Costume Clothes", "Candle", "Rations", "Waterskin", "Disguise Kit"], - "explorer": ["Explorer's Pack", "Backpack", "Bedroll", "Mess Kit", "Tinderbox", "Torch", "Rations", "Waterskin", "Hempen Rope (50 ft.)"], - "monsterhunter": ["Monster Hunter's Pack", "Chest", "Crowbar", "Hammer", "Wooden Stake", "Holy Symbol", "Flask of Holy Water", "Manacles", "Steel Mirror", "Oil Flask", "Tinderbox", "Torch"], - "priest": ["Priest's Pack", "Backpack", "Blanket", "Candle", "Tinderbox", "Alms Box", "Block of Incense", "Censor", "Vestments", "Rations", "Waterskin"], - "scholar": ["Scholar's Pack", "Backpack", "Book of Lore", "Ink Bottle", "Ink Pen", "Parchment", "Bag of Sand", "Small Knife"] -} \ No newline at end of file +{ + "burglar": [ + "Burglar's Pack", + "Backpack", + "Ball Bearings", + "String", + "Bell", + "Candle", + "Crowbar", + "Hammer", + "Piton", + "Hooded Lantern", + "Oil Flask", + "Rations", + "Tinderbox", + "Waterskin", + "Hempen Rope (50 ft.)" + ], + "diplomat": [ + "Diplomat's Pack", + "Chest", + "Map or Scroll Case", + "Fine Clothes", + "Ink Bottle", + "Ink Pen", + "Lamp", + "Oil Flask", + "Paper", + "Perfume", + "Sealing Wax", + "Soap" + ], + "dungeoneer": [ + "Dungeoneer's Pack", + "Backpack", + "Crowbar", + "Hammer", + "Piton", + "Torch", + "Tinderbox", + "Rations", + "Waterskin", + "Hempen Rope (50 ft.)" + ], + "entertainer": [ + "Entertainer's Pack", + "Backpack", + "Bedroll", + "Costume Clothes", + "Candle", + "Rations", + "Waterskin", + "Disguise Kit" + ], + "explorer": [ + "Explorer's Pack", + "Backpack", + "Bedroll", + "Mess Kit", + "Tinderbox", + "Torch", + "Rations", + "Waterskin", + "Hempen Rope (50 ft.)" + ], + "monsterhunter": [ + "Monster Hunter's Pack", + "Chest", + "Crowbar", + "Hammer", + "Wooden Stake", + "Holy Symbol", + "Flask of Holy Water", + "Manacles", + "Steel Mirror", + "Oil Flask", + "Tinderbox", + "Torch" + ], + "priest": [ + "Priest's Pack", + "Backpack", + "Blanket", + "Candle", + "Tinderbox", + "Alms Box", + "Block of Incense", + "Censor", + "Vestments", + "Rations", + "Waterskin" + ], + "scholar": [ + "Scholar's Pack", + "Backpack", + "Book of Lore", + "Ink Bottle", + "Ink Pen", + "Parchment", + "Bag of Sand", + "Small Knife" + ] +} diff --git a/module.json b/module.json index ef375e6..be706a1 100644 --- a/module.json +++ b/module.json @@ -1,72 +1,72 @@ -{ - "id": "compendium-browser", - "title": "Compendium Browser", - "description": "

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

Verified with Foundry 9 and 10 only

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 0.7.0: Supports Foundry 0.8+ only; faster searches using Foundry 0.8 queries!", - "version": "0.9.0", - "author": "Spetzel#0103", - "authors": [ - { - "name": "Spetzel#0103", - "url": "https://github.com/spetzel2020" - }, - { - "name": "ZoltanDM", - "url": "https://github.com/ZoltanTheDM" - }, - { - "name": "Felix#6196" - }, - { - "name": "DrVeoj#4863", - "url": "https://github.com/joevaughan" - } - ], - "systems": ["dnd5e"], - "scripts": ["./compendium-browser.js"], - "styles": ["./compendium-browser.css"], - "packs": [], - "languages": [ - { - "lang": "en", - "name": "English", - "path": "lang/en.json" - }, - { - "lang": "ja", - "name": "Japanese", - "path": "lang/ja.json" - }, - { - "lang": "fr", - "name": "French (FRANCE)", - "path": "lang/fr.json" - }, - { - "lang": "pt-BR", - "name": "Português (Brasil)", - "path": "lang/pt-BR.json" - }, - { - "lang": "es", - "name": "Español", - "path": "lang/es.json" - }, - { - "lang": "de", - "name": "Deutsch", - "path": "lang/de.json" - } - ], - "url": "https://github.com/League-of-Foundry-Developers/compendium-browser", - "manifest": "https://github.com/League-of-Foundry-Developers/compendium-browser/releases/latest/download/module.json", - "download": "https://github.com/League-of-Foundry-Developers/compendium-browser/releases/download/latest/compendium-browser.zip", - "compatibility": { - "minimum": "9", - "verified": "11.313", - "maximum": "11" - }, - "allowBugReporter": true, - "bugs": "https://github.com/ZoltanTheDM/compendium-browser/issues", - "readme": "https://github.com/ZoltanTheDM/compendium-browser/blob/master/README.md", - "changelog": "https://github.com/ZoltanTheDM/compendium-browser/blob/master/Patchnotes.md" -} +{ + "id": "compendium-browser", + "title": "Compendium Browser", + "description": "

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

Verified with Foundry 9 and 10 only

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 0.7.0: Supports Foundry 0.8+ only; faster searches using Foundry 0.8 queries!", + "version": "0.9.0", + "author": "Spetzel#0103", + "authors": [ + { + "name": "Spetzel#0103", + "url": "https://github.com/spetzel2020" + }, + { + "name": "ZoltanDM", + "url": "https://github.com/ZoltanTheDM" + }, + { + "name": "Felix#6196" + }, + { + "name": "DrVeoj#4863", + "url": "https://github.com/joevaughan" + } + ], + "systems": ["dnd5e"], + "scripts": ["./compendium-browser.js"], + "styles": ["./compendium-browser.css"], + "packs": [], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + }, + { + "lang": "ja", + "name": "Japanese", + "path": "lang/ja.json" + }, + { + "lang": "fr", + "name": "French (FRANCE)", + "path": "lang/fr.json" + }, + { + "lang": "pt-BR", + "name": "Português (Brasil)", + "path": "lang/pt-BR.json" + }, + { + "lang": "es", + "name": "Español", + "path": "lang/es.json" + }, + { + "lang": "de", + "name": "Deutsch", + "path": "lang/de.json" + } + ], + "url": "https://github.com/League-of-Foundry-Developers/compendium-browser", + "manifest": "https://github.com/League-of-Foundry-Developers/compendium-browser/releases/latest/download/module.json", + "download": "https://github.com/League-of-Foundry-Developers/compendium-browser/releases/download/latest/compendium-browser.zip", + "compatibility": { + "minimum": "9", + "verified": "11.313", + "maximum": "11" + }, + "allowBugReporter": true, + "bugs": "https://github.com/ZoltanTheDM/compendium-browser/issues", + "readme": "https://github.com/ZoltanTheDM/compendium-browser/blob/master/README.md", + "changelog": "https://github.com/ZoltanTheDM/compendium-browser/blob/master/Patchnotes.md" +} diff --git a/spell-classes.json b/spell-classes.json index 41193b7..0df7a2c 100644 --- a/spell-classes.json +++ b/spell-classes.json @@ -1,503 +1,502 @@ - { - "abidalzimshorridwilting": "sorcerer,wizard", - "absorbelements": "artificer,druid,ranger,sorcerer,wizard", - "arcaneweapon": "artificerrevisited", - "acidsplash": "artificer,sorcerer,wizard,artificerrevisited", - "aganazzarsscorcher": "sorcerer,wizard", - "aid": "artificer,bard,cleric,paladin,artificer,artificerrevisited", - "alarm": "artificer,ranger,wizard,artificer,artificerrevisited", - "alterself": "artificer,sorcerer,wizard,artificer,artificerrevisited", - "animalfriendship": "bard,druid,ranger", - "animalmessenger": "bard,druid,ranger", - "animalshapes": "druid", - "animatedead": "cleric,wizard", - "animateobjects": "artificer,bard,sorcerer,wizard,artificerrevisited", - "antilifeshell": "druid", - "antimagicfield": "cleric,wizard", - "antipathysympathy": "bard,druid,wizard", - "arcaneeye": "artificer,wizard,artificer,artificerrevisited", - "arcanegate": "sorcerer,warlock,wizard", - "arcanelock": "artificer,wizard,artificer,artificerrevisited", - "armorofagathys": "warlock", - "armsofhadar": "warlock", - "astralprojection": "cleric,warlock,wizard", - "augury": "cleric", - "auraoflife": "paladin", - "auraofpurity": "paladin", - "auraofvitality": "paladin", - "awaken": "bard,druid", - "bane": "bard,cleric", - "banishingsmite": "paladin", - "banishment": "cleric,paladin,sorcerer,warlock,wizard", - "barkskin": "druid,ranger", - "beaconofhope": "cleric", - "beastbond": "druid,ranger", - "beastsense": "druid,ranger", - "bestowcurse": "bard,cleric,wizard", - "bigbyshand": "artificer,wizard,artificerrevisited", - "arcanehand": "wizard,artificerrevisited", - "bladebarrier": "cleric", - "bladeward": "bard,sorcerer,warlock,wizard", - "bless": "cleric,paladin", - "bladeofdisaster": "sorcerer,warlock,wizard", - "blight": "druid,sorcerer,warlock,wizard", - "blindingsmite": "paladin", - "blindnessdeafness": "bard,cleric,sorcerer,wizard", - "blink": "artificer,sorcerer,wizard,artificer,artificerrevisited", - "blur": "artificer,sorcerer,wizard,artificer,artificerrevisited", - "bonesoftheearth": "druid", - "boomingblade": "artificer,sorcerer,warlock,wizard", - "brandingsmite": "paladin", - "burninghands": "sorcerer,wizard", - "calllightning": "druid", - "calmemotions": "bard,cleric", - "catapult": "artificer,sorcerer,wizard", - "catnap": "artificer,bard,sorcerer,wizard", - "causefear": "warlock,wizard", - "ceremony": "cleric,paladin", - "chainlightning": "sorcerer,wizard", - "chaosbolt": "sorcerer", - "charmmonster": "bard,druid,sorcerer,warlock,wizard", - "charmperson": "bard,druid,sorcerer,warlock,wizard", - "chilltouch": "sorcerer,warlock,wizard", - "chromaticorb": "sorcerer,wizard", - "circleofdeath": "sorcerer,warlock,wizard", - "circleofpower": "paladin", - "clairvoyance": "bard,cleric,sorcerer,wizard", - "clone": "wizard", - "cloudofdaggers": "bard,sorcerer,warlock,wizard", - "cloudkill": "sorcerer,wizard", - "colorspray": "bard,sorcerer,wizard", - "command": "bard,cleric,paladin", - "commune": "cleric", - "communewithnature": "druid,ranger", - "compelledduel": "paladin", - "comprehendlanguages": "bard,sorcerer,warlock,wizard", - "compulsion": "bard", - "coneofcold": "sorcerer,wizard", - "confusion": "bard,druid,sorcerer,wizard", - "conjureanimals": "druid,ranger", - "conjurebarrage": "ranger", - "conjurecelestial": "cleric", - "conjureelemental": "druid,wizard", - "conjurefey": "druid,warlock", - "conjureminorelementals": "druid,wizard", - "conjurevolley": "ranger", - "conjurewoodlandbeings": "druid,ranger", - "contactotherplane": "warlock,wizard", - "contagion": "cleric,druid", - "contingency": "wizard", - "continualflame": "artificer,cleric,wizard,artificer,artificerrevisited", - "controlflames": "druid,sorcerer,wizard", - "controlwater": "cleric,druid,wizard", - "controlweather": "cleric,druid,wizard", - "controlwinds": "druid,sorcerer,wizard", - "cordonofarrows": "ranger", - "counterspell": "sorcerer,warlock,wizard", - "createbonfire": "artificer,druid,sorcerer,warlock,wizard", - "createfoodandwater": "artificer,cleric,paladin", - "createhomunculus": "wizard", - "createmagen": "wizard", - "createundead": "cleric,warlock,wizard", - "createordestroywater": "cleric,druid", - "creation": "artificer,sorcerer,wizard,artificerrevisited", - "crownofmadness": "bard,sorcerer,warlock,wizard", - "crownofstars": "sorcerer,warlock,wizard", - "crusadersmantle": "paladin", - "curewounds": "artificer,bard,cleric,druid,paladin,ranger,artificer,artificerrevisited", - "dancinglights": "artificer,bard,sorcerer,wizard,artificerrevisited", - "dansemacabre": "warlock,wizard", - "darkness": "sorcerer,warlock,wizard", - "darkvision": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", - "dawn": "cleric,wizard", - "daylight": "cleric,druid,paladin,ranger,sorcerer", - "deathward": "cleric,paladin,artificer", - "delayedblastfireball": "sorcerer,wizard", - "demiplane": "warlock,wizard", - "destructivewave": "paladin", - "detectevilandgood": "cleric,paladin", - "detectmagic": "artificer,bard,cleric,druid,paladin,ranger,sorcerer,wizard,artificerrevisited", - "detectpoisonanddisease": "cleric,druid,paladin,ranger", - "detectthoughts": "bard,sorcerer,wizard", - "dimensiondoor": "bard,sorcerer,warlock,wizard", - "disguiseself": "artificer,bard,sorcerer,wizard,artificer,artificerrevisited", - "disintegrate": "sorcerer,wizard", - "dispelevilandgood": "cleric,paladin", - "dispelmagic": "artificer,bard,cleric,druid,paladin,sorcerer,warlock,wizard,artificerrevisited", - "dissonantwhispers": "bard", - "distortvalue": "bard,sorcerer,warlock,wizard", - "divination": "cleric", - "divinefavor": "paladin", - "divineword": "cleric", - "dominatebeast": "druid,sorcerer", - "dominatemonster": "bard,sorcerer,warlock,wizard", - "dominateperson": "bard,sorcerer,wizard", - "dragonsbreath": "sorcerer,wizard", - "drawmijsinstantsummons": "wizard", - "dreamoftheblueveil": "bard,sorcerer,warlock,wizard", - "instantsummons": "wizard", - "dream": "bard,warlock,wizard", - "druidgrove": "druid", - "druidcraft": "druid", - "dustdevil": "druid,sorcerer,wizard", - "earthtremor": "bard,druid,sorcerer,wizard", - "earthbind": "druid,sorcerer,warlock,wizard", - "earthquake": "cleric,druid,sorcerer", - "eldritchblast": "warlock", - "elementalbane": "artificer,druid,warlock,wizard", - "elementalweapon": "artificer,paladin,artificerrevisited", - "enemiesabound": "bard,sorcerer,warlock,wizard", - "enervation": "sorcerer,warlock,wizard", - "enhanceability": "artificer,bard,cleric,druid,sorcerer,artificer,artificerrevisited", - "enlargereduce": "artificer,bard,sorcerer,wizard,artificer,artificerrevisited", - "ensnaringstrike": "ranger", - "entangle": "druid", - "enthrall": "bard,warlock", - "eruptingearth": "druid,sorcerer,wizard", - "etherealness": "bard,cleric,sorcerer,warlock,wizard", - "evardsblacktentacles": "wizard", - "blacktentacles": "wizard", - "expeditiousretreat": "artificer,sorcerer,warlock,wizard,artificer,artificerrevisited", - "eyebite": "bard,sorcerer,warlock,wizard", - "fabricate": "artificer,wizard,artificer,artificerrevisited", - "faeriefire": "artificer,bard,druid", - "falselife": "artificer,sorcerer,wizard,artificer,artificerrevisited", - "farstep": "sorcerer,warlock,wizard", - "fastfriends": "bard,cleric,wizard", - "fear": "bard,sorcerer,warlock,wizard", - "featherfall": "artificer,bard,sorcerer,wizard", - "feeblemind": "bard,druid,warlock,wizard", - "feigndeath": "bard,cleric,druid,wizard", - "findfamiliar": "wizard", - "findgreatersteed": "paladin", - "findsteed": "paladin", - "findtraps": "cleric,druid,ranger", - "findthepath": "bard,cleric,druid", - "fingerofdeath": "sorcerer,warlock,wizard", - "firebolt": "artificer,sorcerer,wizard,artificerrevisited", - "fireshield": "wizard", - "firestorm": "cleric,druid,sorcerer", - "fireball": "sorcerer,wizard", - "flamearrows": "artificer,druid,ranger,sorcerer,wizard", - "flameblade": "druid", - "flamestrike": "cleric", - "flamingsphere": "druid,wizard", - "fleshtostone": "warlock,wizard", - "fly": "artificer,sorcerer,warlock,wizard,artificer,artificerrevisited", - "fogcloud": "druid,ranger,sorcerer,wizard", - "forbiddance": "cleric", - "forcecage": "bard,warlock,wizard", - "foresight": "bard,druid,warlock,wizard", - "freedomofmovement": "artificer,bard,cleric,druid,ranger,artificer,artificerrevisited", - "friends": "bard,sorcerer,warlock,wizard", - "frostbite": "artificer,druid,sorcerer,warlock,wizard", - "frostfingers": "wizard", - "gaseousform": "sorcerer,warlock,wizard,artificer,artificerrevisited", - "gate": "cleric,sorcerer,wizard", - "geas": "bard,cleric,druid,paladin,wizard", - "gentlerepose": "cleric,wizard", - "giantinsect": "druid", - "giftofgab": "bard,wizard", - "glibness": "bard,warlock", - "globeofinvulnerability": "sorcerer,wizard", - "glyphofwarding": "artificer,bard,cleric,wizard,artificer,artificerrevisited", - "goodberry": "druid,ranger", - "graspingvine": "druid,ranger", - "grease": "artificer,wizard,artificerrevisited", - "greaterinvisibility": "bard,sorcerer,wizard", - "greaterrestoration": "artificer,bard,cleric,druid,artificerrevisited", - "greenflameblade": "artificer,sorcerer,warlock,wizard", - "guardianoffaith": "cleric", - "guardianofnature": "druid,ranger", - "guardsandwards": "bard,wizard", - "guidance": "artificer,cleric,druid,artificerrevisited", - "guidingbolt": "cleric", - "gust": "druid,sorcerer,wizard", - "gustofwind": "druid,sorcerer,wizard", - "hailofthorns": "ranger", - "hallow": "cleric", - "hallucinatoryterrain": "bard,druid,warlock,wizard", - "harm": "cleric", - "haste": "artificer,sorcerer,wizard,artificer,artificerrevisited", - "heal": "cleric,druid", - "healingspirit": "druid,ranger", - "healingword": "bard,cleric,druid", - "heatmetal": "artificer,bard,druid,artificerrevisited", - "hellishrebuke": "warlock", - "heroesfeast": "bard,cleric,druid", - "heroism": "bard,paladin", - "hex": "warlock", - "holdmonster": "bard,sorcerer,warlock,wizard", - "holdperson": "bard,cleric,druid,sorcerer,warlock,wizard", - "holyaura": "cleric", - "holyweapon": "cleric,paladin", - "hungerofhadar": "warlock", - "huntersmark": "ranger", - "hypnoticpattern": "bard,sorcerer,warlock,wizard", - "iceknife": "druid,sorcerer,wizard", - "icestorm": "druid,sorcerer,wizard", - "identify": "artificer,bard,wizard,artificerrevisited", - "illusorydragon": "wizard", - "illusoryscript": "bard,warlock,wizard", - "immolation": "sorcerer,wizard", - "imprisonment": "warlock,wizard", - "incendiarycloud": "sorcerer,wizard", - "incitegreed": "cleric,warlock,wizard", - "infernalcalling": "warlock,wizard", - "infestation": "druid,sorcerer,warlock,wizard", - "inflictwounds": "cleric", - "insectplague": "cleric,druid,sorcerer", - "instantsummons": "wizard", - "intellectfortress": "bard", - "investitureofflame": "druid,sorcerer,warlock,wizard", - "investitureofice": "druid,sorcerer,warlock,wizard", - "investitureofstone": "druid,sorcerer,warlock,wizard", - "investitureofwind": "druid,sorcerer,warlock,wizard", - "invisibility": "artificer,bard,sorcerer,warlock,wizard,artificer,artificerrevisited", - "invulnerability": "wizard", - "intellectfortress": "artificer,bard,sorcerer,warlock,wizard", - "jimsglowingcoin": "wizard", - "jimsmagicmissile": "wizard", - "jump": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", - "knock": "bard,sorcerer,wizard", - "legendlore": "bard,cleric,wizard", - "leomundssecretchest": "artificer,wizard,artificer,artificerrevisited", - "leomundstinyhut": "bard,wizard", - "lesserrestoration": "artificer,bard,cleric,druid,paladin,ranger,artificer,artificerrevisited", - "levitate": "artificer,sorcerer,wizard,artificerrevisited", - "lifetransference": "cleric,wizard", - "light": "artificer,bard,cleric,sorcerer,wizard,artificerrevisited", - "lightningarrow": "ranger", - "lightningbolt": "sorcerer,wizard", - "lightninglure": "artificer,sorcerer,warlock,wizard", - "locateanimalsorplants": "bard,druid,ranger", - "locatecreature": "bard,cleric,druid,paladin,ranger,wizard", - "locateobject": "bard,cleric,druid,paladin,ranger,wizard", - "longstrider": "artificer,bard,druid,ranger,wizard,artificer,artificerrevisited", - "maddeningdarkness": "warlock,wizard", - "maelstrom": "druid", - "magearmor": "sorcerer,wizard", - "magehand": "artificer,bard,sorcerer,warlock,wizard,artificerrevisited", - "magiccircle": "cleric,paladin,warlock,wizard", - "magicjar": "wizard", - "magicmissile": "sorcerer,wizard", - "magicmouth": "artificer,bard,wizard,artificerrevisited", - "magicstone": "artificer,druid,warlock", - "magicweapon": "artificer,paladin,wizard,artificer,artificerrevisited", - "majorimage": "bard,sorcerer,warlock,wizard", - "masscurewounds": "bard,cleric,druid", - "massheal": "cleric", - "masshealingword": "bard,cleric", - "masspolymorph": "bard,sorcerer,wizard", - "masssuggestion": "bard,sorcerer,warlock,wizard", - "maximiliansearthengrasp": "sorcerer,wizard", - "maze": "wizard", - "meldintostone": "cleric,druid", - "melfsacidarrow": "wizard", - "melfsminutemeteors": "sorcerer,wizard", - "acidarrow": "wizard", - "mending": "artificer,bard,cleric,druid,sorcerer,wizard,artificerrevisited", - "mentalprison": "sorcerer,warlock,wizard", - "message": "artificer,bard,sorcerer,wizard,artificerrevisited", - "meteorswarm": "sorcerer,wizard", - "mightyfortress": "wizard", - "mindblank": "bard,wizard", - "mindsliver": "sorcerer,warlock,wizard", - "mindspike": "sorcerer,warlock,wizard", - "minorillusion": "bard,sorcerer,warlock,wizard", - "miragearcane": "bard,druid,wizard", - "mirrorimage": "bard,sorcerer,warlock,wizard", - "mislead": "bard,wizard", - "mistystep": "sorcerer,warlock,wizard", - "modifymemory": "bard,wizard", - "moldearth": "druid,sorcerer,wizard", - "moonbeam": "druid", - "mordenkainensfaithfulhound": "artificer,wizard,artificer,artificerrevisited", - "motivationalspeech": "bard,cleric", - "faithfulhound": "wizard,artificer,artificerrevisited", - "mordenkainensmagnificentmansion": "bard,wizard", - "magnificentmansion": "bard,wizard", - "mordenkainensprivatesanctum": "artificer,wizard,artificer,artificerrevisited", - "mordenkainenssword": "bard,wizard", - "arcanesword": "bard,wizard", - "moveearth": "druid,sorcerer,wizard", - "negativeenergyflood": "warlock,wizard", - "nondetection": "bard,ranger,wizard", - "nystulsmagicaura": "wizard", - "arcanistsmagicaura": "wizard", - "otilukesfreezingsphere": "wizard", - "otilukesresilientsphere": "artificer,wizard,artificer,artificerrevisited", - "ottosirresistibledance": "bard,wizard", - "passwithouttrace": "druid,ranger", - "passwall": "wizard", - "phantasmalforce": "bard,sorcerer,wizard", - "phantasmalkiller": "bard,wizard", - "phantomsteed": "wizard", - "planarally": "cleric", - "planarbinding": "bard,cleric,druid,wizard", - "planeshift": "cleric,druid,sorcerer,warlock,wizard", - "plantgrowth": "bard,druid,ranger", - "poisonspray": "artificer,druid,sorcerer,warlock,wizard,artificerrevisited", - "polymorph": "bard,druid,sorcerer,wizard", - "powerwordheal": "bard", - "powerwordkill": "bard,sorcerer,warlock,wizard", - "powerwordpain": "sorcerer,warlock,wizard", - "powerwordstun": "bard,sorcerer,warlock,wizard", - "prayerofhealing": "cleric", - "prestidigitation": "artificer,bard,sorcerer,warlock,wizard,artificerrevisited", - "primalsavagery": "druid", - "primordialward": "druid", - "prismaticspray": "bard,sorcerer,wizard", - "prismaticwall": "bard,wizard", - "produceflame": "druid", - "programmedillusion": "bard,wizard", - "projectimage": "bard,wizard", - "protectionfromenergy": "artificer,cleric,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", - "protectionfromevilandgood": "cleric,paladin,warlock,wizard", - "protectionfrompoison": "artificer,cleric,druid,paladin,ranger,artificer,artificerrevisited", - "psychicscream": "bard,sorcerer,warlock,wizard", - "purifyfoodanddrink": "artificer,cleric,druid,paladin", - "pyrotechnics": "artificer,bard,sorcerer,wizard", - "raisedead": "bard,cleric,paladin", - "rarystelepathicbond": "bard,wizard", - "rayofenfeeblement": "warlock,wizard", - "rayoffrost": "artificer,sorcerer,wizard,artificerrevisited", - "rayofsickness": "sorcerer,wizard", - "regenerate": "bard,cleric,druid", - "reincarnate": "druid", - "removecurse": "cleric,paladin,warlock,wizard", - "resistance": "artificer,cleric,druid,artificerrevisited", - "resurrection": "bard,cleric", - "reversegravity": "druid,sorcerer,wizard", - "revivify": "artificer,cleric,paladin,artificer,artificerrevisited", - "ropetrick": "artificer,wizard,artificer,artificerrevisited", - "sacredflame": "cleric", - "sanctuary": "artificer,cleric,artificer,artificerrevisited", - "scatter": "sorcerer,warlock,wizard", - "scorchingray": "sorcerer,wizard", - "scrying": "bard,cleric,druid,warlock,wizard", - "searingsmite": "paladin", - "seeinvisibility": "artificer,bard,sorcerer,wizard,artificerrevisited", - "seeming": "bard,sorcerer,wizard", - "sending": "bard,cleric,wizard", - "sequester": "wizard", - "shadowblade": "sorcerer,warlock,wizard", - "shadowofmoil": "warlock", - "shapewater": "druid,sorcerer,wizard", - "shapechange": "druid,wizard", - "shatter": "bard,sorcerer,warlock,wizard", - "shield": "sorcerer,wizard", - "shieldoffaith": "cleric,paladin,artificer,artificerrevisited", - "shillelagh": "druid", - "shockinggrasp": "artificer,sorcerer,wizard,artificerrevisited", - "sickeningradiance": "sorcerer,warlock,wizard", - "silence": "bard,cleric,ranger", - "silentimage": "bard,sorcerer,wizard", - "simulacrum": "wizard", - "skillempowerment": "artificer,bard,sorcerer,wizard", - "skywrite": "artificer,bard,druid,wizard", - "sleep": "bard,sorcerer,wizard", - "sleetstorm": "druid,sorcerer,wizard", - "slow": "bard,sorcerer,wizard", - "snare": "artificer,druid,ranger,wizard", - "snillocssnowballswarm": "sorcerer,wizard", - "soulcage": "warlock,wizard", - "sparethedying": "artificer,cleric,artificerrevisited", - "speakwithanimals": "bard,druid,ranger", - "speakwithdead": "bard,cleric", - "speakwithplants": "bard,druid,ranger", - "spiderclimb": "artificer,sorcerer,warlock,wizard,artificer,artificerrevisited", - "spikegrowth": "druid,ranger", - "spiritguardians": "cleric", - "spiritualweapon": "cleric", - "spiritshroud": "cleric,paladin,warlock,wizard", - "staggeringsmite": "paladin", - "steelwindstrike": "ranger,wizard", - "stinkingcloud": "bard,sorcerer,wizard", - "stoneshape": "artificer,cleric,druid,wizard,artificer,artificerrevisited", - "stoneskin": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", - "stormofvengeance": "druid", - "stormsphere": "sorcerer,wizard", - "suggestion": "bard,sorcerer,warlock,wizard", - "summonaberration": "warlock,wizard", - "summonbeast": "druid,ranger", - "summoncelestial": "cleric,paladin", - "summonconstruct": "artificer,wizard", - "summonelemental": "druid,ranger,wizard", - "summonfey": "druid,ranger,warlock,wizard", - "summonfiend": "warlock,wizard", - "summongreaterdemon": "warlock,wizard", - "summonlesserdemons": "warlock,wizard", - "summonshadowspawn": "warlock,wizard", - "summonundead": "warlock,wizard", - "sunbeam": "druid,sorcerer,wizard", - "sunburst": "druid,sorcerer,wizard", - "swiftquiver": "ranger", - "swordburst": "artificer,sorcerer,warlock,wizard", - "synapticstatic": "bard,sorcerer,warlock,wizard", - "symbol": "bard,cleric,wizard", - "tashascausticbrew": "artificer,sorcerer,wizard", - "tashashideouslaughter": "bard,wizard", - "hideouslaughter": "bard,wizard", - "tashasmindwhip": "sorcerer,wizard", - "tashasotherworldlyguise": "sorcerer,warlock,wizard", - "telekinesis": "sorcerer,wizard", - "telepathy": "wizard", - "teleport": "bard,sorcerer,wizard", - "teleportationcircle": "bard,sorcerer,wizard", - "templeofthegods": "cleric", - "tensersfloatingdisk": "wizard", - "tensersfloatingdisc": "wizard", - "tenserstransformation": "wizard", - "floatingdisc": "wizard", - "thaumaturgy": "cleric", - "thornwhip": "artificer,druid,artificerrevisited", - "thunderstep": "sorcerer,warlock,wizard", - "thunderclap": "artificer,bard,druid,sorcerer,warlock,wizard", - "thunderoussmite": "paladin", - "thunderwave": "bard,druid,sorcerer,wizard", - "tidalwave": "druid,sorcerer,wizard", - "timestop": "sorcerer,wizard", - "tinyservant": "artificer,wizard", - "tollthedead": "cleric,warlock,wizard", - "tongues": "bard,cleric,sorcerer,warlock,wizard", - "transmuterock": "artificer,druid,wizard", - "transportviaplants": "druid", - "treestride": "druid,ranger", - "truepolymorph": "bard,warlock,wizard", - "trueresurrection": "cleric,druid", - "trueseeing": "bard,cleric,sorcerer,warlock,wizard", - "truestrike": "bard,sorcerer,warlock,wizard", - "tsunami": "druid", - "unseenservant": "bard,warlock,wizard", - "vampirictouch": "warlock,wizard", - "viciousmockery": "bard", - "vitriolicsphere": "sorcerer,wizard", - "walloffire": "druid,sorcerer,wizard", - "wallofforce": "wizard", - "wallofice": "wizard", - "walloflight": "sorcerer,warlock,wizard", - "wallofsand": "wizard", - "wallofstone": "artificer,druid,sorcerer,wizard,artificerrevisited", - "wallofthorns": "druid", - "wallofwater": "druid,sorcerer,wizard", - "wardingbond": "cleric", - "wardingwind": "bard,druid,sorcerer,wizard", - "waterbreathing": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", - "waterwalk": "artificer,cleric,druid,ranger,sorcerer,artificer,artificerrevisited", - "waterysphere": "druid,sorcerer,wizard", - "web": "artificer,sorcerer,wizard", - "weird": "wizard", - "whirlwind": "druid,sorcerer,wizard", - "windwalk": "druid", - "windwall": "druid,ranger", - "wish": "sorcerer,wizard", - "witchbolt": "sorcerer,warlock,wizard", - "wordofradiance": "cleric", - "wordofrecall": "cleric", - "wrathofnature": "druid,ranger", - "wrathfulsmite": "paladin", - "zephyrstrike": "ranger", - "zoneoftruth": "bard,cleric,paladin" + "abidalzimshorridwilting": "sorcerer,wizard", + "absorbelements": "artificer,druid,ranger,sorcerer,wizard", + "arcaneweapon": "artificerrevisited", + "acidsplash": "artificer,sorcerer,wizard,artificerrevisited", + "aganazzarsscorcher": "sorcerer,wizard", + "aid": "artificer,bard,cleric,paladin,artificer,artificerrevisited", + "alarm": "artificer,ranger,wizard,artificer,artificerrevisited", + "alterself": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "animalfriendship": "bard,druid,ranger", + "animalmessenger": "bard,druid,ranger", + "animalshapes": "druid", + "animatedead": "cleric,wizard", + "animateobjects": "artificer,bard,sorcerer,wizard,artificerrevisited", + "antilifeshell": "druid", + "antimagicfield": "cleric,wizard", + "antipathysympathy": "bard,druid,wizard", + "arcaneeye": "artificer,wizard,artificer,artificerrevisited", + "arcanegate": "sorcerer,warlock,wizard", + "arcanelock": "artificer,wizard,artificer,artificerrevisited", + "armorofagathys": "warlock", + "armsofhadar": "warlock", + "astralprojection": "cleric,warlock,wizard", + "augury": "cleric", + "auraoflife": "paladin", + "auraofpurity": "paladin", + "auraofvitality": "paladin", + "awaken": "bard,druid", + "bane": "bard,cleric", + "banishingsmite": "paladin", + "banishment": "cleric,paladin,sorcerer,warlock,wizard", + "barkskin": "druid,ranger", + "beaconofhope": "cleric", + "beastbond": "druid,ranger", + "beastsense": "druid,ranger", + "bestowcurse": "bard,cleric,wizard", + "bigbyshand": "artificer,wizard,artificerrevisited", + "arcanehand": "wizard,artificerrevisited", + "bladebarrier": "cleric", + "bladeward": "bard,sorcerer,warlock,wizard", + "bless": "cleric,paladin", + "bladeofdisaster": "sorcerer,warlock,wizard", + "blight": "druid,sorcerer,warlock,wizard", + "blindingsmite": "paladin", + "blindnessdeafness": "bard,cleric,sorcerer,wizard", + "blink": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "blur": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "bonesoftheearth": "druid", + "boomingblade": "artificer,sorcerer,warlock,wizard", + "brandingsmite": "paladin", + "burninghands": "sorcerer,wizard", + "calllightning": "druid", + "calmemotions": "bard,cleric", + "catapult": "artificer,sorcerer,wizard", + "catnap": "artificer,bard,sorcerer,wizard", + "causefear": "warlock,wizard", + "ceremony": "cleric,paladin", + "chainlightning": "sorcerer,wizard", + "chaosbolt": "sorcerer", + "charmmonster": "bard,druid,sorcerer,warlock,wizard", + "charmperson": "bard,druid,sorcerer,warlock,wizard", + "chilltouch": "sorcerer,warlock,wizard", + "chromaticorb": "sorcerer,wizard", + "circleofdeath": "sorcerer,warlock,wizard", + "circleofpower": "paladin", + "clairvoyance": "bard,cleric,sorcerer,wizard", + "clone": "wizard", + "cloudofdaggers": "bard,sorcerer,warlock,wizard", + "cloudkill": "sorcerer,wizard", + "colorspray": "bard,sorcerer,wizard", + "command": "bard,cleric,paladin", + "commune": "cleric", + "communewithnature": "druid,ranger", + "compelledduel": "paladin", + "comprehendlanguages": "bard,sorcerer,warlock,wizard", + "compulsion": "bard", + "coneofcold": "sorcerer,wizard", + "confusion": "bard,druid,sorcerer,wizard", + "conjureanimals": "druid,ranger", + "conjurebarrage": "ranger", + "conjurecelestial": "cleric", + "conjureelemental": "druid,wizard", + "conjurefey": "druid,warlock", + "conjureminorelementals": "druid,wizard", + "conjurevolley": "ranger", + "conjurewoodlandbeings": "druid,ranger", + "contactotherplane": "warlock,wizard", + "contagion": "cleric,druid", + "contingency": "wizard", + "continualflame": "artificer,cleric,wizard,artificer,artificerrevisited", + "controlflames": "druid,sorcerer,wizard", + "controlwater": "cleric,druid,wizard", + "controlweather": "cleric,druid,wizard", + "controlwinds": "druid,sorcerer,wizard", + "cordonofarrows": "ranger", + "counterspell": "sorcerer,warlock,wizard", + "createbonfire": "artificer,druid,sorcerer,warlock,wizard", + "createfoodandwater": "artificer,cleric,paladin", + "createhomunculus": "wizard", + "createmagen": "wizard", + "createundead": "cleric,warlock,wizard", + "createordestroywater": "cleric,druid", + "creation": "artificer,sorcerer,wizard,artificerrevisited", + "crownofmadness": "bard,sorcerer,warlock,wizard", + "crownofstars": "sorcerer,warlock,wizard", + "crusadersmantle": "paladin", + "curewounds": "artificer,bard,cleric,druid,paladin,ranger,artificer,artificerrevisited", + "dancinglights": "artificer,bard,sorcerer,wizard,artificerrevisited", + "dansemacabre": "warlock,wizard", + "darkness": "sorcerer,warlock,wizard", + "darkvision": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", + "dawn": "cleric,wizard", + "daylight": "cleric,druid,paladin,ranger,sorcerer", + "deathward": "cleric,paladin,artificer", + "delayedblastfireball": "sorcerer,wizard", + "demiplane": "warlock,wizard", + "destructivewave": "paladin", + "detectevilandgood": "cleric,paladin", + "detectmagic": "artificer,bard,cleric,druid,paladin,ranger,sorcerer,wizard,artificerrevisited", + "detectpoisonanddisease": "cleric,druid,paladin,ranger", + "detectthoughts": "bard,sorcerer,wizard", + "dimensiondoor": "bard,sorcerer,warlock,wizard", + "disguiseself": "artificer,bard,sorcerer,wizard,artificer,artificerrevisited", + "disintegrate": "sorcerer,wizard", + "dispelevilandgood": "cleric,paladin", + "dispelmagic": "artificer,bard,cleric,druid,paladin,sorcerer,warlock,wizard,artificerrevisited", + "dissonantwhispers": "bard", + "distortvalue": "bard,sorcerer,warlock,wizard", + "divination": "cleric", + "divinefavor": "paladin", + "divineword": "cleric", + "dominatebeast": "druid,sorcerer", + "dominatemonster": "bard,sorcerer,warlock,wizard", + "dominateperson": "bard,sorcerer,wizard", + "dragonsbreath": "sorcerer,wizard", + "drawmijsinstantsummons": "wizard", + "dreamoftheblueveil": "bard,sorcerer,warlock,wizard", + "instantsummons": "wizard", + "dream": "bard,warlock,wizard", + "druidgrove": "druid", + "druidcraft": "druid", + "dustdevil": "druid,sorcerer,wizard", + "earthtremor": "bard,druid,sorcerer,wizard", + "earthbind": "druid,sorcerer,warlock,wizard", + "earthquake": "cleric,druid,sorcerer", + "eldritchblast": "warlock", + "elementalbane": "artificer,druid,warlock,wizard", + "elementalweapon": "artificer,paladin,artificerrevisited", + "enemiesabound": "bard,sorcerer,warlock,wizard", + "enervation": "sorcerer,warlock,wizard", + "enhanceability": "artificer,bard,cleric,druid,sorcerer,artificer,artificerrevisited", + "enlargereduce": "artificer,bard,sorcerer,wizard,artificer,artificerrevisited", + "ensnaringstrike": "ranger", + "entangle": "druid", + "enthrall": "bard,warlock", + "eruptingearth": "druid,sorcerer,wizard", + "etherealness": "bard,cleric,sorcerer,warlock,wizard", + "evardsblacktentacles": "wizard", + "blacktentacles": "wizard", + "expeditiousretreat": "artificer,sorcerer,warlock,wizard,artificer,artificerrevisited", + "eyebite": "bard,sorcerer,warlock,wizard", + "fabricate": "artificer,wizard,artificer,artificerrevisited", + "faeriefire": "artificer,bard,druid", + "falselife": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "farstep": "sorcerer,warlock,wizard", + "fastfriends": "bard,cleric,wizard", + "fear": "bard,sorcerer,warlock,wizard", + "featherfall": "artificer,bard,sorcerer,wizard", + "feeblemind": "bard,druid,warlock,wizard", + "feigndeath": "bard,cleric,druid,wizard", + "findfamiliar": "wizard", + "findgreatersteed": "paladin", + "findsteed": "paladin", + "findtraps": "cleric,druid,ranger", + "findthepath": "bard,cleric,druid", + "fingerofdeath": "sorcerer,warlock,wizard", + "firebolt": "artificer,sorcerer,wizard,artificerrevisited", + "fireshield": "wizard", + "firestorm": "cleric,druid,sorcerer", + "fireball": "sorcerer,wizard", + "flamearrows": "artificer,druid,ranger,sorcerer,wizard", + "flameblade": "druid", + "flamestrike": "cleric", + "flamingsphere": "druid,wizard", + "fleshtostone": "warlock,wizard", + "fly": "artificer,sorcerer,warlock,wizard,artificer,artificerrevisited", + "fogcloud": "druid,ranger,sorcerer,wizard", + "forbiddance": "cleric", + "forcecage": "bard,warlock,wizard", + "foresight": "bard,druid,warlock,wizard", + "freedomofmovement": "artificer,bard,cleric,druid,ranger,artificer,artificerrevisited", + "friends": "bard,sorcerer,warlock,wizard", + "frostbite": "artificer,druid,sorcerer,warlock,wizard", + "frostfingers": "wizard", + "gaseousform": "sorcerer,warlock,wizard,artificer,artificerrevisited", + "gate": "cleric,sorcerer,wizard", + "geas": "bard,cleric,druid,paladin,wizard", + "gentlerepose": "cleric,wizard", + "giantinsect": "druid", + "giftofgab": "bard,wizard", + "glibness": "bard,warlock", + "globeofinvulnerability": "sorcerer,wizard", + "glyphofwarding": "artificer,bard,cleric,wizard,artificer,artificerrevisited", + "goodberry": "druid,ranger", + "graspingvine": "druid,ranger", + "grease": "artificer,wizard,artificerrevisited", + "greaterinvisibility": "bard,sorcerer,wizard", + "greaterrestoration": "artificer,bard,cleric,druid,artificerrevisited", + "greenflameblade": "artificer,sorcerer,warlock,wizard", + "guardianoffaith": "cleric", + "guardianofnature": "druid,ranger", + "guardsandwards": "bard,wizard", + "guidance": "artificer,cleric,druid,artificerrevisited", + "guidingbolt": "cleric", + "gust": "druid,sorcerer,wizard", + "gustofwind": "druid,sorcerer,wizard", + "hailofthorns": "ranger", + "hallow": "cleric", + "hallucinatoryterrain": "bard,druid,warlock,wizard", + "harm": "cleric", + "haste": "artificer,sorcerer,wizard,artificer,artificerrevisited", + "heal": "cleric,druid", + "healingspirit": "druid,ranger", + "healingword": "bard,cleric,druid", + "heatmetal": "artificer,bard,druid,artificerrevisited", + "hellishrebuke": "warlock", + "heroesfeast": "bard,cleric,druid", + "heroism": "bard,paladin", + "hex": "warlock", + "holdmonster": "bard,sorcerer,warlock,wizard", + "holdperson": "bard,cleric,druid,sorcerer,warlock,wizard", + "holyaura": "cleric", + "holyweapon": "cleric,paladin", + "hungerofhadar": "warlock", + "huntersmark": "ranger", + "hypnoticpattern": "bard,sorcerer,warlock,wizard", + "iceknife": "druid,sorcerer,wizard", + "icestorm": "druid,sorcerer,wizard", + "identify": "artificer,bard,wizard,artificerrevisited", + "illusorydragon": "wizard", + "illusoryscript": "bard,warlock,wizard", + "immolation": "sorcerer,wizard", + "imprisonment": "warlock,wizard", + "incendiarycloud": "sorcerer,wizard", + "incitegreed": "cleric,warlock,wizard", + "infernalcalling": "warlock,wizard", + "infestation": "druid,sorcerer,warlock,wizard", + "inflictwounds": "cleric", + "insectplague": "cleric,druid,sorcerer", + "instantsummons": "wizard", + "intellectfortress": "bard", + "investitureofflame": "druid,sorcerer,warlock,wizard", + "investitureofice": "druid,sorcerer,warlock,wizard", + "investitureofstone": "druid,sorcerer,warlock,wizard", + "investitureofwind": "druid,sorcerer,warlock,wizard", + "invisibility": "artificer,bard,sorcerer,warlock,wizard,artificer,artificerrevisited", + "invulnerability": "wizard", + "intellectfortress": "artificer,bard,sorcerer,warlock,wizard", + "jimsglowingcoin": "wizard", + "jimsmagicmissile": "wizard", + "jump": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", + "knock": "bard,sorcerer,wizard", + "legendlore": "bard,cleric,wizard", + "leomundssecretchest": "artificer,wizard,artificer,artificerrevisited", + "leomundstinyhut": "bard,wizard", + "lesserrestoration": "artificer,bard,cleric,druid,paladin,ranger,artificer,artificerrevisited", + "levitate": "artificer,sorcerer,wizard,artificerrevisited", + "lifetransference": "cleric,wizard", + "light": "artificer,bard,cleric,sorcerer,wizard,artificerrevisited", + "lightningarrow": "ranger", + "lightningbolt": "sorcerer,wizard", + "lightninglure": "artificer,sorcerer,warlock,wizard", + "locateanimalsorplants": "bard,druid,ranger", + "locatecreature": "bard,cleric,druid,paladin,ranger,wizard", + "locateobject": "bard,cleric,druid,paladin,ranger,wizard", + "longstrider": "artificer,bard,druid,ranger,wizard,artificer,artificerrevisited", + "maddeningdarkness": "warlock,wizard", + "maelstrom": "druid", + "magearmor": "sorcerer,wizard", + "magehand": "artificer,bard,sorcerer,warlock,wizard,artificerrevisited", + "magiccircle": "cleric,paladin,warlock,wizard", + "magicjar": "wizard", + "magicmissile": "sorcerer,wizard", + "magicmouth": "artificer,bard,wizard,artificerrevisited", + "magicstone": "artificer,druid,warlock", + "magicweapon": "artificer,paladin,wizard,artificer,artificerrevisited", + "majorimage": "bard,sorcerer,warlock,wizard", + "masscurewounds": "bard,cleric,druid", + "massheal": "cleric", + "masshealingword": "bard,cleric", + "masspolymorph": "bard,sorcerer,wizard", + "masssuggestion": "bard,sorcerer,warlock,wizard", + "maximiliansearthengrasp": "sorcerer,wizard", + "maze": "wizard", + "meldintostone": "cleric,druid", + "melfsacidarrow": "wizard", + "melfsminutemeteors": "sorcerer,wizard", + "acidarrow": "wizard", + "mending": "artificer,bard,cleric,druid,sorcerer,wizard,artificerrevisited", + "mentalprison": "sorcerer,warlock,wizard", + "message": "artificer,bard,sorcerer,wizard,artificerrevisited", + "meteorswarm": "sorcerer,wizard", + "mightyfortress": "wizard", + "mindblank": "bard,wizard", + "mindsliver": "sorcerer,warlock,wizard", + "mindspike": "sorcerer,warlock,wizard", + "minorillusion": "bard,sorcerer,warlock,wizard", + "miragearcane": "bard,druid,wizard", + "mirrorimage": "bard,sorcerer,warlock,wizard", + "mislead": "bard,wizard", + "mistystep": "sorcerer,warlock,wizard", + "modifymemory": "bard,wizard", + "moldearth": "druid,sorcerer,wizard", + "moonbeam": "druid", + "mordenkainensfaithfulhound": "artificer,wizard,artificer,artificerrevisited", + "motivationalspeech": "bard,cleric", + "faithfulhound": "wizard,artificer,artificerrevisited", + "mordenkainensmagnificentmansion": "bard,wizard", + "magnificentmansion": "bard,wizard", + "mordenkainensprivatesanctum": "artificer,wizard,artificer,artificerrevisited", + "mordenkainenssword": "bard,wizard", + "arcanesword": "bard,wizard", + "moveearth": "druid,sorcerer,wizard", + "negativeenergyflood": "warlock,wizard", + "nondetection": "bard,ranger,wizard", + "nystulsmagicaura": "wizard", + "arcanistsmagicaura": "wizard", + "otilukesfreezingsphere": "wizard", + "otilukesresilientsphere": "artificer,wizard,artificer,artificerrevisited", + "ottosirresistibledance": "bard,wizard", + "passwithouttrace": "druid,ranger", + "passwall": "wizard", + "phantasmalforce": "bard,sorcerer,wizard", + "phantasmalkiller": "bard,wizard", + "phantomsteed": "wizard", + "planarally": "cleric", + "planarbinding": "bard,cleric,druid,wizard", + "planeshift": "cleric,druid,sorcerer,warlock,wizard", + "plantgrowth": "bard,druid,ranger", + "poisonspray": "artificer,druid,sorcerer,warlock,wizard,artificerrevisited", + "polymorph": "bard,druid,sorcerer,wizard", + "powerwordheal": "bard", + "powerwordkill": "bard,sorcerer,warlock,wizard", + "powerwordpain": "sorcerer,warlock,wizard", + "powerwordstun": "bard,sorcerer,warlock,wizard", + "prayerofhealing": "cleric", + "prestidigitation": "artificer,bard,sorcerer,warlock,wizard,artificerrevisited", + "primalsavagery": "druid", + "primordialward": "druid", + "prismaticspray": "bard,sorcerer,wizard", + "prismaticwall": "bard,wizard", + "produceflame": "druid", + "programmedillusion": "bard,wizard", + "projectimage": "bard,wizard", + "protectionfromenergy": "artificer,cleric,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", + "protectionfromevilandgood": "cleric,paladin,warlock,wizard", + "protectionfrompoison": "artificer,cleric,druid,paladin,ranger,artificer,artificerrevisited", + "psychicscream": "bard,sorcerer,warlock,wizard", + "purifyfoodanddrink": "artificer,cleric,druid,paladin", + "pyrotechnics": "artificer,bard,sorcerer,wizard", + "raisedead": "bard,cleric,paladin", + "rarystelepathicbond": "bard,wizard", + "rayofenfeeblement": "warlock,wizard", + "rayoffrost": "artificer,sorcerer,wizard,artificerrevisited", + "rayofsickness": "sorcerer,wizard", + "regenerate": "bard,cleric,druid", + "reincarnate": "druid", + "removecurse": "cleric,paladin,warlock,wizard", + "resistance": "artificer,cleric,druid,artificerrevisited", + "resurrection": "bard,cleric", + "reversegravity": "druid,sorcerer,wizard", + "revivify": "artificer,cleric,paladin,artificer,artificerrevisited", + "ropetrick": "artificer,wizard,artificer,artificerrevisited", + "sacredflame": "cleric", + "sanctuary": "artificer,cleric,artificer,artificerrevisited", + "scatter": "sorcerer,warlock,wizard", + "scorchingray": "sorcerer,wizard", + "scrying": "bard,cleric,druid,warlock,wizard", + "searingsmite": "paladin", + "seeinvisibility": "artificer,bard,sorcerer,wizard,artificerrevisited", + "seeming": "bard,sorcerer,wizard", + "sending": "bard,cleric,wizard", + "sequester": "wizard", + "shadowblade": "sorcerer,warlock,wizard", + "shadowofmoil": "warlock", + "shapewater": "druid,sorcerer,wizard", + "shapechange": "druid,wizard", + "shatter": "bard,sorcerer,warlock,wizard", + "shield": "sorcerer,wizard", + "shieldoffaith": "cleric,paladin,artificer,artificerrevisited", + "shillelagh": "druid", + "shockinggrasp": "artificer,sorcerer,wizard,artificerrevisited", + "sickeningradiance": "sorcerer,warlock,wizard", + "silence": "bard,cleric,ranger", + "silentimage": "bard,sorcerer,wizard", + "simulacrum": "wizard", + "skillempowerment": "artificer,bard,sorcerer,wizard", + "skywrite": "artificer,bard,druid,wizard", + "sleep": "bard,sorcerer,wizard", + "sleetstorm": "druid,sorcerer,wizard", + "slow": "bard,sorcerer,wizard", + "snare": "artificer,druid,ranger,wizard", + "snillocssnowballswarm": "sorcerer,wizard", + "soulcage": "warlock,wizard", + "sparethedying": "artificer,cleric,artificerrevisited", + "speakwithanimals": "bard,druid,ranger", + "speakwithdead": "bard,cleric", + "speakwithplants": "bard,druid,ranger", + "spiderclimb": "artificer,sorcerer,warlock,wizard,artificer,artificerrevisited", + "spikegrowth": "druid,ranger", + "spiritguardians": "cleric", + "spiritualweapon": "cleric", + "spiritshroud": "cleric,paladin,warlock,wizard", + "staggeringsmite": "paladin", + "steelwindstrike": "ranger,wizard", + "stinkingcloud": "bard,sorcerer,wizard", + "stoneshape": "artificer,cleric,druid,wizard,artificer,artificerrevisited", + "stoneskin": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", + "stormofvengeance": "druid", + "stormsphere": "sorcerer,wizard", + "suggestion": "bard,sorcerer,warlock,wizard", + "summonaberration": "warlock,wizard", + "summonbeast": "druid,ranger", + "summoncelestial": "cleric,paladin", + "summonconstruct": "artificer,wizard", + "summonelemental": "druid,ranger,wizard", + "summonfey": "druid,ranger,warlock,wizard", + "summonfiend": "warlock,wizard", + "summongreaterdemon": "warlock,wizard", + "summonlesserdemons": "warlock,wizard", + "summonshadowspawn": "warlock,wizard", + "summonundead": "warlock,wizard", + "sunbeam": "druid,sorcerer,wizard", + "sunburst": "druid,sorcerer,wizard", + "swiftquiver": "ranger", + "swordburst": "artificer,sorcerer,warlock,wizard", + "synapticstatic": "bard,sorcerer,warlock,wizard", + "symbol": "bard,cleric,wizard", + "tashascausticbrew": "artificer,sorcerer,wizard", + "tashashideouslaughter": "bard,wizard", + "hideouslaughter": "bard,wizard", + "tashasmindwhip": "sorcerer,wizard", + "tashasotherworldlyguise": "sorcerer,warlock,wizard", + "telekinesis": "sorcerer,wizard", + "telepathy": "wizard", + "teleport": "bard,sorcerer,wizard", + "teleportationcircle": "bard,sorcerer,wizard", + "templeofthegods": "cleric", + "tensersfloatingdisk": "wizard", + "tensersfloatingdisc": "wizard", + "tenserstransformation": "wizard", + "floatingdisc": "wizard", + "thaumaturgy": "cleric", + "thornwhip": "artificer,druid,artificerrevisited", + "thunderstep": "sorcerer,warlock,wizard", + "thunderclap": "artificer,bard,druid,sorcerer,warlock,wizard", + "thunderoussmite": "paladin", + "thunderwave": "bard,druid,sorcerer,wizard", + "tidalwave": "druid,sorcerer,wizard", + "timestop": "sorcerer,wizard", + "tinyservant": "artificer,wizard", + "tollthedead": "cleric,warlock,wizard", + "tongues": "bard,cleric,sorcerer,warlock,wizard", + "transmuterock": "artificer,druid,wizard", + "transportviaplants": "druid", + "treestride": "druid,ranger", + "truepolymorph": "bard,warlock,wizard", + "trueresurrection": "cleric,druid", + "trueseeing": "bard,cleric,sorcerer,warlock,wizard", + "truestrike": "bard,sorcerer,warlock,wizard", + "tsunami": "druid", + "unseenservant": "bard,warlock,wizard", + "vampirictouch": "warlock,wizard", + "viciousmockery": "bard", + "vitriolicsphere": "sorcerer,wizard", + "walloffire": "druid,sorcerer,wizard", + "wallofforce": "wizard", + "wallofice": "wizard", + "walloflight": "sorcerer,warlock,wizard", + "wallofsand": "wizard", + "wallofstone": "artificer,druid,sorcerer,wizard,artificerrevisited", + "wallofthorns": "druid", + "wallofwater": "druid,sorcerer,wizard", + "wardingbond": "cleric", + "wardingwind": "bard,druid,sorcerer,wizard", + "waterbreathing": "artificer,druid,ranger,sorcerer,wizard,artificer,artificerrevisited", + "waterwalk": "artificer,cleric,druid,ranger,sorcerer,artificer,artificerrevisited", + "waterysphere": "druid,sorcerer,wizard", + "web": "artificer,sorcerer,wizard", + "weird": "wizard", + "whirlwind": "druid,sorcerer,wizard", + "windwalk": "druid", + "windwall": "druid,ranger", + "wish": "sorcerer,wizard", + "witchbolt": "sorcerer,warlock,wizard", + "wordofradiance": "cleric", + "wordofrecall": "cleric", + "wrathofnature": "druid,ranger", + "wrathfulsmite": "paladin", + "zephyrstrike": "ranger", + "zoneoftruth": "bard,cleric,paladin" } diff --git a/sub-classes.json b/sub-classes.json index 072d52c..0dfe120 100644 --- a/sub-classes.json +++ b/sub-classes.json @@ -1,15 +1,94 @@ -{ - "artificer":["Alchemist","Gunsmith"], - "barbarian":["Ancesstrial Guardian", "Battlerager", "Berserker", "Storm Herald", "Totem Warriro", "Zealot"], - "bard":["Eloquence", "Glamour", "Lore", "Swords", "Valor", "Whispers"], - "cleric":["Arcana", "Death", "Forge", "Grave", "Knowledge", "Life", "Light", "Nature", "Order", "Tempest", "Trickery", "War Domain"], - "druid":["Dreams", "Land", "Moon", "Sheapherd", "Spores"], - "fighter":["Arcane Archer", "Battle Master", "Cavalier", "Champion", "Echo Knight", "Eldritch Knight", "Samurai"], - "monk":["Drunken Master", "Four Elements", "Kensei", "Long Death", "Open Hand", "Shadow", "Sun Soul"], - "paladin":["Ancients", "Conquest", "Crown", "Devotion", "Glory", "Oathbreaker", "Redemption", "Vengeance"], - "ranger":["Beast Master", "Gloom Stalker", "Horizon Walker", "Hunter", "Monster Slayer"], - "rogue":["Arcane Trickster", "Assassin", "Inquisitive", "Mastermind", "Scout", "Swashbuckler", "Thief"], - "sorcerer":["Devine Soul", "Draconic", "Shadow", "Storm", "Wild"], - "warlock":["Archfey", "Celestial", "Fiend","Great Old One", "Hexblade", "Undying"], - "wizard":["Abjuration", "Bladesinging", "Chronurgy", "Conjuration", "Divination", "Énchantment", "Evocation", "Graviturgy", "Illusion", "Necromancy", "Transmutation", "War Magic"] -} \ No newline at end of file +{ + "artificer": ["Alchemist", "Gunsmith"], + "barbarian": [ + "Ancesstrial Guardian", + "Battlerager", + "Berserker", + "Storm Herald", + "Totem Warriro", + "Zealot" + ], + "bard": ["Eloquence", "Glamour", "Lore", "Swords", "Valor", "Whispers"], + "cleric": [ + "Arcana", + "Death", + "Forge", + "Grave", + "Knowledge", + "Life", + "Light", + "Nature", + "Order", + "Tempest", + "Trickery", + "War Domain" + ], + "druid": ["Dreams", "Land", "Moon", "Sheapherd", "Spores"], + "fighter": [ + "Arcane Archer", + "Battle Master", + "Cavalier", + "Champion", + "Echo Knight", + "Eldritch Knight", + "Samurai" + ], + "monk": [ + "Drunken Master", + "Four Elements", + "Kensei", + "Long Death", + "Open Hand", + "Shadow", + "Sun Soul" + ], + "paladin": [ + "Ancients", + "Conquest", + "Crown", + "Devotion", + "Glory", + "Oathbreaker", + "Redemption", + "Vengeance" + ], + "ranger": [ + "Beast Master", + "Gloom Stalker", + "Horizon Walker", + "Hunter", + "Monster Slayer" + ], + "rogue": [ + "Arcane Trickster", + "Assassin", + "Inquisitive", + "Mastermind", + "Scout", + "Swashbuckler", + "Thief" + ], + "sorcerer": ["Devine Soul", "Draconic", "Shadow", "Storm", "Wild"], + "warlock": [ + "Archfey", + "Celestial", + "Fiend", + "Great Old One", + "Hexblade", + "Undying" + ], + "wizard": [ + "Abjuration", + "Bladesinging", + "Chronurgy", + "Conjuration", + "Divination", + "Énchantment", + "Evocation", + "Graviturgy", + "Illusion", + "Necromancy", + "Transmutation", + "War Magic" + ] +}