├── .gitattributes ├── LICENSE ├── .gitignore ├── styles ├── overview.css ├── keys.css ├── optionals.css ├── shared.css ├── sheet.css └── workshop.css ├── rollup.config.mjs ├── example-foundry-config.yaml ├── templates ├── sheet-navigation.hbs ├── sheet-configuration.hbs ├── sheet-header.hbs ├── sheet-description.hbs ├── sheet-filters.hbs ├── subapplications │ ├── applied-bonuses-dialog.hbs │ ├── keys-dialog.hbs │ ├── optional-selector.hbs │ └── character-sheet-tab.hbs ├── babonus-workshop.hbs ├── sheet-advanced.hbs └── sheet-bonuses.hbs ├── scripts ├── applications │ ├── _module.mjs │ ├── enrichers.mjs │ ├── applied-bonuses-dialog.mjs │ ├── keys-dialog.mjs │ ├── bonus-collection.mjs │ ├── injections.mjs │ ├── character-sheet-tab.mjs │ ├── babonus-workshop.mjs │ ├── token-aura.mjs │ └── bonus-collector.mjs ├── models │ ├── _module.mjs │ ├── aura-model.mjs │ ├── consumption-model.mjs │ └── modifiers-model.mjs ├── registry.mjs ├── fields │ ├── _module.mjs │ ├── custom-scripts-field.mjs │ ├── identifiers-field.mjs │ ├── source-classes-field.mjs │ ├── markers-field.mjs │ ├── feature-types-field.mjs │ ├── health-percentages-field.mjs │ ├── token-sizes-field.mjs │ ├── remaining-spell-slots-field.mjs │ ├── attack-modes-field.mjs │ ├── arbitrary-comparison-field.mjs │ ├── filter-mixin.mjs │ ├── checkbox-fields.mjs │ └── semicolon-fields.mjs ├── constants.mjs ├── hooks.mjs └── api.mjs ├── jsconfig.json ├── package.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── main.yml ├── tools └── create-symlinks.mjs ├── .eslintrc.json ├── module.json └── .vscode └── settings.json /.gitattributes: -------------------------------------------------------------------------------- 1 | *.mjs text eol=lf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 zhell9201 2 | 3 | All rights reserved. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | babonus.lock 2 | node_modules/ 3 | foundry-config.yaml 4 | foundry 5 | -------------------------------------------------------------------------------- /styles/overview.css: -------------------------------------------------------------------------------- 1 | .babonus.overview table :is(td, th) { 2 | text-align: center; 3 | } 4 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | input: "scripts/hooks.mjs", 3 | output: { 4 | file: "module.mjs", 5 | format: "esm" 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /example-foundry-config.yaml: -------------------------------------------------------------------------------- 1 | # Copy this file and rename it to "foundry-config.yaml" 2 | installPath: "C:\\Program Files\\Foundry Virtual Tabletop" # Root of your Foundry install, used for creating symlinks for type purposes 3 | -------------------------------------------------------------------------------- /templates/sheet-navigation.hbs: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /templates/sheet-configuration.hbs: -------------------------------------------------------------------------------- 1 |
{{localize "BABONUS.CurrentFiltersTooltip"}}
4 | {{/unless}} 5 | {{#each filters}} {{{this}}} {{/each}} 6 || {{localize "BABONUS.OverviewName"}} | 6 |{{localize "BABONUS.OverviewImmediateParent"}} | 7 |{{localize "BABONUS.OverviewActor"}} | 8 |
|---|---|---|
| {{name}} | 14 |15 | 16 | {{ifThen parent.name parent.name (localize "Template")}} 17 | 18 | | 19 |20 | 21 | {{actor.name}} 22 | 23 | | 24 |
{{{localize description}}}
2 |${game.i18n.localize("BABONUS.RegionConfigHint")}
`; 154 | div.querySelector("[data-action]").addEventListener("click", (event) => babonus.openBabonusWorkshop(config.document)); 155 | fg.after(div); 156 | } 157 | 158 | /* -------------------------------------------------- */ 159 | 160 | export default { 161 | HeaderButton, 162 | HeaderButtonDialog, 163 | injectRegionConfigElement 164 | }; 165 | -------------------------------------------------------------------------------- /scripts/fields/checkbox-fields.mjs: -------------------------------------------------------------------------------- 1 | import {MODULE} from "../constants.mjs"; 2 | import FilterMixin from "./filter-mixin.mjs"; 3 | 4 | const {SetField, NumberField, StringField, SchemaField} = foundry.data.fields; 5 | 6 | class BaseField extends FilterMixin(SetField) { 7 | /** @override */ 8 | static render(bonus) { 9 | const field = bonus.schema.getField(`filters.${this.name}`); 10 | const value = bonus.filters[this.name] ?? new Set(); 11 | const template = ` 12 | `; 22 | return Handlebars.compile(template)({ 23 | field: field, value: value, hint: field.hint, label: field.label 24 | }); 25 | } 26 | 27 | /* -------------------------------------------------- */ 28 | 29 | /** @override */ 30 | _cleanType(value, source) { 31 | const choices = (this.element.choices instanceof Function) ? this.element.choices() : this.element.choices; 32 | value = super._cleanType(value, source).filter(v => v in choices); 33 | return value; 34 | } 35 | } 36 | 37 | /* -------------------------------------------------- */ 38 | 39 | class ProficiencyLevelsField extends BaseField { 40 | /** @override */ 41 | static name = "proficiencyLevels"; 42 | 43 | /* -------------------------------------------------- */ 44 | 45 | constructor() { 46 | super(new NumberField({choices: CONFIG.DND5E.proficiencyLevels})); 47 | } 48 | } 49 | 50 | /* -------------------------------------------------- */ 51 | 52 | class ItemTypesField extends BaseField { 53 | /** @override */ 54 | static name = "itemTypes"; 55 | 56 | /* -------------------------------------------------- */ 57 | 58 | constructor() { 59 | super(new StringField({ 60 | choices: Object.keys(dnd5e.dataModels.item.config).reduce((acc, type) => { 61 | if (!dnd5e.dataModels.item.config[type].schema.getField("activities")) return acc; 62 | acc[type] = game.i18n.localize(`TYPES.Item.${type}`); 63 | return acc; 64 | }, {}) 65 | })); 66 | } 67 | } 68 | 69 | /* -------------------------------------------------- */ 70 | 71 | class SpellLevelsField extends BaseField { 72 | /** @override */ 73 | static name = "spellLevels"; 74 | 75 | /* -------------------------------------------------- */ 76 | 77 | constructor() { 78 | super(new NumberField({choices: CONFIG.DND5E.spellLevels})); 79 | } 80 | } 81 | 82 | /* -------------------------------------------------- */ 83 | 84 | class SpellComponentsField extends FilterMixin(SchemaField) { 85 | /** @override */ 86 | static name = "spellComponents"; 87 | 88 | /* -------------------------------------------------- */ 89 | 90 | constructor(fields = {}, options = {}) { 91 | super({ 92 | types: new BaseField(new StringField({ 93 | choices: () => CONFIG.DND5E.validProperties.spell.reduce((acc, p) => { 94 | const prop = CONFIG.DND5E.itemProperties[p]; 95 | if (prop) acc[p] = prop; 96 | return acc; 97 | }, {}) 98 | })), 99 | match: new StringField({ 100 | required: true, 101 | initial: "ANY", 102 | choices: MODULE.SPELL_COMPONENT_CHOICES 103 | }), 104 | ...fields 105 | }, options); 106 | } 107 | 108 | /* -------------------------------------------------- */ 109 | 110 | /** @override */ 111 | static render(bonus) { 112 | const template = ` 113 | `; 134 | 135 | return Handlebars.compile(template)({ 136 | types: bonus.schema.getField("filters.spellComponents.types"), 137 | match: bonus.schema.getField("filters.spellComponents.match"), 138 | typesValue: bonus.filters.spellComponents.types, 139 | matchValue: bonus.filters.spellComponents.match 140 | }); 141 | } 142 | 143 | /* -------------------------------------------------- */ 144 | 145 | /** @override */ 146 | static storage(bonus) { 147 | return !!this.value(bonus).types?.filter(u => u).size; 148 | } 149 | } 150 | 151 | /* -------------------------------------------------- */ 152 | 153 | class ActorCreatureSizesField extends BaseField { 154 | /** @override */ 155 | static name = "actorCreatureSizes"; 156 | 157 | /* -------------------------------------------------- */ 158 | 159 | constructor() { 160 | super(new StringField({choices: CONFIG.DND5E.actorSizes})); 161 | } 162 | } 163 | 164 | /* -------------------------------------------------- */ 165 | 166 | class PreparationModesField extends BaseField { 167 | /** @override */ 168 | static name = "preparationModes"; 169 | 170 | /* -------------------------------------------------- */ 171 | 172 | constructor() { 173 | super(new StringField({choices: CONFIG.DND5E.spellPreparationModes})); 174 | } 175 | } 176 | 177 | /* -------------------------------------------------- */ 178 | 179 | export default { 180 | ActorCreatureSizesField, 181 | ItemTypesField, 182 | PreparationModesField, 183 | ProficiencyLevelsField, 184 | SpellComponentsField, 185 | SpellLevelsField 186 | }; 187 | -------------------------------------------------------------------------------- /scripts/models/consumption-model.mjs: -------------------------------------------------------------------------------- 1 | import {MODULE} from "../constants.mjs"; 2 | 3 | const {BooleanField, StringField, SchemaField, NumberField} = foundry.data.fields; 4 | 5 | export default class ConsumptionModel extends foundry.abstract.DataModel { 6 | /** @override */ 7 | static defineSchema() { 8 | return { 9 | enabled: new BooleanField(), 10 | type: new StringField({ 11 | required: true, 12 | initial: "", 13 | blank: true, 14 | choices: MODULE.CONSUMPTION_TYPES 15 | }), 16 | subtype: new StringField({ 17 | required: true, 18 | blank: true, 19 | initial: "" 20 | }), 21 | scales: new BooleanField(), 22 | formula: new StringField({required: true}), 23 | value: new SchemaField({ 24 | min: new StringField({required: true}), 25 | max: new StringField({required: true}), 26 | step: new NumberField({integer: true, min: 1, step: 1}) 27 | }) 28 | }; 29 | } 30 | 31 | /* -------------------------------------------------- */ 32 | /* Data preparation */ 33 | /* -------------------------------------------------- */ 34 | 35 | /** @override */ 36 | _initialize(...args) { 37 | super._initialize(...args); 38 | this.prepareDerivedData(); 39 | } 40 | 41 | /* -------------------------------------------------- */ 42 | 43 | /** @override */ 44 | prepareDerivedData() { 45 | const rollData = this.getRollData(); 46 | this.value.min = this.value.min ? dnd5e.utils.simplifyBonus(this.value.min, rollData) : 1; 47 | this.value.max = this.value.max ? dnd5e.utils.simplifyBonus(this.value.max, rollData) : null; 48 | if ((this.value.min > this.value.max) && (this.value.max !== null)) { 49 | const m = this.value.min; 50 | this.value.min = this.value.max; 51 | this.value.max = m; 52 | } 53 | } 54 | 55 | /* -------------------------------------------------- */ 56 | /* Migrations */ 57 | /* -------------------------------------------------- */ 58 | 59 | /** @override */ 60 | static migrateData(source) { 61 | // Resource as a consumption type is deprecated fully and without replacement. 62 | if (source.type === "resource") source.type = ""; 63 | } 64 | 65 | /* -------------------------------------------------- */ 66 | /* Properties */ 67 | /* -------------------------------------------------- */ 68 | 69 | /** 70 | * The babonus this lives on. 71 | * @type {Babonus} 72 | */ 73 | get bonus() { 74 | return this.parent; 75 | } 76 | 77 | /* -------------------------------------------------- */ 78 | 79 | /** 80 | * Whether the set up in consumption can be used to create something that consumes. 81 | * This looks only at the consumption data and not at anything else about the babonus. 82 | * @type {boolean} 83 | */ 84 | get isValidConsumption() { 85 | const {type, value} = this; 86 | if (!(type in MODULE.CONSUMPTION_TYPES) || ["save", "hitdie"].includes(this.parent.type)) return false; 87 | const invalidScale = this.scales && ((this.value.max ?? Infinity) < this.value.min); 88 | 89 | switch (type) { 90 | case "uses": 91 | if (!(this.bonus.parent instanceof Item) || invalidScale) return false; 92 | return this.bonus.parent.hasLimitedUses && (value.min > 0); 93 | case "quantity": 94 | if (!(this.bonus.parent instanceof Item) || invalidScale) return false; 95 | return this.bonus.parent.system.schema.has("quantity") && (value.min > 0); 96 | case "effect": 97 | return this.bonus.parent instanceof ActiveEffect; 98 | case "health": 99 | case "slots": 100 | if (invalidScale) return false; 101 | return value.min > 0; 102 | case "currency": 103 | if (invalidScale) return false; 104 | return Object.keys(CONFIG.DND5E.currencies).includes(this.subtype) && (value.min > 0); 105 | case "inspiration": 106 | return true; 107 | case "hitdice": 108 | if (invalidScale) return false; 109 | return ["smallest", "largest"].concat(CONFIG.DND5E.hitDieTypes).includes(this.subtype) && (value.min > 0); 110 | default: 111 | return false; 112 | } 113 | } 114 | 115 | /* -------------------------------------------------- */ 116 | /* Instance methods */ 117 | /* -------------------------------------------------- */ 118 | 119 | /** 120 | * Is the actor or user able to make the change when performing the consumption? 121 | * This checks for permission issues only as well as properties being existing on the actor. 122 | * It does not check for correct setup of the consumption data on the bonus. 123 | * This can be used to determine whether a bonus should appear in the Optional Selector, but 124 | * NOT due to lack of resources. 125 | * @param {Actor5e} actor The actor performing the roll. 126 | * @returns {boolean} 127 | */ 128 | canActorConsume(actor) { 129 | if (!this.isValidConsumption) return false; 130 | 131 | switch (this.type) { 132 | case "uses": 133 | case "quantity": 134 | case "effect": 135 | return this.bonus.parent.isOwner; 136 | case "slots": 137 | return !!actor.system.spells && actor.isOwner; 138 | case "health": 139 | return !!actor.system.attributes?.hp && actor.isOwner; 140 | case "currency": 141 | return !!actor.system.currency && actor.isOwner; 142 | case "inspiration": 143 | case "hitdice": 144 | return (actor.type === "character") && actor.isOwner; 145 | default: 146 | return false; 147 | } 148 | } 149 | 150 | /* -------------------------------------------------- */ 151 | 152 | /** 153 | * Whether there are enough remaining of the target to be consumed. 154 | * @param {Actor5e|Item5e|ActiveEffect5e} document The target of consumption. 155 | * @param {number} [min] A different minimum value to test against. 156 | * @returns {boolean} 157 | */ 158 | canBeConsumed(document, min) { 159 | if (!this.isValidConsumption) return false; 160 | 161 | min ??= this.value.min; 162 | const {hd, hp} = document.system?.attributes ?? {}; 163 | 164 | switch (this.type) { 165 | case "uses": 166 | return document.system.uses.value >= min; 167 | case "quantity": 168 | return document.system.quantity >= min; 169 | case "effect": 170 | return document.parent.effects.has(document.id); 171 | case "slots": 172 | return Object.values(document.system.spells).some(s => s.value && s.max && s.level && (s.level >= min)); 173 | case "health": 174 | return (hp.value + hp.temp) >= min; 175 | case "currency": 176 | return document.system.currency[this.subtype] >= min; 177 | case "inspiration": 178 | return document.system.attributes.inspiration; 179 | case "hitdice": 180 | return (["smallest", "largest"].includes(this.subtype) ? hd.value : hd.bySize[this.subtype] ?? 0) >= min; 181 | default: 182 | return false; 183 | } 184 | } 185 | 186 | /* -------------------------------------------------- */ 187 | 188 | /** 189 | * Get applicable roll data from the origin. 190 | * @returns {object} The roll data. 191 | */ 192 | getRollData() { 193 | return this.bonus.getRollData({deterministic: true}); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /scripts/hooks.mjs: -------------------------------------------------------------------------------- 1 | import {MODULE, SETTINGS} from "./constants.mjs"; 2 | import * as filterings from "./applications/filterings.mjs"; 3 | import api from "./api.mjs"; 4 | import applications from "./applications/_module.mjs"; 5 | import characterSheetTabSetup from "./applications/character-sheet-tab.mjs"; 6 | import enricherSetup from "./applications/enrichers.mjs"; 7 | import fields from "./fields/_module.mjs"; 8 | import injections from "./applications/injections.mjs"; 9 | import models from "./models/_module.mjs"; 10 | import mutators from "./mutators.mjs"; 11 | import OptionalSelector from "./applications/optional-selector.mjs"; 12 | import registry from "./registry.mjs"; 13 | 14 | // Setup API object. 15 | globalThis.babonus = { 16 | ...api, 17 | abstract: { 18 | DataModels: models.Babonus, 19 | DataFields: { 20 | fields: fields, 21 | models: models 22 | }, 23 | TYPES: Object.keys(models.Babonus), 24 | applications: applications 25 | }, 26 | filters: {...filterings.filters} 27 | }; 28 | 29 | /* -------------------------------------------------- */ 30 | 31 | /** 32 | * Render the optional bonus selector on a roll dialog. 33 | * @param {Dialog} dialog The dialog being rendered. 34 | */ 35 | async function _renderDialog(dialog) { 36 | const m = dialog.options.babonus; 37 | if (!m) return; 38 | const r = registry.get(m.registry); 39 | if (!r) return; 40 | r.dialog = dialog; 41 | new OptionalSelector(m.registry).render(); 42 | } 43 | 44 | /* -------------------------------------------------- */ 45 | 46 | /* Settings. */ 47 | function _createSettings() { 48 | game.settings.register(MODULE.ID, SETTINGS.PLAYERS, { 49 | name: "BABONUS.SettingsShowBuilderForPlayersName", 50 | hint: "BABONUS.SettingsShowBuilderForPlayersHint", 51 | scope: "world", 52 | config: true, 53 | type: Boolean, 54 | default: true 55 | }); 56 | 57 | game.settings.register(MODULE.ID, SETTINGS.LABEL, { 58 | name: "BABONUS.SettingsDisplayLabelName", 59 | hint: "BABONUS.SettingsDisplayLabelHint", 60 | scope: "world", 61 | config: true, 62 | type: Boolean, 63 | default: false 64 | }); 65 | 66 | game.settings.register(MODULE.ID, SETTINGS.SCRIPT, { 67 | name: "BABONUS.SettingsDisableCustomScriptFilterName", 68 | hint: "BABONUS.SettingsDisableCustomScriptFilterHint", 69 | scope: "world", 70 | config: true, 71 | type: Boolean, 72 | default: false, 73 | requiresReload: true 74 | }); 75 | 76 | game.settings.register(MODULE.ID, SETTINGS.AURA, { 77 | name: "BABONUS.SettingsShowAuraRangesName", 78 | hint: "BABONUS.SettingsShowAuraRangesHint", 79 | scope: "world", 80 | config: true, 81 | type: Boolean, 82 | default: false, 83 | requiresReload: false 84 | }); 85 | 86 | game.settings.register(MODULE.ID, SETTINGS.RADIUS, { 87 | name: "BABONUS.SettingsPadAuraRadius", 88 | hint: "BABONUS.SettingsPadAuraRadiusHint", 89 | scope: "world", 90 | config: true, 91 | type: Boolean, 92 | default: true, 93 | requiresReload: false 94 | }); 95 | 96 | // Allow for modifiers to the fumble range to go below 1? 97 | game.settings.register(MODULE.ID, SETTINGS.FUMBLE, { 98 | name: "BABONUS.SettingsAllowFumbleNegationName", 99 | hint: "BABONUS.SettingsAllowFumbleNegationHint", 100 | scope: "world", 101 | config: true, 102 | type: Boolean, 103 | default: false, 104 | requiresReload: false 105 | }); 106 | 107 | game.settings.register(MODULE.ID, SETTINGS.SHEET_TAB, { 108 | name: "BABONUS.SettingsShowSheetTab", 109 | hint: "BABONUS.SettingsShowSheetTabHint", 110 | scope: "world", 111 | config: true, 112 | type: Boolean, 113 | default: false, 114 | requiresReload: true 115 | }); 116 | } 117 | 118 | /* -------------------------------------------------- */ 119 | 120 | /** 121 | * On-drop handler for the hotbar. 122 | * @param {Hotbar} bar The hotbar application. 123 | * @param {object} dropData The drop data. 124 | * @param {string} dropData.type The type of the dropped document. 125 | * @param {string} dropData.uuid The uuid of the dropped document. 126 | * @param {number} slot The slot on the hotbar where it was dropped. 127 | */ 128 | async function _onHotbarDrop(bar, {type, uuid}, slot) { 129 | if (type !== "Babonus") return; 130 | const bonus = await babonus.fromUuid(uuid); 131 | const data = { 132 | img: bonus.img, 133 | command: `babonus.hotbarToggle("${uuid}");`, 134 | name: `${game.i18n.localize("BABONUS.ToggleBonus")}: ${bonus.name}`, 135 | type: CONST.MACRO_TYPES.SCRIPT 136 | }; 137 | const macro = game.macros.find(m => { 138 | return Object.entries(data).every(([k, v]) => m[k] === v) && m.isAuthor; 139 | }) ?? await Macro.implementation.create(data); 140 | return game.user.assignHotbarMacro(macro, slot); 141 | } 142 | 143 | /* -------------------------------------------------- */ 144 | 145 | /** Setup the global 'trees' for proficiency searching. */ 146 | async function setupTree() { 147 | const trees = {}; 148 | for (const k of ["languages", "weapon", "armor", "tool", "skills"]) { 149 | trees[k] = await dnd5e.documents.Trait.choices(k); 150 | } 151 | babonus.trees = trees; 152 | } 153 | 154 | /* -------------------------------------------------- */ 155 | 156 | // General setup. 157 | Hooks.once("init", _createSettings); 158 | Hooks.once("init", enricherSetup); 159 | Hooks.once("init", () => game.modules.get(MODULE.ID).api = globalThis.babonus); 160 | Hooks.on("hotbarDrop", _onHotbarDrop); 161 | Hooks.once("setup", () => characterSheetTabSetup()); 162 | 163 | // Any application injections. 164 | Hooks.on("getActiveEffectConfigHeaderButtons", (...T) => injections.HeaderButton.inject(...T)); 165 | Hooks.on("getActorSheetHeaderButtons", (...T) => injections.HeaderButton.inject(...T)); 166 | Hooks.on("getDialogHeaderButtons", (...T) => injections.HeaderButtonDialog.inject(...T)); 167 | Hooks.on("getItemSheetHeaderButtons", (...T) => injections.HeaderButton.inject(...T)); 168 | Hooks.on("renderDialog", _renderDialog); 169 | Hooks.on("renderRollConfigurationDialog", _renderDialog); 170 | Hooks.on("renderRegionConfig", injections.injectRegionConfigElement); 171 | 172 | // Roll hooks. Delay these to let other modules modify behaviour first. 173 | Hooks.once("ready", function() { 174 | Hooks.callAll("babonus.preInitializeRollHooks"); 175 | 176 | Hooks.on("dnd5e.postActivityConsumption", mutators.postActivityConsumption); 177 | Hooks.on("dnd5e.preRollAbilitySave", mutators.preRollAbilitySave); 178 | Hooks.on("dnd5e.preRollAbilityTest", mutators.preRollAbilityTest); 179 | Hooks.on("dnd5e.preRollAttackV2", mutators.preRollAttack); 180 | Hooks.on("dnd5e.preRollDamageV2", mutators.preRollDamage); 181 | Hooks.on("dnd5e.preRollDeathSave", mutators.preRollDeathSave); 182 | Hooks.on("dnd5e.preRollHitDieV2", mutators.preRollHitDie); 183 | Hooks.on("dnd5e.preRollSkill", mutators.preRollSkill); 184 | Hooks.on("dnd5e.preRollToolCheck", mutators.preRollToolCheck); 185 | Hooks.on("dnd5e.preCreateActivityTemplate", mutators.preCreateActivityTemplate); 186 | 187 | Hooks.callAll("babonus.initializeRollHooks"); 188 | }); 189 | 190 | Hooks.once("init", function() { 191 | const hook = game.modules.get("babele")?.active && (game.babele?.initialized === false) ? "babele.ready" : "ready"; 192 | Hooks.once(hook, () => setupTree()); 193 | }); 194 | 195 | Hooks.once("i18nInit", function() { 196 | for (const model of Object.values(babonus.abstract.DataFields.models.Babonus)) { 197 | Localization.localizeDataModel(model); 198 | } 199 | 200 | const localizeObject = object => { 201 | for (const [k, v] of Object.entries(object)) { 202 | object[k] = game.i18n.localize(v); 203 | } 204 | }; 205 | 206 | localizeObject(MODULE.ATTACK_MODES_CHOICES); 207 | localizeObject(MODULE.CONSUMPTION_TYPES); 208 | localizeObject(MODULE.DISPOSITION_TYPES); 209 | localizeObject(MODULE.HEALTH_PERCENTAGES_CHOICES); 210 | localizeObject(MODULE.MODIFIER_MODES); 211 | localizeObject(MODULE.SPELL_COMPONENT_CHOICES); 212 | localizeObject(MODULE.TOKEN_SIZES_CHOICES); 213 | }); 214 | -------------------------------------------------------------------------------- /scripts/applications/character-sheet-tab.mjs: -------------------------------------------------------------------------------- 1 | import {MODULE, SETTINGS} from "../constants.mjs"; 2 | 3 | const SHEET_MAPPINGS = new Map(); 4 | 5 | /** 6 | * Handle rendering a new tab on the v2 character sheet. 7 | * @param {ActorSheet} sheet The rendered sheet. 8 | * @param {HTMLElement} html The element of the sheet. 9 | */ 10 | async function _onRenderCharacterSheet2(sheet, [html]) { 11 | const template = "modules/babonus/templates/subapplications/character-sheet-tab.hbs"; 12 | 13 | const bonuses = {}; 14 | const uuids = new Set(); 15 | SHEET_MAPPINGS.set(sheet.document.uuid, new Map()); 16 | 17 | async function _prepareBonus(bonus, rollData) { 18 | SHEET_MAPPINGS.get(sheet.document.uuid).set(bonus.uuid, bonus); 19 | uuids.add(bonus.uuid); 20 | const section = bonuses[bonus.type] ??= {}; 21 | section.label ??= `BABONUS.${bonus.type.toUpperCase()}.Label`; 22 | section.key ??= bonus.type; 23 | section.bonuses ??= []; 24 | section.bonuses.push({ 25 | bonus: bonus, 26 | labels: bonus.sheet._prepareLabels().slice(1).filterJoin(" • "), 27 | tooltip: await TextEditor.enrichHTML(bonus.description, { 28 | rollData: rollData, relativeTo: bonus.origin 29 | }), 30 | isEmbedded: bonus.parent.isEmbedded, 31 | parentName: bonus.parent.name 32 | }); 33 | } 34 | 35 | const data = sheet.actor.getRollData(); 36 | for (const bonus of babonus.getCollection(sheet.actor)) await _prepareBonus(bonus, data); 37 | for (const item of sheet.actor.items) { 38 | const data = item.getRollData(); 39 | for (const bonus of babonus.getCollection(item)) await _prepareBonus(bonus, data); 40 | for (const effect of item.effects) for (const bonus of babonus.getCollection(effect)) { 41 | await _prepareBonus(bonus, data); 42 | } 43 | } 44 | for (const effect of sheet.actor.effects) { 45 | for (const bonus of babonus.getCollection(effect)) await _prepareBonus(bonus, data); 46 | } 47 | 48 | bonuses.all = {label: "BABONUS.Bonuses", key: "all", bonuses: []}; 49 | 50 | const div = document.createElement("DIV"); 51 | const isActive = (sheet._tabs[0].active === MODULE.ID) ? "active" : ""; 52 | const isEdit = sheet.constructor.MODES.EDIT === sheet._mode; 53 | 54 | sheet._filters[MODULE.ID] ??= {name: "", properties: new Set()}; 55 | div.innerHTML = await renderTemplate(template, { 56 | ICON: MODULE.ICON, 57 | parentName: sheet.document.name, 58 | isActive: isActive, 59 | isEdit: isEdit, 60 | sections: Object.values(bonuses).sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang)) 61 | }); 62 | 63 | div.querySelectorAll("[data-action]").forEach(n => { 64 | n.addEventListener("click", async (event) => { 65 | const {clientX, clientY} = event; 66 | const target = event.currentTarget; 67 | const action = target.dataset.action; 68 | const uuid = target.closest("[data-item-uuid]")?.dataset.itemUuid; 69 | if (!uuid) return; 70 | switch (action) { 71 | case "toggle": 72 | return SHEET_MAPPINGS.get(sheet.document.uuid).get(uuid).toggle(); 73 | case "edit": 74 | return SHEET_MAPPINGS.get(sheet.document.uuid).get(uuid).sheet.render({force: true}); 75 | case "delete": 76 | return SHEET_MAPPINGS.get(sheet.document.uuid).get(uuid).deleteDialog(); 77 | case "contextMenu": 78 | event.preventDefault(); 79 | event.stopPropagation(); 80 | return target.dispatchEvent(new PointerEvent("contextmenu", { 81 | view: window, bubbles: true, cancelable: true, clientX, clientY 82 | })); 83 | default: 84 | return; 85 | } 86 | }); 87 | }); 88 | 89 | div.firstElementChild.addEventListener("drop", async (event) => { 90 | const data = TextEditor.getDragEventData(event); 91 | if (!sheet.isEditable) return; 92 | const bonus = await babonus.fromUuid(data.uuid); 93 | if (!bonus || uuids.has(bonus.uuid)) return; 94 | babonus.embedBabonus(sheet.document, bonus); 95 | }); 96 | 97 | div.querySelectorAll("[data-item-uuid][draggable]").forEach(n => { 98 | n.addEventListener("dragstart", async (event) => { 99 | const uuid = event.currentTarget.dataset.itemUuid; 100 | const bab = SHEET_MAPPINGS.get(sheet.document.uuid).get(uuid); 101 | const dragData = bab.toDragData(); 102 | if (!dragData) return; 103 | event.dataTransfer.setData("text/plain", JSON.stringify(dragData)); 104 | }); 105 | }); 106 | 107 | div.querySelector("[data-action='otter-dance']").addEventListener("click", (event) => { 108 | const spin = [{transform: "rotate(0)"}, {transform: "rotate(360deg)"}]; 109 | const time = {duration: 1000, iterations: 1}; 110 | if (!event.currentTarget.getAnimations().length) event.currentTarget.animate(spin, time); 111 | }); 112 | 113 | div.querySelectorAll("[data-action='bonus-source']").forEach(n => { 114 | n.addEventListener("click", async (event) => { 115 | const uuid = event.currentTarget.dataset.uuid; 116 | const item = await fromUuid(uuid); 117 | return item.sheet.render(true); 118 | }); 119 | }); 120 | 121 | const body = html.querySelector(".tab-body"); 122 | if (!body || body.querySelector(":scope > .tab.babonus")) return; 123 | 124 | body.appendChild(div.firstElementChild); 125 | html.querySelectorAll("button.create-child").forEach(button => { 126 | // Assigning listener to all buttons due to weirdness on npc sheet. 127 | button.addEventListener("click", _createChildBonus.bind(sheet)); 128 | }); 129 | 130 | new dnd5e.applications.ContextMenu5e(html, ".item[data-item-uuid]", [], { 131 | onOpen: _onOpenContextMenu.bind(sheet) 132 | }); 133 | } 134 | 135 | /* -------------------------------------------------- */ 136 | 137 | /** 138 | * Populate the context menu options. 139 | * @this {ActorSheet} 140 | * @param {HTMLElement} element The targeted element. 141 | */ 142 | function _onOpenContextMenu(element) { 143 | const bonus = SHEET_MAPPINGS.get(this.document.uuid).get(element.dataset.itemUuid); 144 | ui.context.menuItems = [{ 145 | name: "BABONUS.ContextMenu.Edit", 146 | icon: "", 147 | callback: () => bonus.sheet.render({force: true}) 148 | }, { 149 | name: "BABONUS.ContextMenu.Duplicate", 150 | icon: "", 151 | callback: () => babonus.duplicateBonus(bonus) 152 | }, { 153 | name: "BABONUS.ContextMenu.Delete", 154 | icon: "", 155 | callback: () => bonus.deleteDialog() 156 | }, { 157 | name: "BABONUS.ContextMenu.Enable", 158 | icon: "", 159 | condition: () => !bonus.enabled, 160 | callback: () => bonus.toggle(), 161 | group: "instance" 162 | }, { 163 | name: "BABONUS.ContextMenu.Disable", 164 | icon: "", 165 | condition: () => bonus.enabled, 166 | callback: () => bonus.toggle(), 167 | group: "instance" 168 | }]; 169 | } 170 | 171 | /* -------------------------------------------------- */ 172 | 173 | /** 174 | * Utility method that creates a popup dialog for a new bonus. 175 | * @this {ActorSheet} 176 | * @returns {Promise} 177 | */ 178 | async function _createChildBonus() { 179 | if (!this.isEditable || (this._tabs[0]?.active !== MODULE.ID)) return; 180 | const template = "systems/dnd5e/templates/apps/document-create.hbs"; 181 | const data = { 182 | folders: [], 183 | folder: null, 184 | hasFolders: false, 185 | type: babonus.abstract.TYPES[0], 186 | types: babonus.abstract.TYPES.reduce((acc, type) => { 187 | const label = game.i18n.localize(`BABONUS.${type.toUpperCase()}.Label`); 188 | acc.push({ 189 | type: type, 190 | label: label, 191 | icon: babonus.abstract.DataModels[type].metadata.defaultImg 192 | }); 193 | return acc; 194 | }, []).sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang)) 195 | }; 196 | const title = game.i18n.localize("BABONUS.Create"); 197 | return Dialog.prompt({ 198 | content: await renderTemplate(template, data), 199 | label: title, 200 | title: title, 201 | render: (html) => { 202 | const app = html.closest(".app"); 203 | app.querySelectorAll(".window-header .header-button").forEach(btn => { 204 | const label = btn.innerText; 205 | const icon = btn.querySelector("i"); 206 | btn.innerHTML = icon.outerHTML; 207 | btn.dataset.tooltip = label; 208 | btn.setAttribute("aria-label", label); 209 | }); 210 | app.querySelector(".document-name").select(); 211 | }, 212 | callback: async (html) => { 213 | const data = new FormDataExtended(html.querySelector("FORM")).object; 214 | if (!data.name?.trim()) delete data.name; 215 | const bonus = babonus.createBabonus(data, this.document); 216 | return babonus.embedBabonus(this.document, bonus); 217 | }, 218 | rejectClose: false, 219 | options: {jQuery: false, width: 350, classes: ["dnd5e2", "create-document", "dialog", "babonus"]} 220 | }); 221 | } 222 | 223 | /* -------------------------------------------------- */ 224 | 225 | /** 226 | * Add a new tab to the v2 character sheet. 227 | */ 228 | function _addCharacterTab() { 229 | const classes = [ 230 | dnd5e.applications.actor.ActorSheet5eCharacter2, 231 | dnd5e.applications.actor.ActorSheet5eNPC2 232 | ]; 233 | for (const cls of classes) { 234 | cls.TABS.push({ 235 | tab: MODULE.ID, label: MODULE.NAME, icon: MODULE.ICON 236 | }); 237 | /*cls.FILTER_COLLECTIONS.babonus = function(c, f) { 238 | console.warn({c,f}) 239 | return Array.from(babonus.getCollection(this.document)); 240 | }; 241 | return;*/ 242 | const fn = cls.prototype._filterChildren; 243 | class sheet extends cls { 244 | /** @override */ 245 | _filterChildren(collection, filters) { 246 | if (collection !== "babonus") return fn.call(this, collection, filters); 247 | 248 | const embedded = babonus.findEmbeddedDocumentsWithBonuses(this.document); 249 | 250 | const actor = babonus.getCollection(this.document).contents; 251 | const items = embedded.items?.flatMap(item => babonus.getCollection(item).contents) ?? []; 252 | const effects = embedded.effects?.flatMap(effect => babonus.getCollection(effect).contents) ?? []; 253 | return actor.concat(items).concat(effects); 254 | } 255 | } 256 | cls.prototype._filterChildren = sheet.prototype._filterChildren; 257 | } 258 | } 259 | 260 | /* -------------------------------------------------- */ 261 | 262 | /** Initialize this part of the module. */ 263 | export default function characterSheetTabSetup() { 264 | if (!game.settings.get(MODULE.ID, SETTINGS.SHEET_TAB)) return; 265 | if (!game.user.isGM && !game.settings.get(MODULE.ID, SETTINGS.PLAYERS)) return; 266 | _addCharacterTab(); 267 | Hooks.on("renderActorSheet5eCharacter2", _onRenderCharacterSheet2); 268 | Hooks.on("renderActorSheet5eNPC2", _onRenderCharacterSheet2); 269 | } 270 | -------------------------------------------------------------------------------- /scripts/fields/semicolon-fields.mjs: -------------------------------------------------------------------------------- 1 | import FilterMixin from "./filter-mixin.mjs"; 2 | 3 | const {SetField, StringField} = foundry.data.fields; 4 | 5 | class BaseField extends FilterMixin(SetField) { 6 | /** @override */ 7 | static canExclude = true; 8 | 9 | /* -------------------------------------------------- */ 10 | 11 | /** @override */ 12 | static trash = false; 13 | 14 | /* -------------------------------------------------- */ 15 | 16 | /** 17 | * Encapsulate this in a fieldset when using the formGroup hbs helper? 18 | * @type {boolean} 19 | */ 20 | static fieldset = true; 21 | 22 | /* -------------------------------------------------- */ 23 | 24 | constructor(options = {}) { 25 | super(new StringField(), options); 26 | } 27 | 28 | /* -------------------------------------------------- */ 29 | 30 | /** @override */ 31 | _cast(value) { 32 | // If the given value is a string, split it at each ';' and trim the results to get an array. 33 | if (typeof value === "string") value = value.split(";").map(v => v.trim()); 34 | return super._cast(value); 35 | } 36 | 37 | /* -------------------------------------------------- */ 38 | 39 | /** @override */ 40 | _cleanType(value, source) { 41 | value = super._cleanType(value, source).reduce((acc, v) => { 42 | if (v) acc.add(v); 43 | return acc; 44 | }, new Set()); 45 | return Array.from(value); 46 | } 47 | 48 | /* -------------------------------------------------- */ 49 | 50 | /** @override */ 51 | _toInput(config) { 52 | if ((config.value instanceof Set) || Array.isArray(config.value)) { 53 | config.value = Array.from(config.value).join(";"); 54 | } 55 | return foundry.data.fields.StringField.prototype._toInput.call(this, config); 56 | } 57 | 58 | /* -------------------------------------------------- */ 59 | 60 | /** @override */ 61 | toFormGroup(formConfig, inputConfig) { 62 | const element = super.toFormGroup(formConfig, inputConfig); 63 | 64 | const input = element.querySelector("input"); 65 | const button = document.createElement("BUTTON"); 66 | button.dataset.action = "keysDialog"; 67 | button.dataset.property = input.name; 68 | button.dataset.id = this.constructor.name; 69 | button.type = "button"; 70 | button.innerHTML = ` ${game.i18n.localize("BABONUS.Keys")}`; 71 | input.after(button); 72 | 73 | if (this.constructor.fieldset) { 74 | const set = document.createElement("FIELDSET"); 75 | const label = element.querySelector("LABEL"); 76 | set.innerHTML = ` 77 | `; 83 | label.remove(); 84 | 85 | const hint = element.querySelector(".hint"); 86 | hint.remove(); 87 | set.appendChild(hint); 88 | 89 | set.appendChild(element); 90 | return set; 91 | } 92 | 93 | return element; 94 | } 95 | 96 | /* -------------------------------------------------- */ 97 | 98 | /** @override */ 99 | static render(bonus) { 100 | const template = "{{formGroup field value=value}}"; 101 | const data = { 102 | field: bonus.schema.getField(`filters.${this.name}`), 103 | value: bonus.filters[this.name] 104 | }; 105 | 106 | return Handlebars.compile(template)(data); 107 | } 108 | 109 | /* -------------------------------------------------- */ 110 | 111 | /** 112 | * Retrieve the choices for a Keys dialog when configuring this field. 113 | * @returns {{value: string, label: string}[]} 114 | */ 115 | static choices() { 116 | throw new Error("This must be subclassed!"); 117 | } 118 | } 119 | 120 | /* -------------------------------------------------- */ 121 | 122 | class AbilitiesField extends BaseField { 123 | /** @override */ 124 | static name = "abilities"; 125 | 126 | /* -------------------------------------------------- */ 127 | 128 | /** @override */ 129 | static canExclude = true; 130 | 131 | /* -------------------------------------------------- */ 132 | 133 | /** @override */ 134 | static choices() { 135 | const abilities = Object.entries(CONFIG.DND5E.abilities); 136 | return abilities.map(([value, {label}]) => ({value, label})); 137 | } 138 | } 139 | 140 | /* -------------------------------------------------- */ 141 | 142 | class SaveAbilitiesField extends AbilitiesField { 143 | /** @override */ 144 | static name = "saveAbilities"; 145 | } 146 | 147 | /* -------------------------------------------------- */ 148 | 149 | class ThrowTypesField extends AbilitiesField { 150 | /** @override */ 151 | static name = "throwTypes"; 152 | 153 | /* -------------------------------------------------- */ 154 | 155 | /** @override */ 156 | static canExclude = false; 157 | 158 | /* -------------------------------------------------- */ 159 | 160 | /** @override */ 161 | static choices() { 162 | const choices = super.choices(); 163 | 164 | choices.push({ 165 | value: "death", 166 | label: game.i18n.localize("DND5E.DeathSave") 167 | }, { 168 | value: "concentration", 169 | label: game.i18n.localize("DND5E.Concentration") 170 | }); 171 | 172 | return choices; 173 | } 174 | } 175 | 176 | /* -------------------------------------------------- */ 177 | 178 | class StatusEffectsField extends BaseField { 179 | /** @override */ 180 | static name = "statusEffects"; 181 | 182 | /* -------------------------------------------------- */ 183 | 184 | /** @override */ 185 | static choices() { 186 | return CONFIG.statusEffects.reduce((acc, {id, img, name}) => { 187 | if (id && img && name) acc.push({value: id, label: name, icon: img}); 188 | return acc; 189 | }, []); 190 | } 191 | } 192 | 193 | /* -------------------------------------------------- */ 194 | 195 | class TargetEffectsField extends StatusEffectsField { 196 | /** @override */ 197 | static name = "targetEffects"; 198 | } 199 | 200 | /* -------------------------------------------------- */ 201 | 202 | class AuraBlockersField extends StatusEffectsField { 203 | /** @override */ 204 | static name = "auraBlockers"; 205 | 206 | /* -------------------------------------------------- */ 207 | 208 | /** @override */ 209 | static canExclude = false; 210 | 211 | /* -------------------------------------------------- */ 212 | 213 | /** @override */ 214 | static trash = false; 215 | 216 | /* -------------------------------------------------- */ 217 | 218 | /** @override */ 219 | static fieldset = false; 220 | } 221 | 222 | /* -------------------------------------------------- */ 223 | 224 | class CreatureTypesField extends BaseField { 225 | /** @override */ 226 | static name = "creatureTypes"; 227 | 228 | /* -------------------------------------------------- */ 229 | 230 | /** @override */ 231 | static choices() { 232 | const types = Object.entries(CONFIG.DND5E.creatureTypes); 233 | return types.map(([k, v]) => { 234 | return {value: k, label: v.label}; 235 | }).sort((a, b) => a.label.localeCompare(b.label)); 236 | } 237 | } 238 | 239 | /* -------------------------------------------------- */ 240 | 241 | class ActorCreatureTypesField extends CreatureTypesField { 242 | /** @override */ 243 | static name = "actorCreatureTypes"; 244 | } 245 | 246 | /* -------------------------------------------------- */ 247 | 248 | class BaseArmorsField extends BaseField { 249 | /** @override */ 250 | static name = "baseArmors"; 251 | 252 | /* -------------------------------------------------- */ 253 | 254 | /** @override */ 255 | static choices() { 256 | return Array.from(babonus.trees.armor.asSet()).map(k => { 257 | return { 258 | value: k, 259 | label: dnd5e.documents.Trait.keyLabel(`armor:${k}`) 260 | }; 261 | }); 262 | } 263 | } 264 | 265 | /* -------------------------------------------------- */ 266 | 267 | class TargetArmorsField extends BaseArmorsField { 268 | /** @override */ 269 | static name = "targetArmors"; 270 | } 271 | 272 | /* -------------------------------------------------- */ 273 | 274 | class BaseToolsField extends BaseField { 275 | /** @override */ 276 | static name = "baseTools"; 277 | 278 | /* -------------------------------------------------- */ 279 | 280 | /** @override */ 281 | static choices() { 282 | return Array.from(babonus.trees.tool.asSet()).map(k => { 283 | return { 284 | value: k, 285 | label: dnd5e.documents.Trait.keyLabel(`tool:${k}`) 286 | }; 287 | }); 288 | } 289 | } 290 | 291 | /* -------------------------------------------------- */ 292 | 293 | class BaseWeaponsField extends BaseField { 294 | /** @override */ 295 | static name = "baseWeapons"; 296 | 297 | /* -------------------------------------------------- */ 298 | 299 | /** @override */ 300 | static choices() { 301 | return Array.from(babonus.trees.weapon.asSet()).map(k => { 302 | return { 303 | value: k, 304 | label: dnd5e.documents.Trait.keyLabel(`weapon:${k}`) 305 | }; 306 | }); 307 | } 308 | } 309 | 310 | /* -------------------------------------------------- */ 311 | 312 | class DamageTypesField extends BaseField { 313 | /** @override */ 314 | static name = "damageTypes"; 315 | 316 | /* -------------------------------------------------- */ 317 | 318 | /** @override */ 319 | static choices() { 320 | const damages = Object.entries(CONFIG.DND5E.damageTypes); 321 | const heals = Object.entries(CONFIG.DND5E.healingTypes); 322 | return [...damages, ...heals].map(([k, v]) => ({value: k, label: v.label})); 323 | } 324 | } 325 | 326 | /* -------------------------------------------------- */ 327 | 328 | class SkillIdsField extends BaseField { 329 | /** @override */ 330 | static name = "skillIds"; 331 | 332 | /* -------------------------------------------------- */ 333 | 334 | /** @override */ 335 | static choices() { 336 | return Array.from(babonus.trees.skills.asSet()).map(k => { 337 | return { 338 | value: k, 339 | label: dnd5e.documents.Trait.keyLabel(`skills:${k}`) 340 | }; 341 | }); 342 | } 343 | } 344 | 345 | /* -------------------------------------------------- */ 346 | 347 | class SpellSchoolsField extends BaseField { 348 | /** @override */ 349 | static name = "spellSchools"; 350 | 351 | /* -------------------------------------------------- */ 352 | 353 | /** @override */ 354 | static choices() { 355 | const schools = Object.entries(CONFIG.DND5E.spellSchools); 356 | return schools.map(([k, v]) => ({value: k, label: v.label})); 357 | } 358 | } 359 | 360 | /* -------------------------------------------------- */ 361 | 362 | class WeaponPropertiesField extends BaseField { 363 | /** @override */ 364 | static name = "weaponProperties"; 365 | 366 | /* -------------------------------------------------- */ 367 | 368 | /** @override */ 369 | static choices() { 370 | const keys = CONFIG.DND5E.validProperties.weapon; 371 | const labels = CONFIG.DND5E.itemProperties; 372 | return keys.reduce((acc, k) => { 373 | const label = labels[k]?.label; 374 | if (label) acc.push({value: k, label: label}); 375 | return acc; 376 | }, []); 377 | } 378 | } 379 | 380 | /* -------------------------------------------------- */ 381 | 382 | class ActorLanguagesField extends BaseField { 383 | /** @override */ 384 | static name = "actorLanguages"; 385 | 386 | /* -------------------------------------------------- */ 387 | 388 | /** @override */ 389 | static choices() { 390 | const trait = dnd5e.documents.Trait; 391 | const choices = babonus.trees.languages; 392 | 393 | const langs = new Set(); 394 | const cats = new Set(); 395 | 396 | const construct = (c) => { 397 | for (const [key, choice] of Object.entries(c)) { 398 | if (choice.children) { 399 | cats.add(key); 400 | construct(choice.children); 401 | } else langs.add(key); 402 | } 403 | }; 404 | 405 | construct(choices); 406 | 407 | const toLabel = (k, isCat = true) => ({value: k, label: trait.keyLabel(`languages:${k}`), isCategory: isCat}); 408 | 409 | return Array.from(cats.map(k => toLabel(k, true))).concat(Array.from(langs.map(k => toLabel(k, false)))); 410 | } 411 | } 412 | 413 | /* -------------------------------------------------- */ 414 | 415 | export default { 416 | AbilitiesField, 417 | ActorCreatureTypesField, 418 | ActorLanguagesField, 419 | AuraBlockersField, 420 | BaseArmorsField, 421 | BaseToolsField, 422 | BaseWeaponsField, 423 | CreatureTypesField, 424 | DamageTypesField, 425 | SaveAbilitiesField, 426 | SkillIdsField, 427 | SpellSchoolsField, 428 | StatusEffectsField, 429 | TargetArmorsField, 430 | TargetEffectsField, 431 | ThrowTypesField, 432 | WeaponPropertiesField 433 | }; 434 | -------------------------------------------------------------------------------- /scripts/applications/babonus-workshop.mjs: -------------------------------------------------------------------------------- 1 | import {MODULE} from "../constants.mjs"; 2 | 3 | export default class BabonusWorkshop extends dnd5e.applications.DialogMixin(Application) { 4 | constructor(object, options) { 5 | super(object, options); 6 | this.object = object; 7 | this.isItem = object.documentName === "Item"; 8 | this.isEffect = object.documentName === "ActiveEffect"; 9 | this.isActor = object.documentName === "Actor"; 10 | this.appId = `${this.document.uuid.replaceAll(".", "-")}-babonus-workshop`; 11 | } 12 | 13 | /* -------------------------------------------------- */ 14 | 15 | /** 16 | * The right-hand side bonuses that have a collapsed description. 17 | * @type {Set