{"version":3,"file":"dnd5e.mjs","sources":["module/utils.mjs","module/applications/context-menu.mjs","module/applications/api/application-v2-mixin.mjs","module/applications/api/application.mjs","module/applications/api/dialog.mjs","module/applications/actor/rest/base-rest-dialog.mjs","module/applications/actor/rest/long-rest-dialog.mjs","module/applications/actor/rest/short-rest-dialog.mjs","module/data/calendar/calendar-data.mjs","module/applications/calendar/base-calendar-hud.mjs","module/applications/calendar/set-date-dialog.mjs","module/applications/calendar/calendar-hud.mjs","module/canvas/map-location-control-icon.mjs","module/dice/simplify-roll-formula.mjs","module/data/fields/embedded-data-field.mjs","module/data/fields/formula-field.mjs","module/data/activity/fields/consumption-targets-field.mjs","module/data/shared/source-field.mjs","module/data/fields/mapping-field.mjs","module/data/actor/fields/travel-field.mjs","module/applications/create-document-dialog.mjs","module/applications/activity/activity-choice-dialog.mjs","module/applications/api/pseudo-document-sheet.mjs","module/applications/advancement/advancement-config-v2.mjs","module/applications/advancement/advancement-flow-v2.mjs","module/data/abstract/sparse-data-model.mjs","module/data/fields/advancement-data-field.mjs","module/data/advancement/base-advancement.mjs","module/documents/mixins/pseudo-document.mjs","module/documents/advancement/advancement.mjs","module/applications/advancement/advancement-manager.mjs","module/applications/advancement/advancement-confirmation-dialog.mjs","module/applications/item/create-scroll-dialog.mjs","module/documents/actor/select-choices.mjs","module/documents/actor/trait.mjs","module/applications/advancement/trait-config.mjs","module/applications/advancement/trait-flow.mjs","module/data/advancement/trait.mjs","module/documents/advancement/trait.mjs","module/data/abstract/system-data-model.mjs","module/data/abstract/item-data-model.mjs","module/data/item/fields/spellcasting-field.mjs","module/data/fields/advancement-field.mjs","module/data/fields/advancement-collection-field.mjs","module/data/item/templates/advancement.mjs","module/data/fields/identifier-field.mjs","module/data/item/templates/item-description.mjs","module/dice/aggregate-damage-rolls.mjs","module/data/shared/activation-field.mjs","module/data/shared/duration-field.mjs","module/data/shared/range-field.mjs","module/data/shared/target-field.mjs","module/data/shared/uses-field.mjs","module/data/activity/fields/applied-effect-field.mjs","module/data/activity/base-activity.mjs","module/data/fields/activities-field.mjs","module/data/fields/advantage-mode-field.mjs","module/data/fields/local-document-field.mjs","module/data/item/templates/starting-equipment.mjs","module/data/item/class.mjs","module/data/shared/currency.mjs","module/data/item/templates/equippable-item.mjs","module/data/item/templates/identifiable.mjs","module/data/item/templates/physical-item.mjs","module/data/item/container.mjs","module/applications/activity/activity-sheet.mjs","module/applications/activity/cast-sheet.mjs","module/data/activity/cast-data.mjs","module/applications/activity/activity-usage-dialog.mjs","module/canvas/ability-template.mjs","module/data/chat-message/fields/deltas-field.mjs","module/documents/mixins/dependent.mjs","module/documents/activity/mixin.mjs","module/documents/activity/cast.mjs","module/data/item/templates/activities.mjs","module/data/item/templates/item-type.mjs","module/data/item/templates/mountable.mjs","module/data/item/fields/item-type-field.mjs","module/data/item/equipment.mjs","module/applications/award.mjs","module/applications/dice/roll-configuration-dialog.mjs","module/applications/dice/d20-configuration-dialog.mjs","module/applications/dice/attack-configuration-dialog.mjs","module/documents/macro.mjs","module/enrichers.mjs","module/data/item/spell.mjs","module/documents/scaling.mjs","module/documents/actor/proficiency.mjs","module/documents/mixins/flags.mjs","module/documents/mixins/document.mjs","module/documents/item.mjs","module/documents/active-effect.mjs","module/data/shared/movement-field.mjs","module/data/shared/roll-config-field.mjs","module/data/shared/senses-field.mjs","module/data/actor/templates/attributes.mjs","module/data/abstract/actor-data-model.mjs","module/data/actor/templates/common.mjs","module/data/actor/templates/details.mjs","module/data/actor/fields/simple-trait-field.mjs","module/data/actor/fields/damage-trait-field.mjs","module/data/actor/templates/traits.mjs","module/data/actor/vehicle.mjs","module/data/calendar/calendar-of-greyhawk.mjs","module/data/calendar/calendar-of-harptos.mjs","module/data/calendar/calendar-of-khorvaire.mjs","module/applications/activity/attack-sheet.mjs","module/data/shared/damage-field.mjs","module/data/activity/attack-data.mjs","module/documents/activity/attack.mjs","module/applications/activity/check-sheet.mjs","module/data/activity/check-data.mjs","module/documents/activity/check.mjs","module/applications/activity/damage-sheet.mjs","module/data/activity/damage-data.mjs","module/documents/activity/damage.mjs","module/applications/activity/enchant-sheet.mjs","module/applications/activity/enchant-usage-dialog.mjs","module/data/activity/enchant-data.mjs","module/documents/activity/enchant.mjs","module/applications/activity/forward-sheet.mjs","module/data/activity/forward-data.mjs","module/documents/activity/forward.mjs","module/applications/activity/heal-sheet.mjs","module/data/activity/heal-data.mjs","module/documents/activity/heal.mjs","module/data/activity/order-data.mjs","module/applications/activity/order-usage-dialog.mjs","module/applications/currency-manager.mjs","module/documents/activity/order.mjs","module/applications/activity/save-sheet.mjs","module/data/activity/save-data.mjs","module/documents/activity/save.mjs","module/applications/activity/summon-sheet.mjs","module/applications/activity/summon-usage-dialog.mjs","module/filter.mjs","module/applications/settings/compendium-browser-settings.mjs","module/applications/compendium-browser.mjs","module/canvas/token-placement.mjs","module/data/activity/summon-data.mjs","module/documents/activity/summon.mjs","module/applications/fields.mjs","module/data/settings/transformation-setting.mjs","module/applications/activity/transform-sheet.mjs","module/applications/activity/transform-usage-dialog.mjs","module/data/activity/transform-data.mjs","module/documents/activity/transform.mjs","module/applications/activity/utility-sheet.mjs","module/data/activity/utility-data.mjs","module/documents/activity/utility.mjs","module/applications/dice/skill-tool-configuration-dialog.mjs","module/applications/property-attribution.mjs","module/data/chat-message/fields/activations-field.mjs","module/documents/actor/actor.mjs","module/applications/advancement/ability-score-improvement-config.mjs","module/applications/advancement/ability-score-improvement-flow.mjs","module/data/advancement/ability-score-improvement.mjs","module/documents/advancement/ability-score-improvement.mjs","module/applications/advancement/hit-points-config.mjs","module/applications/advancement/hit-points-flow.mjs","module/documents/advancement/hit-points.mjs","module/applications/advancement/item-choice-config.mjs","module/applications/advancement/item-grant-flow-v2.mjs","module/applications/advancement/item-choice-flow.mjs","module/data/advancement/spell-config.mjs","module/data/advancement/item-choice.mjs","module/applications/advancement/item-grant-config.mjs","module/data/advancement/item-grant.mjs","module/documents/advancement/item-grant.mjs","module/documents/advancement/item-choice.mjs","module/data/advancement/scale-value.mjs","module/applications/advancement/scale-value-config.mjs","module/applications/advancement/scale-value-flow.mjs","module/documents/advancement/scale-value.mjs","module/applications/advancement/size-config.mjs","module/applications/advancement/size-flow.mjs","module/data/advancement/size.mjs","module/documents/advancement/size.mjs","module/applications/advancement/subclass-flow.mjs","module/data/advancement/subclass.mjs","module/documents/advancement/subclass.mjs","module/config.mjs","module/data/settings/bastion-setting.mjs","module/applications/settings/base-settings.mjs","module/applications/settings/bastion-settings.mjs","module/data/settings/calendar-setting.mjs","module/applications/settings/calendar-settings.mjs","module/applications/settings/combat-settings.mjs","module/module-art.mjs","module/applications/settings/module-art-settings.mjs","module/applications/settings/variant-rules-settings.mjs","module/applications/settings/visibility-settings.mjs","module/data/settings/primary-party-setting.mjs","module/config-legacy.mjs","module/settings.mjs","module/applications/actor/bastion-attack-dialog.mjs","module/drag-drop.mjs","module/applications/api/drag-drop-mixin.mjs","module/applications/components/adopted-stylesheet-mixin.mjs","module/applications/components/checkbox.mjs","module/applications/advancement/advancement-migration-dialog.mjs","module/applications/api/document-sheet.mjs","module/applications/components/effects.mjs","module/applications/actor/api/base-config-sheet.mjs","module/applications/shared/creature-type-config.mjs","module/applications/shared/movement-senses-config.mjs","module/applications/shared/source-config.mjs","module/applications/item/config/starting-equipment-config.mjs","module/applications/item/item-sheet.mjs","module/applications/api/primary-sheet-mixin.mjs","module/applications/actor/config/base-proficiency-config.mjs","module/applications/actor/config/ability-config.mjs","module/applications/actor/config/armor-class-config.mjs","module/applications/actor/config/concentration-config.mjs","module/applications/actor/config/traits-config.mjs","module/applications/actor/config/damages-config.mjs","module/applications/actor/config/death-config.mjs","module/applications/actor/config/hit-dice-config.mjs","module/applications/actor/config/hit-points-config.mjs","module/applications/actor/config/initiative-config.mjs","module/applications/actor/config/languages-config.mjs","module/applications/actor/config/skill-tool-config.mjs","module/applications/actor/config/skills-config.mjs","module/applications/actor/config/spell-slots-config.mjs","module/applications/actor/config/tools-config.mjs","module/applications/actor/config/weapons-config.mjs","module/applications/actor/transform-dialog.mjs","module/applications/components/filter-menu.mjs","module/applications/components/item-list-controls.mjs","module/applications/actor/api/base-actor-sheet.mjs","module/applications/actor/character-sheet.mjs","module/applications/actor/api/multi-actor-sheet.mjs","module/applications/actor/encounter-sheet.mjs","module/applications/actor/group-sheet.mjs","module/applications/actor/config/habitat-config.mjs","module/applications/actor/config/treasure-config.mjs","module/applications/actor/npc-sheet.mjs","module/applications/actor/vehicle-sheet.mjs","module/applications/advancement/advancement-config.mjs","module/applications/advancement/advancement-flow.mjs","module/applications/advancement/item-grant-flow.mjs","module/applications/combat/combat-tracker.mjs","module/applications/components/activities.mjs","module/applications/components/copyable-text.mjs","module/applications/components/chat-tray-element.mjs","module/applications/components/targeted-application-mixin.mjs","module/applications/components/damage-application.mjs","module/applications/components/effect-application.mjs","module/applications/components/enchantment-application.mjs","module/applications/components/filigree-box.mjs","module/applications/components/filter-state.mjs","module/applications/components/icon.mjs","module/applications/components/inventory.mjs","module/applications/components/proficiency-cycle.mjs","module/applications/components/slide-toggle.mjs","module/applications/components/_module.mjs","module/applications/dice/damage-configuration-dialog.mjs","module/applications/item/container-sheet.mjs","module/applications/item/item-compendium.mjs","module/applications/item/item-directory.mjs","module/applications/journal/journal-editor.mjs","module/applications/journal/class-page-sheet.mjs","module/applications/journal/map-page-sheet.mjs","module/applications/journal/rule-page-sheet.mjs","module/applications/journal/config/journal-navigation-config.mjs","module/applications/journal/journal-entry-sheet.mjs","module/applications/journal/journal-sheet.mjs","module/data/collection/items-collection.mjs","module/applications/journal/spells-unlinked-config.mjs","module/applications/journal/spells-page-sheet.mjs","module/applications/journal/table-of-contents.mjs","module/applications/region-behavior/difficult-terrain-config.mjs","module/applications/region-behavior/rotate-area-config.mjs","module/applications/settings/sidebar.mjs","module/applications/accordion.mjs","module/dice/basic-die.mjs","module/dice/basic-roll.mjs","module/dice/damage-roll.mjs","module/documents/chat-message.mjs","module/applications/chat-log.mjs","module/applications/dialog-mixin.mjs","module/applications/roll-table-sheet.mjs","module/applications/sheet-config.mjs","module/applications/tabs.mjs","module/applications/token-config.mjs","module/canvas/detection-modes/blindsight.mjs","module/canvas/layers/tokens.mjs","module/canvas/note.mjs","module/canvas/token.mjs","module/canvas/ruler.mjs","module/data/abstract.mjs","module/data/abstract/chat-message-data-model.mjs","module/data/active-effect/enchantment.mjs","module/data/active-effect/_module.mjs","module/documents/actor/hit-dice.mjs","module/data/shared/creature-type-field.mjs","module/data/actor/templates/creature.mjs","module/data/actor/character.mjs","module/data/actor/templates/group.mjs","module/data/actor/encounter.mjs","module/data/actor/group-system-flags.mjs","module/data/actor/group.mjs","module/data/actor/npc.mjs","module/data/actor/_module.mjs","module/data/chat-message/bastion-attack-message-data.mjs","module/data/chat-message/bastion-turn-message-data.mjs","module/data/chat-message/request-message-data.mjs","module/data/chat-message/rest-message-data.mjs","module/data/chat-message/turn-message-data.mjs","module/data/chat-message/usage-message-data.mjs","module/data/chat-message/_module.mjs","module/data/collection/actors-collection.mjs","module/data/item/background.mjs","module/data/item/consumable.mjs","module/data/item/facility.mjs","module/data/item/feat.mjs","module/data/item/loot.mjs","module/data/item/race.mjs","module/data/item/subclass.mjs","module/data/item/tool.mjs","module/data/item/weapon.mjs","module/data/item/_module.mjs","module/data/journal/class.mjs","module/data/journal/map.mjs","module/data/journal/rule.mjs","module/data/journal/spells.mjs","module/data/journal/subclass.mjs","module/data/journal/_module.mjs","module/data/region-behavior/difficult-terrain.mjs","module/data/region-behavior/rotate-area.mjs","module/data/region-behavior/_module.mjs","module/data/spellcasting/spellcasting-model.mjs","module/data/terrain-data.mjs","module/data/user/user-system-flags.mjs","module/dice/d20-die.mjs","module/dice/d20-roll.mjs","module/documents/adventure.mjs","module/documents/combat.mjs","module/documents/combatant.mjs","module/documents/combatant-group.mjs","module/documents/journal-entry-page.mjs","module/documents/token.mjs","module/documents/user.mjs","module/documents/actor/bastion.mjs","module/documents/_module.mjs","module/migration.mjs","module/module-registration.mjs","module/registry.mjs","module/tooltips.mjs","dnd5e.mjs"],"sourcesContent":["/**\n * @import { TargetDescriptor5e, UnitConfiguration } from \"./_types.mjs\";\n * @import { RollData } from \"./documents/_types.mjs\";\n */\n\n/* -------------------------------------------- */\n/* Collections */\n/* -------------------------------------------- */\n\n/**\n * Create a list of options for all documents within a collection grouped by folders.\n * @param {DirectoryCollection} collection Directory collection to use when building the options.\n * @param {object} [options={}]\n * @param {(object => boolean)} [options.disabled] Callback used to determine if an entry should be disabled.\n * @returns {FormSelectOption[]}\n */\nexport function getCollectionDocumentOptions(collection, { disabled }={}) {\n const options = [];\n const traverse = node => {\n if ( !node ) return;\n let group;\n if ( node.folder ) group = `${\"─\".repeat(node.folder.depth - 1)} ${node.folder.name}`.trim();\n for ( const entry of node.entries ) {\n const option = { value: entry._id, label: entry.name, group };\n if ( disabled ) option.disabled = disabled(entry);\n options.push(option);\n }\n node.children.forEach(traverse);\n };\n traverse(collection.tree);\n return options;\n}\n\n/* -------------------------------------------- */\n/* Formatters */\n/* -------------------------------------------- */\n\n/**\n * Format a Challenge Rating using the proper fractional symbols.\n * @param {number} value CR value to format.\n * @param {object} [options={}]\n * @param {boolean} [options.narrow=true] Use narrow fractions (e.g. ⅛) rather than wide ones (e.g. 1/8).\n * @returns {string}\n */\nexport function formatCR(value, { narrow=true }={}) {\n if ( value === null ) return \"—\";\n const fractions = narrow ? { 0.125: \"⅛\", 0.25: \"¼\", 0.5: \"½\" } : { 0.125: \"1/8\", 0.25: \"1/4\", 0.5: \"1/2\" };\n return fractions[value] ?? formatNumber(value);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Create a valid identifier from the provided string.\n * @param {string} input\n * @returns {string}\n */\nexport function formatIdentifier(input) {\n input = input.replaceAll(/(\\w+)([\\\\|/])(\\w+)/g, \"$1-$3\");\n return input.slugify({ strict: true });\n}\n\n/* -------------------------------------------- */\n\n/**\n * Form a number using the provided length unit.\n * @param {number} value The length to format.\n * @param {string} unit Length unit as defined in `CONFIG.DND5E.movementUnits`.\n * @param {object} [options={}] Formatting options passed to `formatNumber`.\n * @returns {string}\n */\nexport function formatLength(value, unit, options={}) {\n return _formatSystemUnits(value, unit, CONFIG.DND5E.movementUnits[unit], options);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Format a modifier for display with its sign separate.\n * @param {number} mod The modifier.\n * @returns {Handlebars.SafeString}\n */\nexport function formatModifier(mod) {\n if ( !Number.isFinite(mod) ) return new Handlebars.SafeString(\"—\");\n return new Handlebars.SafeString(`${mod < 0 ? \"-\" : \"+\"}${Math.abs(mod)}`);\n}\n\n/* -------------------------------------------- */\n\n/**\n * A helper for using Intl.NumberFormat within handlebars.\n * @param {number} value The value to format.\n * @param {object} options Options forwarded to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat}\n * @param {string} [options.blank] Format a zero or otherwise empty value as the given string.\n * @param {boolean} [options.numerals] Format the number as roman numerals.\n * @param {boolean} [options.ordinal] Use ordinal formatting.\n * @param {boolean} [options.words] Write out number as full word, if possible.\n * @returns {string}\n */\nexport function formatNumber(value, { blank, numerals, ordinal, words, ...options }={}) {\n if ( words && game.i18n.has(`DND5E.NUMBER.${value}`, false) ) return game.i18n.localize(`DND5E.NUMBER.${value}`);\n if ( !value && (typeof blank === \"string\") ) return blank;\n if ( numerals ) return _formatNumberAsNumerals(value);\n if ( ordinal ) return _formatNumberAsOrdinal(value, options);\n const formatter = new Intl.NumberFormat(game.i18n.lang, options);\n return formatter.format(value);\n}\n\n/**\n * Roman numerals.\n * @type {Record}\n */\nconst _roman = {\n M: 1000, CM: 900, D: 500, CD: 400, C: 100, XC: 90, L: 50, XL: 40, X: 10, IX: 9, V: 5, IV: 4, I: 1\n};\n\n/**\n * Format a number as roman numerals.\n * @param {number} n The number to format.\n * @returns {string}\n */\nfunction _formatNumberAsNumerals(n) {\n let out = \"\";\n if ( (n < 1) || !Number.isInteger(n) ) return out;\n for ( const [numeral, decimal] of Object.entries(_roman) ) {\n const quotient = Math.floor(n / decimal);\n n -= quotient * decimal;\n out += numeral.repeat(quotient);\n }\n return out;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Format a number using an ordinal format.\n * @param {number} n The number to format.\n * @param {object} options Options forwarded to `formatNumber`.\n * @returns {string}\n */\nfunction _formatNumberAsOrdinal(n, options={}) {\n const pr = getPluralRules({ type: \"ordinal\" }).select(n);\n const number = formatNumber(n, options);\n return game.i18n.has(`DND5E.ORDINAL.${pr}`) ? game.i18n.format(`DND5E.ORDINAL.${pr}`, { number }) : number;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Produce a number with the parts wrapped in their own spans.\n * @param {number} value A number for format.\n * @param {object} [options] Formatting options.\n * @returns {string}\n */\nexport function formatNumberParts(value, options) {\n if ( options.numerals ) throw new Error(\"Cannot segment numbers when formatted as numerals.\");\n return new Intl.NumberFormat(game.i18n.lang, options).formatToParts(value)\n .reduce((str, { type, value }) => `${str}${value}`, \"\");\n}\n\n/* -------------------------------------------- */\n\n/**\n * Form a number using the provided travel speed unit.\n * @param {number} value Travel speed to display.\n * @param {string} unit Unit as defined in `CONFIG.DND5E.travelUnits`.\n * @param {object} [options={}] Formatting options passed to `formatNumber`.\n * @param {string} [options.period=\"hour\"] Time period formatting unit (e.g. hour or day).\n * @returns {string}\n */\nexport function formatTravelSpeed(value, unit, { period=\"hour\", ...options }={}) {\n const unitConfig = CONFIG.DND5E.travelUnits[unit];\n options.unit ??= `${unitConfig?.formattingUnit ?? unit}-per-${period}`;\n return _formatSystemUnits(value, unit, unitConfig, options);\n}\n\n/* -------------------------------------------- */\n\n/**\n * A helper for using Intl.NumberFormat within handlebars for format a range.\n * @param {number} min The lower end of the range.\n * @param {number} max The upper end of the range.\n * @param {object} options Options forwarded to {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat}\n * @returns {string}\n */\nexport function formatRange(min, max, options) {\n const formatter = new Intl.NumberFormat(game.i18n.lang, options);\n return formatter.formatRange(min, max);\n}\n\n/* -------------------------------------------- */\n\n/**\n * A helper function to format textarea text to HTML with linebreaks.\n * @param {string} value The text to format.\n * @returns {Handlebars.SafeString}\n */\nexport function formatText(value) {\n return new Handlebars.SafeString(value?.replaceAll(\"\\n\", \"
\") ?? \"\");\n}\n\n/* -------------------------------------------- */\n\n/**\n * A helper function that formats a time in a human-readable format.\n * @param {number} value Time to display.\n * @param {string} unit Units as defined in `CONFIG.DND5E.timeUnits`.\n * @param {object} [options={}] Formatting options passed to `formatNumber`.\n * @returns {string}\n */\nexport function formatTime(value, unit, options={}) {\n options.maximumFractionDigits ??= 0;\n options.unitDisplay ??= \"long\";\n const config = CONFIG.DND5E.timeUnits[unit];\n if ( config?.counted ) {\n if ( (options.unitDisplay === \"narrow\") && game.i18n.has(`${config.counted}.narrow`) ) {\n return game.i18n.format(`${config.counted}.narrow`, { number: formatNumber(value, options) });\n } else {\n const pr = new Intl.PluralRules(game.i18n.lang);\n return game.i18n.format(`${config.counted}.${pr.select(value)}`, { number: formatNumber(value, options) });\n }\n }\n try {\n return formatNumber(value, { ...options, style: \"unit\", unit });\n } catch(err) {\n return formatNumber(value, options);\n }\n}\n\n/* -------------------------------------------- */\n\n/**\n * Form a number using the provided volume unit.\n * @param {number} value The volume to format.\n * @param {string} unit Volume unit as defined in `CONFIG.DND5E.volumeUnits`.\n * @param {object} [options={}] Formatting options passed to `formatNumber`.\n * @returns {string}\n */\nexport function formatVolume(value, unit, options={}) {\n return _formatSystemUnits(value, unit, CONFIG.DND5E.volumeUnits[unit], options);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Form a number using the provided weight unit.\n * @param {number} value The weight to format.\n * @param {string} unit Weight unit as defined in `CONFIG.DND5E.weightUnits`.\n * @param {object} [options={}] Formatting options passed to `formatNumber`.\n * @returns {string}\n */\nexport function formatWeight(value, unit, options={}) {\n return _formatSystemUnits(value, unit, CONFIG.DND5E.weightUnits[unit], options);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Format a number using one of core's built-in unit types.\n * @param {number} value Value to display.\n * @param {string} unit Name of the unit to use.\n * @param {UnitConfiguration} config Configuration data for the unit.\n * @param {object} [options={}] Formatting options passed to `formatNumber`.\n * @param {boolean} [options.parts=false] Format to parts.\n * @returns {string}\n */\nfunction _formatSystemUnits(value, unit, config, { parts=false, ...options }={}) {\n options.unitDisplay ??= \"short\";\n if ( config?.counted ) {\n const localizationKey = `${config.counted}.${options.unitDisplay}.${getPluralRules().select(value)}`;\n return game.i18n.format(localizationKey, { number: formatNumber(value, options) });\n }\n unit = config?.formattingUnit ?? unit;\n if ( isValidUnit(unit) ) {\n options.style ??= \"unit\";\n options.unit ??= unit;\n }\n return (parts ? formatNumberParts : formatNumber)(value, options);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Cached store of Intl.PluralRules instances.\n * @type {Record}\n */\nconst _pluralRules = {};\n\n/**\n * Get a PluralRules object, fetching from cache if possible.\n * @param {object} [options={}]\n * @param {string} [options.type=cardinal]\n * @returns {Intl.PluralRules}\n */\nexport function getPluralRules({ type=\"cardinal\" }={}) {\n _pluralRules[type] ??= new Intl.PluralRules(game.i18n.lang, { type });\n return _pluralRules[type];\n}\n\n/* -------------------------------------------- */\n/* Formulas */\n/* -------------------------------------------- */\n\n/**\n * Return whether a string is a valid reroll, explosion, min, or max dice modifier.\n * @param {string} mod The modifier to test.\n * @returns {boolean}\n */\nexport function isValidDieModifier(mod) {\n const regex = {\n reroll: /rr?([0-9]+)?([<>=]+)?([0-9]+)?/i,\n explode: /xo?([0-9]+)?([<>=]+)?([0-9]+)?/i,\n minimum: /(?:min)([0-9]+)/i,\n maximum: /(?:max)([0-9]+)/i,\n dropKeep: /[dk]([hl])?([0-9]+)?/i,\n count: /(?:c[sf])([<>=]+)?([0-9]+)?/i\n };\n return Object.values(regex).some(rgx => rgx.test(mod));\n}\n\n/* -------------------------------------------- */\n\n/**\n * Convert a delta string into a number.\n * @param {string} raw The raw string.\n * @param {number} target A target number to apply the delta to.\n * @returns {number}\n */\nexport function parseDelta(raw, target) {\n if ( !raw ) return target;\n let value = Number(raw);\n if ( (raw[0] === \"+\") || (raw[0] === \"-\") ) {\n const delta = parseFloat(raw);\n value = target + delta;\n }\n else if ( raw[0] === \"=\" ) value = Number(raw.slice(1));\n return Number.isNaN(value) ? target : value;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Handle a delta input for a number value from a form.\n * @param {HTMLInputElement} input Input that contains the modified value.\n * @param {Document} target Target document to be updated.\n * @returns {number|void}\n */\nexport function parseInputDelta(input, target) {\n if ( !input?.value ) return input?.value;\n const prop = input.dataset.name ?? input.name;\n let current = foundry.utils.getProperty(target?._source ?? {}, prop) ?? foundry.utils.getProperty(target, prop);\n const value = Math.clamp(\n parseDelta(input.value, Number(current)),\n input.dataset.min ?? -Infinity,\n input.dataset.max ?? Infinity\n );\n if ( Number.isNaN(value) ) return;\n input.value = value.toString();\n return value;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Prepare the final formula value for a model field.\n * @param {ItemDataModel|BaseActivityData} model Model for which the value is being prepared.\n * @param {string} keyPath Path to the field within the model.\n * @param {string} label Label to use in preparation warnings.\n * @param {RollData} rollData Roll data to use when replacing formula values.\n */\nexport function prepareFormulaValue(model, keyPath, label, rollData) {\n const value = foundry.utils.getProperty(model, keyPath);\n if ( !value ) return;\n const item = model.item ?? model.parent;\n const property = game.i18n.localize(label);\n try {\n const formula = replaceFormulaData(value, rollData, { item, property });\n const roll = new Roll(formula);\n foundry.utils.setProperty(model, keyPath, roll.evaluateSync().total);\n } catch(err) {\n if ( item.isEmbedded ) {\n const message = game.i18n.format(\"DND5E.FormulaMalformedError\", { property, name: model.name ?? item.name });\n item.actor._preparationWarnings.push({ message, link: item.uuid, type: \"error\" });\n console.error(message, err);\n }\n }\n}\n\n/* -------------------------------------------- */\n\n/**\n * Replace referenced data attributes in the roll formula with values from the provided data.\n * If the attribute is not found in the provided data, display a warning on the actor.\n * @param {string} formula The original formula within which to replace.\n * @param {object} data The data object which provides replacements.\n * @param {object} [options={}]\n * @param {Actor5e} [options.actor] Actor for which the value is being prepared.\n * @param {Item5e} [options.item] Item for which the value is being prepared.\n * @param {string|null} [options.missing=\"0\"] Value to use when replacing missing references, or `null` to not replace.\n * @param {string} [options.property] Name of the property to which this formula belongs.\n * @returns {string} Formula with replaced data.\n */\nexport function replaceFormulaData(formula, data, { actor, item, missing=\"0\", property }={}) {\n const dataRgx = new RegExp(/@([a-z.0-9_-]+)/gi);\n const missingReferences = new Set();\n formula = String(formula).replace(dataRgx, (match, term) => {\n let value = foundry.utils.getProperty(data, term);\n if ( value == null ) {\n missingReferences.add(match);\n return missing ?? match[0];\n }\n return String(value).trim();\n });\n actor ??= item?.parent;\n if ( (missingReferences.size > 0) && actor && property ) {\n const listFormatter = new Intl.ListFormat(game.i18n.lang, { style: \"long\", type: \"conjunction\" });\n const message = game.i18n.format(\"DND5E.FormulaMissingReferenceWarn\", {\n property, name: item?.name ?? actor.name, references: listFormatter.format(missingReferences)\n });\n actor._preparationWarnings.push({ message, link: item?.uuid ?? actor.uuid, type: \"warning\" });\n }\n return formula;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Convert a bonus value to a simple integer for displaying on the sheet.\n * @param {number|string|null} bonus Bonus formula.\n * @param {object} [data={}] Data to use for replacing @ strings.\n * @returns {number} Simplified bonus as an integer.\n * @protected\n */\nexport function simplifyBonus(bonus, data={}) {\n if ( !bonus ) return 0;\n if ( Number.isNumeric(bonus) ) return Number(bonus);\n try {\n const roll = new Roll(bonus, data);\n return roll.isDeterministic ? roll.evaluateSync().total : 0;\n } catch(error) {\n console.error(error);\n return 0;\n }\n}\n\n/* -------------------------------------------- */\n/* IDs */\n/* -------------------------------------------- */\n\n/**\n * Create an ID from the input truncating or padding the value to make it reach 16 characters.\n * @param {string} id\n * @returns {string}\n */\nexport function staticID(id) {\n if ( id.length >= 16 ) return id.substring(0, 16);\n return id.padEnd(16, \"0\");\n}\n\n/* -------------------------------------------- */\n/* Keybindings Helper */\n/* -------------------------------------------- */\n\nconst { MODIFIER_CODES: CODES, MODIFIER_KEYS } = (foundry.helpers?.interaction?.KeyboardManager ?? KeyboardManager);\n\n/**\n * Track which KeyboardEvent#code presses associate with each modifier.\n * Added support for treating Meta separate from Control.\n * @enum {string[]}\n */\nconst MODIFIER_CODES = {\n Alt: CODES.Alt,\n Control: CODES.Control.filter(k => k.startsWith(\"Control\")),\n Meta: CODES.Control.filter(k => !k.startsWith(\"Control\")),\n Shift: CODES.Shift\n};\n\n/**\n * Based on the provided event, determine if the keys are pressed to fulfill the specified keybinding.\n * @param {Event} event Triggering event.\n * @param {string} action Keybinding action within the `dnd5e` namespace.\n * @returns {boolean} Is the keybinding triggered?\n */\nexport function areKeysPressed(event, action) {\n if ( !event ) return false;\n const activeModifiers = {};\n const addModifiers = (key, pressed) => {\n activeModifiers[key] = pressed;\n MODIFIER_CODES[key].forEach(n => activeModifiers[n] = pressed);\n };\n addModifiers(MODIFIER_KEYS.ALT, event.altKey);\n addModifiers(MODIFIER_KEYS.CONTROL, event.ctrlKey);\n addModifiers(\"Meta\", event.metaKey);\n addModifiers(MODIFIER_KEYS.SHIFT, event.shiftKey);\n return game.keybindings.get(\"dnd5e\", action).some(b => {\n if ( game.keyboard.downKeys.has(b.key) && b.modifiers.every(m => activeModifiers[m]) ) return true;\n if ( b.modifiers.length ) return false;\n return activeModifiers[b.key];\n });\n}\n\n/* -------------------------------------------- */\n/* Logging */\n/* -------------------------------------------- */\n\n/**\n * Log a console message with the \"D&D 5e\" prefix and styling.\n * @param {string} message Message to display.\n * @param {object} [options={}]\n * @param {string} [options.color=\"#6e0000\"] Color to use for the log.\n * @param {any[]} [options.extras=[]] Extra options passed to the logging method.\n * @param {string} [options.level=\"log\"] Console logging method to call.\n */\nexport function log(message, { color=\"#6e0000\", extras=[], level=\"log\" }={}) {\n console[level](\n `%cD&D 5e | %c${message}`, `color: ${color}; font-variant: small-caps`, \"color: revert\", ...extras\n );\n}\n\n/* -------------------------------------------- */\n/* Object Helpers */\n/* -------------------------------------------- */\n\n/**\n * Transform an object, returning only the keys which match the provided filter.\n * @param {object} obj Object to transform.\n * @param {Function} [filter] Filtering function. If none is provided, it will just check for truthiness.\n * @returns {string[]} Array of filtered keys.\n */\nexport function filteredKeys(obj, filter) {\n filter ??= e => e;\n return Object.entries(obj).filter(e => filter(e[1])).map(e => e[0]);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Check whether an object exists without transversing any getters, preventing any deprecation warnings from triggering.\n * @param {object} object\n * @param {string} keyPath\n * @returns {boolean}\n */\nexport function safePropertyExists(object, keyPath) {\n const parts = keyPath.split(\".\");\n for ( const part of parts ) {\n const descriptor = Object.getOwnPropertyDescriptor(object, part);\n if ( !descriptor || !(\"value\" in descriptor) ) return false;\n object = object[part];\n }\n return true;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Sort the provided object by its values or by an inner sortKey.\n * @param {object} obj The object to sort.\n * @param {string|Function} [sortKey] An inner key upon which to sort or sorting function.\n * @returns {object} A copy of the original object that has been sorted.\n */\nexport function sortObjectEntries(obj, sortKey) {\n let sorted = Object.entries(obj);\n const sort = (lhs, rhs) => foundry.utils.getType(lhs) === \"string\" ? lhs.localeCompare(rhs, game.i18n.lang) : lhs - rhs;\n if ( foundry.utils.getType(sortKey) === \"function\" ) sorted = sorted.sort((lhs, rhs) => sortKey(lhs[1], rhs[1]));\n else if ( sortKey ) sorted = sorted.sort((lhs, rhs) => sort(lhs[1][sortKey], rhs[1][sortKey]));\n else sorted = sorted.sort((lhs, rhs) => sort(lhs[1], rhs[1]));\n return Object.fromEntries(sorted);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Retrieve the indexed data for a Document using its UUID. Will never return a result for embedded documents.\n * @param {string} uuid The UUID of the Document index to retrieve.\n * @returns {object} Document's index if one could be found.\n */\nexport function indexFromUuid(uuid) {\n const parts = uuid.split(\".\");\n let index;\n\n // Compendium Documents\n if ( parts[0] === \"Compendium\" ) {\n const [, scope, packName, id] = parts;\n const pack = game.packs.get(`${scope}.${packName}`);\n index = pack?.index.get(id);\n }\n\n // World Documents\n else if ( parts.length < 3 ) {\n const [docName, id] = parts;\n const collection = CONFIG[docName].collection.instance;\n index = collection.get(id);\n }\n\n return index || null;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Creates an HTML document link for the provided UUID.\n * Try to build links to compendium content synchronously to avoid DB lookups.\n * @param {string} uuid UUID for which to produce the link.\n * @param {object} [options]\n * @param {string} [options.tooltip] Tooltip to add to the link.\n * @param {string} [options.renderBroken] If a UUID cannot found, render it as a broken link instead of returning the\n * empty string.\n * @returns {string} Link to the item or empty string if item wasn't found.\n */\nexport function linkForUuid(uuid, { tooltip, renderBroken }={}) {\n let doc = fromUuidSync(uuid);\n if ( !doc ) {\n if ( renderBroken ) return `\n \n ${game.i18n.localize(\"COMMON.Unknown\")}\n \n `;\n return \"\";\n }\n if ( uuid.startsWith(\"Compendium.\") && !(doc instanceof foundry.abstract.Document) ) {\n const {collection} = foundry.utils.parseUuid(uuid);\n const cls = collection.documentClass;\n // Minimal \"shell\" of a document using index data\n doc = new cls(foundry.utils.deepClone(doc), {pack: collection.metadata.id});\n }\n const a = doc.toAnchor();\n if ( tooltip ) a.dataset.tooltip = tooltip;\n return a.outerHTML;\n}\n\n/* -------------------------------------------- */\n/* Targeting */\n/* -------------------------------------------- */\n\n/**\n * Grab the targeted tokens and return relevant information on them.\n * @returns {TargetDescriptor5e[]}\n */\nexport function getTargetDescriptors() {\n const targets = new Map();\n for ( const token of game.user.targets ) {\n const { name } = token;\n const { img, system, uuid, statuses } = token.actor ?? {};\n if ( uuid ) {\n const ac = statuses.has(\"coverTotal\") ? null : system.attributes?.ac?.value;\n targets.set(uuid, { name, img, uuid, ac: ac ?? null });\n }\n }\n return Array.from(targets.values());\n}\n\n/* -------------------------------------------- */\n\n/**\n * Get currently selected tokens in the scene or user's character's tokens.\n * @param {Actor5e} [actor] Only allow tokens associated with this specific actor.\n * @returns {Token5e[]}\n */\nexport function getSceneTargets(actor) {\n let targets = canvas.tokens?.controlled.filter(t => t.actor && (!actor || t.actor === actor)) ?? [];\n if ( !targets.length && actor ) targets = actor.getActiveTokens();\n else if ( !targets.length && game.user.character ) targets = game.user.character.getActiveTokens();\n return targets;\n}\n\n/* -------------------------------------------- */\n/* Conversions */\n/* -------------------------------------------- */\n\n/**\n * Convert the provided length to another unit.\n * @param {number} value The length being converted.\n * @param {string} from The initial units.\n * @param {string} to The final units.\n * @param {object} [options={}]\n * @param {boolean} [options.strict=true] Throw an error if either unit isn't found.\n * @returns {number}\n */\nexport function convertLength(value, from, to, { strict=true }={}) {\n const message = unit => `Length unit ${unit} not defined in CONFIG.DND5E.movementUnits`;\n return _convertSystemUnits(value, from, to, CONFIG.DND5E.movementUnits, { message, strict });\n}\n\n/* -------------------------------------------- */\n\n/**\n * Convert the provided time value to another unit. If no final unit is provided, then will convert it to the largest\n * unit that can still represent the value as a whole number.\n * @param {number} value The time being converted.\n * @param {string} from The initial unit as defined in `CONFIG.DND5E.timeUnits`.\n * @param {object} [options={}]\n * @param {boolean} [options.combat=false] Use combat units when auto-selecting units, rather than normal units.\n * @param {boolean} [options.strict=true] Throw an error if from unit isn't found.\n * @param {string} [options.to] The final units, if explicitly provided.\n * @returns {{ value: number, unit: string }}\n */\nexport function convertTime(value, from, { combat=false, strict=true, to }={}) {\n const base = value * (CONFIG.DND5E.timeUnits[from]?.conversion ?? 1);\n if ( !to ) {\n // Find unit with largest conversion value that can still display the value\n const unitOptions = Object.entries(CONFIG.DND5E.timeUnits)\n .reduce((arr, [key, v]) => {\n if ( ((v.combat ?? false) === combat) && ((base % v.conversion === 0) || (base >= v.conversion * 2)) ) {\n arr.push({ key, conversion: v.conversion });\n }\n return arr;\n }, [])\n .sort((lhs, rhs) => rhs.conversion - lhs.conversion);\n to = unitOptions[0]?.key ?? from;\n }\n\n const message = unit => `Time unit ${unit} not defined in CONFIG.DND5E.timeUnits`;\n return { value: _convertSystemUnits(value, from, to, CONFIG.DND5E.timeUnits, { message, strict }), unit: to };\n}\n\n/* -------------------------------------------- */\n\n/**\n * Convert the provided travel speed to another unit.\n * @param {number} value The travel speed being converted.\n * @param {string} from The initial unit.\n * @param {object} options\n * @param {boolean} [options.strict=false] Throw an error if either unit isn't found.\n * @param {string} options.to The final unit.\n * @returns {{ value: number, unit: string }}\n */\nexport function convertTravelSpeed(value, from, { strict=false, to }) {\n const message = unit => `Travel speed unit ${unit} not defined in CONFIG.DND5E.travelUnits`;\n return { value: _convertSystemUnits(value, from, to, CONFIG.DND5E.travelUnits, { message, strict }), unit: to };\n}\n\n/* -------------------------------------------- */\n\n/**\n * Convert the provided weight to another unit.\n * @param {number} value The weight being converted.\n * @param {string} from The initial unit as defined in `CONFIG.DND5E.weightUnits`.\n * @param {string} to The final units.\n * @param {object} [options={}]\n * @param {boolean} [options.strict=true] Throw an error if either unit isn't found.\n * @returns {number} Weight in the specified units.\n */\nexport function convertWeight(value, from, to, { strict=true }={}) {\n const message = unit => `Weight unit ${unit} not defined in CONFIG.DND5E.weightUnits`;\n return _convertSystemUnits(value, from, to, CONFIG.DND5E.weightUnits, { message, strict });\n}\n\n/* -------------------------------------------- */\n\n/**\n * Convert from one unit to another using one of core's built-in unit types.\n * @param {number} value Value to display.\n * @param {string} from The initial unit.\n * @param {string} to The final unit.\n * @param {UnitConfiguration} config Configuration data for the unit.\n * @param {object} options\n * @param {function(string): string} [options.message] Method used to produce the error message if unit not found.\n * @param {boolean} [options.strict] Throw an error if either unit isn't found.\n * @returns {string}\n */\nfunction _convertSystemUnits(value, from, to, config, { message, strict }) {\n if ( from === to ) return value;\n if ( strict && !config[from] ) throw new Error(message(from));\n if ( strict && !config[to] ) throw new Error(message(to));\n return value * (config[from]?.conversion ?? 1) / (config[to]?.conversion ?? 1);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Default units to use depending on system setting.\n * @param {\"length\"|\"travel\"|\"volume\"|\"weight\"} type Type of units to select.\n * @returns {string}\n */\nexport function defaultUnits(type) {\n const settingKey = type === \"travel\" ? \"metricLengthUnits\" : `metric${type.capitalize()}Units`;\n return CONFIG.DND5E.defaultUnits[type]?.[game.settings.get(\"dnd5e\", settingKey) ? \"metric\" : \"imperial\"];\n}\n\n/* -------------------------------------------- */\n/* Validators */\n/* -------------------------------------------- */\n\n/**\n * Ensure the provided string contains only the characters allowed in identifiers.\n * @param {string} identifier\n * @param {object} [options={}]\n * @param {boolean} [options.allowType] Consider an identifier with a single \":\" to be valid. Only the portion after\n * the colon must follow the strict identifier validation.\n * @returns {boolean}\n */\nfunction isValidIdentifier(identifier, { allowType=false }={}) {\n if ( allowType ) {\n const split = identifier.split(\":\");\n if ( split.length > 2 ) return false;\n identifier = split[1];\n }\n return /^([a-z0-9_-]+)$/i.test(identifier);\n}\n\nexport const validators = {\n isValidIdentifier: isValidIdentifier\n};\n\n/* -------------------------------------------- */\n\n/**\n * Determine whether the provided unit is usable within `Intl.NumberFormat`.\n * @param {string} unit\n * @returns {boolean}\n */\nexport function isValidUnit(unit) {\n if ( unit?.includes(\"-per-\") ) return unit.split(\"-per-\").every(u => isValidUnit(u));\n return Intl.supportedValuesOf(\"unit\").includes(unit);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Test if a given string is serialized JSON, and parse it if so.\n * @param {string} raw The raw value.\n * @returns {any} The parsed value, or the original value if it was not serialized JSON.\n */\nexport function parseOrString(raw) {\n try { return JSON.parse(raw); } catch(err) {}\n return raw;\n}\n\n/* -------------------------------------------- */\n/* Handlebars Template Helpers */\n/* -------------------------------------------- */\n\n/**\n * Define a set of template paths to pre-load. Pre-loaded templates are compiled and cached for fast access when\n * rendering. These paths will also be available as Handlebars partials by using the file name\n * (e.g. \"dnd5e.actor-traits\").\n * @returns {Promise}\n */\nexport async function preloadHandlebarsTemplates() {\n const partials = [\n // Shared Partials\n \"systems/dnd5e/templates/shared/active-effects.hbs\",\n \"systems/dnd5e/templates/apps/parts/trait-list.hbs\",\n \"systems/dnd5e/templates/apps/parts/traits-list.hbs\",\n\n // Actor Sheet Partials\n \"systems/dnd5e/templates/actors/parts/actor-classes.hbs\",\n \"systems/dnd5e/templates/actors/parts/actor-trait-pills.hbs\",\n \"systems/dnd5e/templates/actors/parts/actor-traits.hbs\",\n \"systems/dnd5e/templates/actors/parts/actor-features.hbs\",\n \"systems/dnd5e/templates/actors/parts/actor-spellbook.hbs\",\n \"systems/dnd5e/templates/actors/parts/actor-warnings.hbs\",\n \"systems/dnd5e/templates/actors/parts/actor-warnings-dialog.hbs\",\n \"systems/dnd5e/templates/actors/parts/biography-textbox.hbs\",\n \"systems/dnd5e/templates/actors/tabs/character-bastion.hbs\",\n \"systems/dnd5e/templates/actors/tabs/character-biography.hbs\",\n \"systems/dnd5e/templates/actors/tabs/character-details.hbs\",\n \"systems/dnd5e/templates/actors/tabs/creature-special-traits.hbs\",\n \"systems/dnd5e/templates/actors/tabs/npc-biography.hbs\",\n\n // Chat Message Partials\n \"systems/dnd5e/templates/chat/parts/card-activities.hbs\",\n \"systems/dnd5e/templates/chat/parts/card-deltas.hbs\",\n\n // Item Sheet Partials\n \"systems/dnd5e/templates/items/details/details-background.hbs\",\n \"systems/dnd5e/templates/items/details/details-class.hbs\",\n \"systems/dnd5e/templates/items/details/details-consumable.hbs\",\n \"systems/dnd5e/templates/items/details/details-container.hbs\",\n \"systems/dnd5e/templates/items/details/details-equipment.hbs\",\n \"systems/dnd5e/templates/items/details/details-facility.hbs\",\n \"systems/dnd5e/templates/items/details/details-feat.hbs\",\n \"systems/dnd5e/templates/items/details/details-loot.hbs\",\n \"systems/dnd5e/templates/items/details/details-mountable.hbs\",\n \"systems/dnd5e/templates/items/details/details-species.hbs\",\n \"systems/dnd5e/templates/items/details/details-spell.hbs\",\n \"systems/dnd5e/templates/items/details/details-spellcasting.hbs\",\n \"systems/dnd5e/templates/items/details/details-starting-equipment.hbs\",\n \"systems/dnd5e/templates/items/details/details-subclass.hbs\",\n \"systems/dnd5e/templates/items/details/details-tool.hbs\",\n \"systems/dnd5e/templates/items/details/details-weapon.hbs\",\n \"systems/dnd5e/templates/items/parts/item-summary.hbs\",\n \"systems/dnd5e/templates/items/parts/item-tooltip.hbs\",\n \"systems/dnd5e/templates/items/parts/spell-block.hbs\",\n\n // Field Partials\n \"systems/dnd5e/templates/shared/fields/field-activation.hbs\",\n \"systems/dnd5e/templates/shared/fields/field-damage.hbs\",\n \"systems/dnd5e/templates/shared/fields/field-duration.hbs\",\n \"systems/dnd5e/templates/shared/fields/field-range.hbs\",\n \"systems/dnd5e/templates/shared/fields/field-targets.hbs\",\n \"systems/dnd5e/templates/shared/fields/field-uses.hbs\",\n \"systems/dnd5e/templates/shared/fields/fieldlist.hbs\",\n \"systems/dnd5e/templates/shared/fields/formlist.hbs\",\n\n // Journal Partials\n \"systems/dnd5e/templates/journal/parts/journal-legacy-traits.hbs\",\n \"systems/dnd5e/templates/journal/parts/journal-modern-traits.hbs\",\n \"systems/dnd5e/templates/journal/parts/journal-table.hbs\",\n\n // Activity Partials\n \"systems/dnd5e/templates/activity/parts/activity-usage-notes.hbs\",\n\n // Advancement Partials\n \"systems/dnd5e/templates/advancement/parts/advancement-controls.hbs\",\n \"systems/dnd5e/templates/advancement/parts/advancement-spell-config.hbs\"\n ];\n\n const paths = {};\n for ( const path of partials ) {\n paths[path.replace(\".hbs\", \".html\")] = path;\n paths[`dnd5e.${path.split(\"/\").pop().replace(\".hbs\", \"\")}`] = path;\n }\n\n return foundry.applications.handlebars.loadTemplates(paths);\n}\n\n/* -------------------------------------------- */\n\n/**\n * A helper that converts the provided object into a series of `data-` entries.\n * @param {object} object Object to convert into dataset entries.\n * @param {object} options Handlebars options.\n * @returns {string}\n */\nfunction dataset(object, options) {\n const entries = [];\n for ( let [key, value] of Object.entries(object ?? {}) ) {\n if ( value === undefined ) continue;\n key = key.replace(/[A-Z]+(?![a-z])|[A-Z]/g, (a, b) => (b ? \"-\" : \"\") + a.toLowerCase());\n entries.push(`data-${key}=\"${Handlebars.escapeExpression(value)}\"`);\n }\n return new Handlebars.SafeString(entries.join(\" \"));\n}\n\n/* -------------------------------------------- */\n\n/**\n * Create an icon element dynamically based on the provided icon string, supporting FontAwesome class strings\n * or paths to SVG or other image types.\n * @param {string} icon Icon class or path.\n * @param {object} [options={}]\n * @param {string} [options.alt] Alt text for the icon.\n * @param {string} [options.classes] Classes to add to the icon.\n * @returns {HTMLElement|null}\n */\nexport function generateIcon(icon, { alt, classes }={}) {\n let element;\n if ( icon?.startsWith(\"fa\") ) {\n element = document.createElement(\"i\");\n element.className = icon;\n } else if ( icon ) {\n element = document.createElement(icon.endsWith(\".svg\") ? \"dnd5e-icon\" : \"img\");\n element.draggable = false;\n element.src = icon;\n } else {\n return null;\n }\n if ( alt ) element[element.tagName === \"IMG\" ? \"alt\" : \"ariaLabel\"] = alt;\n if ( classes ) element.classList.add(...classes.split(\" \"));\n return element;\n}\n\n/* -------------------------------------------- */\n\n/**\n * A helper to create a set of