} results Results of any award operations.\n */\n static async displayAwardMessages(results) {\n const cls = getDocumentClass(\"ChatMessage\");\n const messages = [];\n for ( const [destination, result] of results ) {\n const entries = [];\n for ( const [key, amount] of Object.entries(result.currency ?? {}) ) {\n const label = CONFIG.DND5E.currencies[key].label;\n entries.push(`\n \n ${formatNumber(amount)} \n \n `);\n }\n if ( result.xp ) entries.push(`\n \n ${formatNumber(result.xp)} ${game.i18n.localize(\"DND5E.ExperiencePoints.Abbreviation\")}\n \n `);\n if ( !entries.length ) continue;\n\n const content = game.i18n.format(\"DND5E.Award.Message\", {\n name: destination.name, award: `${game.i18n.getListFormatter().format(entries)} `\n });\n\n const whisperTargets = game.users.filter(user => destination.testUserPermission(user, \"OWNER\"));\n const whisper = whisperTargets.length !== game.users.size;\n const messageData = {\n content,\n whisper: whisper ? whisperTargets : []\n };\n messages.push(messageData);\n }\n if ( messages.length ) cls.createDocuments(messages);\n }\n\n /* -------------------------------------------- */\n /* Chat Command */\n /* -------------------------------------------- */\n\n /**\n * Regular expression used to match the /award command in chat messages.\n * @type {RegExp}\n */\n static COMMAND_PATTERN = new RegExp(/^(?:)?\\/award(?:\\s|<\\/p>$|$)/i);\n\n /* -------------------------------------------- */\n\n /**\n * Regular expression used to split currency & xp values from their labels.\n * @type {RegExp}\n */\n static VALUE_PATTERN = new RegExp(/^(.+?)(\\D+)$/);\n\n /* -------------------------------------------- */\n\n /**\n * Use the `chatMessage` hook to determine if an award command was typed.\n * @param {string} message Text of the message being posted.\n * @returns {boolean|void} Returns `false` to prevent the message from continuing to parse.\n */\n static chatMessage(message) {\n if ( !this.COMMAND_PATTERN.test(message) ) return;\n this.handleAward(message);\n return false;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Parse the award command and grant an award.\n * @param {string} message Award command typed in chat.\n */\n static async handleAward(message) {\n if ( !game.user.isGM ) {\n ui.notifications.error(\"DND5E.Award.NotGMError\", { localize: true });\n return;\n }\n\n try {\n const { currency, xp, party, each } = this.parseAwardCommand(message);\n\n for ( const [key, formula] of Object.entries(currency) ) {\n const roll = new Roll(formula);\n await roll.evaluate();\n currency[key] = roll.total;\n }\n\n // If the party command is set, a primary party is set, and the award isn't empty, skip the UI\n const primaryParty = game.actors.party;\n if ( party && primaryParty && (xp || filteredKeys(currency).length) ) {\n const destinations = each ? primaryParty.system.playerCharacters : [primaryParty];\n const results = new Map();\n await this.awardCurrency(currency, destinations, { each, results });\n await this.awardXP(xp, destinations, { each, results });\n this.displayAwardMessages(results);\n }\n\n // Otherwise show the UI with defaults\n else {\n const savedDestinations = game.user.getFlag(\"dnd5e\", \"awardDestinations\");\n const app = new Award({ award: { currency, xp, each, savedDestinations } });\n app.render({ force: true });\n }\n } catch(err) {\n ui.notifications.warn(err.message);\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Parse the award command.\n * @param {string} message Award command typed in chat.\n * @returns {{currency: Record, xp: number, party: boolean}}\n */\n static parseAwardCommand(message) {\n const command = message.replace(/^|<\\/p>$/gi, \"\").replace(this.COMMAND_PATTERN, \"\").toLowerCase();\n\n const currency = {};\n let each = false;\n let party = false;\n let xp;\n const unrecognized = [];\n for ( const part of command.split(\" \") ) {\n if ( !part ) continue;\n let [, amount, label] = part.match(this.VALUE_PATTERN) ?? [];\n label = label?.toLowerCase();\n try {\n new Roll(amount);\n if ( label in CONFIG.DND5E.currencies ) currency[label] = amount;\n else if ( label === \"xp\" ) xp = Number(amount);\n else if ( part === \"each\" ) each = true;\n else if ( part === \"party\" ) party = true;\n else throw new Error();\n } catch(err) {\n unrecognized.push(part);\n }\n }\n\n // Display warning about an unrecognized commands\n if ( unrecognized.length ) throw new Error(game.i18n.format(\"DND5E.Award.UnrecognizedWarning\", {\n commands: game.i18n.getListFormatter().format(unrecognized.map(u => `\"${u}\"`))\n }));\n\n return { currency, xp, each, party };\n }\n}\n","import Dialog5e from \"../api/dialog.mjs\";\n\nconst { DiceTerm } = foundry.dice.terms;\n\n/**\n * @import {\n * BasicRollConfigurationDialogOptions, BasicRollDialogConfiguration,\n * BasicRollMessageConfiguration, BasicRollProcessConfiguration\n * } from \"../../dice/_types.mjs\";\n */\n\n/**\n * Dialog for configuring one or more rolls.\n * @extends {Dialog5e}\n *\n * @param {BasicRollProcessConfiguration} [config={}] Initial roll configuration.\n * @param {BasicRollMessageConfiguration} [message={}] Message configuration.\n * @param {BasicRollConfigurationDialogOptions} [options={}] Dialog rendering options.\n */\nexport default class RollConfigurationDialog extends Dialog5e {\n constructor(config={}, message={}, options={}) {\n super(options);\n\n this.#config = config;\n this.#message = message;\n this.#buildRolls(foundry.utils.deepClone(this.#config));\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n static DEFAULT_OPTIONS = {\n classes: [\"roll-configuration\"],\n window: {\n title: \"DND5E.RollConfiguration.Title\",\n icon: \"fa-solid fa-dice\"\n },\n form: {\n handler: RollConfigurationDialog.#handleFormSubmission\n },\n position: {\n width: 400\n },\n buildConfig: null,\n rendering: {\n dice: {\n max: 5,\n denominations: new Set([\"d4\", \"d6\", \"d8\", \"d10\", \"d12\", \"d20\"])\n }\n }\n };\n\n /* -------------------------------------------- */\n\n /** @override */\n static PARTS = {\n formulas: {\n template: \"systems/dnd5e/templates/dice/roll-formulas.hbs\"\n },\n configuration: {\n template: \"systems/dnd5e/templates/dice/roll-configuration.hbs\"\n },\n buttons: {\n template: \"systems/dnd5e/templates/dice/roll-buttons.hbs\"\n }\n };\n\n /* -------------------------------------------- */\n\n /**\n * Roll type to use when constructing the rolls.\n * @type {typeof BasicRoll}\n */\n static get rollType() {\n return CONFIG.Dice.BasicRoll;\n }\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /**\n * Roll configuration.\n * @type {BasicRollProcessConfiguration}\n */\n #config;\n\n get config() {\n return this.#config;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Configuration information for the roll message.\n * @type {BasicRollMessageConfiguration}\n */\n #message;\n\n get message() {\n return this.#message;\n }\n\n /* -------------------------------------------- */\n\n /**\n * The rolls being configured.\n * @type {BasicRoll[]}\n */\n #rolls;\n\n get rolls() {\n return this.#rolls;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Roll type to use when constructing the rolls.\n * @type {typeof BasicRoll}\n */\n get rollType() {\n return this.options.rollType ?? this.constructor.rollType;\n }\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /**\n * Identify DiceTerms in this app's rolls.\n * @returns {{ icon: string, label: string }[]}\n * @protected\n */\n _identifyDiceTerms() {\n let dice = [];\n let shouldDisplay = true;\n\n /**\n * Determine if a given term is displayable.\n * @param {RollTerm} term The term.\n * @returns {boolean|void}\n */\n const identifyTerm = term => {\n if ( !(term instanceof DiceTerm) ) return;\n // If any of the terms have complex components, do not attempt to display only some dice, bail out entirely.\n if ( !Number.isFinite(term.number) || !Number.isFinite(term.faces) ) return shouldDisplay = false;\n // If any of the terms are of an unsupported denomination, do not attempt to display only some dice, bail out\n // entirely.\n if ( !this.options.rendering.dice.denominations.has(term.denomination) ) return shouldDisplay = false;\n for ( let i = 0; i < term.number; i++ ) dice.push({\n icon: `systems/dnd5e/icons/svg/dice/${term.denomination}.svg`,\n label: term.denomination,\n denomination: term.denomination\n });\n };\n\n /**\n * Identify any DiceTerms in the given terms.\n * @param {RollTerm[]} terms The terms.\n */\n const identifyDice = (terms=[]) => {\n for ( const term of terms ) {\n identifyTerm(term);\n if ( \"dice\" in term ) identifyDice(term.dice);\n }\n };\n\n this.rolls.forEach(roll => identifyDice(roll.terms));\n if ( dice.length > this.options.rendering.dice.max ) {\n // Compact dice display.\n const byDenom = dice.reduce((obj, { icon, denomination }) => {\n obj[denomination] ??= { icon, count: 0 };\n obj[denomination].count++;\n return obj;\n }, {});\n dice = Object.entries(byDenom).map(([d, { icon, count }]) => ({ icon, label: `${count}${d}` }));\n if ( dice.length > this.options.rendering.dice.max ) shouldDisplay = false;\n }\n else if ( !dice.length ) shouldDisplay = false;\n return shouldDisplay ? dice : [];\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _preparePartContext(partId, context, options) {\n context = await super._preparePartContext(partId, context, options);\n switch ( partId ) {\n case \"buttons\":\n return this._prepareButtonsContext(context, options);\n case \"configuration\":\n return this._prepareConfigurationContext(context, options);\n case \"formulas\":\n return this._prepareFormulasContext(context, options);\n default:\n return context;\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare the context for the buttons.\n * @param {ApplicationRenderContext} context Shared context provided by _prepareContext.\n * @param {HandlebarsRenderOptions} options Options which configure application rendering behavior.\n * @returns {Promise}\n * @protected\n */\n async _prepareButtonsContext(context, options) {\n context.buttons = {\n roll: {\n default: true,\n icon: ' ',\n label: game.i18n.localize(\"DND5E.Roll\")\n }\n };\n return context;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare the context for the roll configuration section.\n * @param {ApplicationRenderContext} context Shared context provided by _prepareContext.\n * @param {HandlebarsRenderOptions} options Options which configure application rendering behavior.\n * @returns {Promise}\n * @protected\n */\n async _prepareConfigurationContext(context, options) {\n context.fields = [{\n field: new foundry.data.fields.StringField({\n label: game.i18n.localize(\"DND5E.RollMode\"), blank: false, required: true\n }),\n name: \"rollMode\",\n value: this.message.rollMode ?? this.options.default?.rollMode ?? CONFIG.Dice.BasicRoll.getMessageMode(),\n options: Object.entries(game.release.generation < 14 ? CONFIG.Dice.rollModes : CONFIG.ChatMessage.modes)\n .filter(([k]) => k !== \"ic\")\n .map(([value, l]) => ({ value, label: game.i18n.localize(l.label) }))\n }];\n return context;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare the context for the formulas list.\n * @param {ApplicationRenderContext} context Shared context provided by _prepareContext.\n * @param {HandlebarsRenderOptions} options Options which configure application rendering behavior.\n * @returns {Promise}\n * @protected\n */\n async _prepareFormulasContext(context, options) {\n context.rolls = this.rolls.map(roll => ({ roll }));\n context.dice = this._identifyDiceTerms() || [];\n return context;\n }\n\n /* -------------------------------------------- */\n /* Roll Handling */\n /* -------------------------------------------- */\n\n /**\n * Build a roll from the provided configuration objects.\n * @param {BasicRollProcessConfiguration} config Roll configuration data.\n * @param {FormDataExtended} [formData] Any data entered into the rolling prompt.\n */\n #buildRolls(config, formData) {\n const RollType = this.rollType;\n this.#rolls = config.rolls?.map((config, index) =>\n RollType.fromConfig(this.#buildConfig(config, formData, index), this.config)\n ) ?? [];\n }\n\n /* -------------------------------------------- */\n\n /**\n * Call the necessary hooks and config building methods before roll is fully built.\n * @param {BasicRollConfiguration} config Roll configuration data.\n * @param {FormDataExtended} [formData] Any data entered into the rolling prompt.\n * @param {number} index Index of the roll within all rolls being prepared.\n * @returns {BasicRollConfiguration}\n */\n #buildConfig(config, formData, index) {\n config = foundry.utils.mergeObject({ parts: [], data: {}, options: {} }, config);\n\n /**\n * A hook event that fires when a roll config is built using the roll prompt. Multiple hooks may be called depending\n * on the rolling method (e.g. `dnd5e.buildSkillRollConfig`, `dnd5e.buildAbilityCheckRollConfig`,\n * `dnd5e.buildRollConfig`).\n * @function dnd5e.buildRollConfig\n * @memberof hookEvents\n * @param {RollConfigurationDialog} app Roll configuration dialog.\n * @param {BasicRollConfiguration} config Roll configuration data.\n * @param {FormDataExtended} [formData] Any data entered into the rolling prompt.\n * @param {number} index Index of the roll within all rolls being prepared.\n */\n for ( const hookName of this.#config.hookNames ?? [\"\"] ) {\n Hooks.callAll(`dnd5e.build${hookName.capitalize()}RollConfig`, this, config, formData, index);\n }\n\n config = this._buildConfig(config, formData, index);\n this.options.buildConfig?.(this.config, config, formData, index);\n\n /**\n * A hook event that fires after a roll config has been built using the roll prompt. Multiple hooks may be called\n * depending on the rolling method (e.g. `dnd5e.postBuildSkillRollConfig`, `dnd5e.postBuildAbilityCheckRollConfig`,\n * `dnd5e.postBuildRollConfig`).\n * @function dnd5e.postBuildRollConfig\n * @memberof hookEvents\n * @param {BasicRollProcessConfiguration} process Full process configuration data.\n * @param {BasicRollConfiguration} config Roll configuration data.\n * @param {number} index Index of the roll within all rolls being prepared.\n * @param {object} [options]\n * @param {RollConfigurationDialog} [options.app] Roll configuration dialog.\n * @param {FormDataExtended} [options.formData] Any data entered into the rolling prompt.\n */\n for ( const hookName of this.#config.hookNames ?? [\"\"] ) {\n Hooks.callAll(`dnd5e.postBuild${hookName.capitalize()}RollConfig`, this.config, config, index, {\n app: this, formData\n });\n }\n\n return config;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare individual configuration object before building a roll.\n * @param {BasicRollConfiguration} config Roll configuration data.\n * @param {FormDataExtended} [formData] Any data entered into the rolling prompt.\n * @param {number} index Index of the roll within all rolls being prepared.\n * @returns {BasicRollConfiguration}\n * @protected\n */\n _buildConfig(config, formData, index) {\n const situational = formData?.get(`roll.${index}.situational`);\n if ( situational && (config.situational !== false) ) {\n config.parts.push(\"@situational\");\n config.data.situational = situational;\n } else {\n config.parts.findSplice(v => v === \"@situational\");\n }\n return config;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Make any final modifications to rolls based on the button clicked.\n * @param {string} action Action on the button clicked.\n * @returns {BasicRoll[]}\n * @protected\n */\n _finalizeRolls(action) {\n return this.rolls;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Rebuild rolls based on an updated config and re-render the dialog.\n */\n rebuild() {\n this._onChangeForm(this.options.form, new Event(\"change\"));\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /**\n * Handle submission of the dialog using the form buttons.\n * @this {RollConfigurationDialog}\n * @param {Event|SubmitEvent} event The form submission event.\n * @param {HTMLFormElement} form The submitted form.\n * @param {FormDataExtended} formData Data from the dialog.\n */\n static async #handleFormSubmission(event, form, formData) {\n if ( formData.has(\"rollMode\") ) this.message.rollMode = formData.get(\"rollMode\");\n this.#rolls = this._finalizeRolls(event.submitter?.dataset?.action);\n await this.close({ dnd5e: { submitted: true } });\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _onChangeForm(formConfig, event) {\n super._onChangeForm(formConfig, event);\n\n const formData = new foundry.applications.ux.FormDataExtended(this.form);\n if ( formData.has(\"rollMode\") ) this.message.rollMode = formData.get(\"rollMode\");\n this.#buildRolls(foundry.utils.deepClone(this.#config), formData);\n this.render({ parts: [\"formulas\"] });\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n _onClose(options={}) {\n if ( !options.dnd5e?.submitted ) this.#rolls = [];\n }\n\n /* -------------------------------------------- */\n /* Factory Methods */\n /* -------------------------------------------- */\n\n /**\n * A helper to handle displaying and responding to the dialog.\n * @param {BasicRollProcessConfiguration} [config] Initial roll configuration.\n * @param {BasicRollDialogConfiguration} [dialog] Dialog configuration options.\n * @param {BasicRollMessageConfiguration} [message] Message configuration.\n * @returns {Promise}\n */\n static async configure(config={}, dialog={}, message={}) {\n return new Promise(resolve => {\n const app = new this(config, message, dialog.options);\n app.addEventListener(\"close\", () => resolve(app.rolls), { once: true });\n if ( dialog.sheet?._renderChild ) dialog.sheet._renderChild(app);\n else app.render({ force: true });\n });\n }\n}\n","import RollConfigurationDialog from \"./roll-configuration-dialog.mjs\";\n\n/**\n * @import {\n * BasicRollConfigurationDialogOptions, BasicRollMessageConfiguration, D20RollProcessConfiguration\n * } from \"../../dice/_types.mjs\";\n */\n\n/**\n * Dialog for configuring d20 rolls.\n * @extends {RollConfigurationDialog}\n *\n * @param {D20RollProcessConfiguration} [config={}] Initial roll configuration.\n * @param {BasicRollMessageConfiguration} [message={}] Message configuration.\n * @param {BasicRollConfigurationDialogOptions} [options={}] Dialog rendering options.\n */\nexport default class D20RollConfigurationDialog extends RollConfigurationDialog {\n\n /** @override */\n static get rollType() {\n return CONFIG.Dice.D20Roll;\n }\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @override */\n async _prepareButtonsContext(context, options) {\n let defaultButton = this.options.defaultButton;\n if ( !defaultButton ) {\n let advantage = false;\n let disadvantage = false;\n for ( const roll of this.config.rolls ) {\n if ( !roll.options ) continue;\n if ( roll.options.advantageMode === CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE ) advantage = true;\n else if ( roll.options.advantageMode === CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE ) disadvantage = true;\n else if ( roll.options.advantage && !roll.options.disadvantage ) advantage = true;\n else if ( !roll.options.advantage && roll.options.disadvantage ) disadvantage = true;\n }\n if ( advantage && !disadvantage ) defaultButton = \"advantage\";\n else if ( !advantage && disadvantage ) defaultButton = \"disadvantage\";\n }\n context.buttons = {\n advantage: {\n default: defaultButton === \"advantage\",\n label: game.i18n.localize(\"DND5E.Advantage\")\n },\n normal: {\n default: ![\"advantage\", \"disadvantage\"].includes(defaultButton),\n label: game.i18n.localize(\"DND5E.Normal\")\n },\n disadvantage: {\n default: defaultButton === \"disadvantage\",\n label: game.i18n.localize(\"DND5E.Disadvantage\")\n }\n };\n return context;\n }\n\n /* -------------------------------------------- */\n /* Roll Handling */\n /* -------------------------------------------- */\n\n /** @override */\n _finalizeRolls(action) {\n let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL;\n if ( action === \"advantage\" ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE;\n else if ( action === \"disadvantage\" ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE;\n return this.rolls.map(roll => {\n roll.options.advantageMode = advantageMode;\n roll.configureModifiers();\n return roll;\n });\n }\n}\n","import D20RollConfigurationDialog from \"./d20-configuration-dialog.mjs\";\n\n/**\n * @import { AttackRollConfigurationDialogOptions } from \"../../dice/_types.mjs\";\n */\n\n/**\n * Extended roll configuration dialog that allows selecting attack mode, ammunition, and weapon mastery.\n * @extends D20RollConfigurationDialog\n */\nexport default class AttackRollConfigurationDialog extends D20RollConfigurationDialog {\n /** @override */\n static DEFAULT_OPTIONS = {\n ammunitionOptions: [],\n attackModeOptions: [],\n masteryOptions: []\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareConfigurationContext(context, options) {\n context = await super._prepareConfigurationContext(context, options);\n const optionsFields = [\n { key: \"attackMode\", label: \"DND5E.ATTACK.Mode.Label\", options: this.options.attackModeOptions },\n { key: \"ammunition\", label: \"DND5E.CONSUMABLE.Type.Ammunition.Label\", options: this.options.ammunitionOptions },\n { key: \"mastery\", label: \"DND5E.WEAPON.Mastery.Label\", options: this.options.masteryOptions }\n ];\n context.fields = [\n ...optionsFields.map(({ key, label, options }) => options.length ? {\n field: new foundry.data.fields.StringField({ label: game.i18n.localize(label), blank: false, required: true }),\n name: key,\n options,\n value: this.config[key]\n } : null).filter(_ => _),\n ...context.fields\n ];\n return context;\n }\n}\n","/**\n * Attempt to create a macro from the dropped data. Will use an existing macro if one exists.\n * @param {object} dropData The dropped data\n * @param {number} slot The hotbar slot to use\n * @returns {Promise}\n */\nexport async function create5eMacro(dropData, slot) {\n const macroData = { type: \"script\", scope: \"actor\" };\n switch ( dropData.type ) {\n case \"Activity\":\n const activity = await fromUuid(dropData.uuid);\n if ( !activity ) {\n ui.notifications.warn(\"MACRO.5eUnownedWarn\", { localize: true });\n return null;\n }\n foundry.utils.mergeObject(macroData, {\n name: `${activity.item.name}: ${activity.name}`,\n img: activity.img,\n command: `dnd5e.documents.macro.rollItem(\"${activity.item._source.name}\", { activityName: \"${\n activity._source.name}\", event });`,\n flags: { \"dnd5e.itemMacro\": true }\n });\n break;\n case \"Item\":\n const itemData = await Item.implementation.fromDropData(dropData);\n if ( !itemData ) {\n ui.notifications.warn(\"MACRO.5eUnownedWarn\", { localize: true });\n return null;\n }\n foundry.utils.mergeObject(macroData, {\n name: itemData.name,\n img: itemData.img,\n command: `dnd5e.documents.macro.rollItem(\"${itemData._source.name}\", { event })`,\n flags: { \"dnd5e.itemMacro\": true }\n });\n break;\n case \"ActiveEffect\":\n const effectData = await ActiveEffect.implementation.fromDropData(dropData);\n if ( !effectData ) {\n ui.notifications.warn(\"MACRO.5eUnownedWarn\", { localize: true });\n return null;\n }\n foundry.utils.mergeObject(macroData, {\n name: effectData.name,\n img: effectData.img,\n command: `dnd5e.documents.macro.toggleEffect(\"${effectData.name}\")`,\n flags: { \"dnd5e.effectMacro\": true }\n });\n break;\n default:\n return;\n }\n\n // Assign the macro to the hotbar\n const macro = game.macros.find(m => {\n return (m.name === macroData.name) && (m.command === macroData.command) && m.isAuthor;\n }) || await Macro.create(macroData);\n game.user.assignHotbarMacro(macro, slot);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Find a document of the specified name and type on an assigned or selected actor.\n * @param {string} name Document name to locate.\n * @param {string} documentType Type of embedded document (e.g. \"Item\" or \"ActiveEffect\").\n * @returns {Document} Document if found, otherwise nothing.\n */\nfunction getMacroTarget(name, documentType) {\n let actor;\n const speaker = ChatMessage.getSpeaker();\n if ( speaker.token ) actor = game.actors.tokens[speaker.token];\n actor ??= game.actors.get(speaker.actor);\n if ( !actor ) {\n ui.notifications.warn(\"MACRO.5eNoActorSelected\", {localize: true});\n return null;\n }\n\n const collection = (documentType === \"Item\") ? actor.items : Array.from(actor.allApplicableEffects());\n\n // Find item in collection\n const documents = collection.filter(i => i._source.name === name);\n const type = game.i18n.localize(`DOCUMENT.${documentType}`);\n if ( documents.length === 0 ) {\n ui.notifications.warn(game.i18n.format(\"MACRO.5eMissingTargetWarn\", { actor: actor.name, type, name }));\n return null;\n }\n if ( documents.length > 1 ) {\n ui.notifications.warn(game.i18n.format(\"MACRO.5eMultipleTargetsWarn\", { actor: actor.name, type, name }));\n }\n return documents[0];\n}\n\n/* -------------------------------------------- */\n\n/**\n * Trigger an item to be used when a macro is clicked.\n * @param {string} itemName Name of the item on the selected actor to trigger.\n * @param {object} [options={}]\n * @param {string} [options.activityName] Name of a specific activity on the item to trigger.\n * @param {Event} [options.event] The triggering event.\n * @returns {Promise} Usage result.\n */\nexport function rollItem(itemName, { activityName, event }={}) {\n let target = getMacroTarget(itemName, \"Item\");\n if ( activityName ) target = target?.system.activities?.getName(activityName);\n return target?.use({ event, legacy: false });\n}\n\n/* -------------------------------------------- */\n\n/**\n * Toggle an effect on and off when a macro is clicked.\n * @param {string} effectName Name of the effect to be toggled.\n * @returns {Promise} The effect after it has been toggled.\n */\nexport function toggleEffect(effectName) {\n const effect = getMacroTarget(effectName, \"ActiveEffect\");\n return effect?.update({ disabled: !effect.disabled });\n}\n","import { formatNumber, getSceneTargets, getTargetDescriptors, simplifyBonus } from \"./utils.mjs\";\nimport Award from \"./applications/award.mjs\";\nimport AttackRollConfigurationDialog from \"./applications/dice/attack-configuration-dialog.mjs\";\nimport simplifyRollFormula from \"./dice/simplify-roll-formula.mjs\";\nimport * as Trait from \"./documents/actor/trait.mjs\";\nimport { rollItem } from \"./documents/macro.mjs\";\n\nconst slugify = value => value?.slugify().replaceAll(\"-\", \"\").replaceAll(\"(\", \"\").replaceAll(\")\", \"\");\n\n/**\n * Set up custom text enrichers.\n */\nexport function registerCustomEnrichers() {\n const stringNames = [\n \"attack\", \"award\", \"check\", \"concentration\", \"damage\", \"heal\", \"healing\", \"item\", \"save\", \"skill\", \"tool\"\n ];\n CONFIG.TextEditor.enrichers.push({\n id: \"dnd5e-enricher\",\n pattern: new RegExp(`\\\\[\\\\[/(?${stringNames.join(\"|\")})(? .*?)?]](?!])(?:{(?[^}]+)})?`, \"gi\"),\n enricher: enrichString,\n onRender: onRenderEnricher\n },\n {\n id: \"dnd5e-lookup\",\n pattern: /\\[\\[(?language|lookup) (?[^\\]]+)]](?:{(?[^}]+)})?/gi,\n enricher: enrichString\n },\n {\n id: \"dnd5e-reference\",\n pattern: /&(?Reference)\\[(?[^\\]]+)](?:{(?[^}]+)})?/gi,\n enricher: enrichString,\n onRender: onRenderEnricher\n });\n}\n\n/* -------------------------------------------- */\n\n/**\n * Parse the enriched string and provide the appropriate content.\n * @param {RegExpMatchArray} match The regular expression match result.\n * @param {EnrichmentOptions} options Options provided to customize text enrichment.\n * @returns {Promise} An HTML element to insert in place of the matched text or null to\n * indicate that no replacement should be made.\n */\nexport async function enrichString(match, options) {\n let { type, config, label } = match.groups;\n config = parseConfig(config, { multiple: [\"damage\", \"heal\", \"healing\"].includes(type) });\n config._input = match[0];\n config._rules = getRulesVersion(config, options);\n delete config.rules;\n switch ( type.toLowerCase() ) {\n case \"attack\": return enrichAttack(config, label, options);\n case \"award\": return enrichAward(config, label, options);\n case \"heal\":\n case \"healing\": config._isHealing = true;\n case \"damage\": return enrichDamage(config, label, options);\n case \"check\":\n case \"skill\":\n case \"tool\": return enrichCheck(config, label, options);\n case \"language\": return enrichLanguage(config, label, options);\n case \"lookup\": return enrichLookup(config, label, options);\n case \"concentration\": config._isConcentration = true;\n case \"save\": return enrichSave(config, label, options);\n case \"item\": return enrichItem(config, label, options);\n case \"reference\": return enrichReference(config, label, options);\n }\n return null;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Parse a roll string into a configuration object.\n * @param {string} match Matched configuration string.\n * @param {object} [options={}]\n * @param {boolean} [options.multiple=false] Support splitting configuration by \"&\" into multiple sub-configurations.\n * If set to `true` then an array of configs will be returned.\n * @returns {object|object[]}\n */\nexport function parseConfig(match=\"\", { multiple=false }={}) {\n if ( multiple ) return match.split(\"&\").map(s => parseConfig(s));\n const config = { _config: match, values: [] };\n for ( const part of match.match(/(?:[^\\s\"]+|\"[^\"]*\")+/g) ?? [] ) {\n if ( !part ) continue;\n const [key, value] = part.split(\"=\");\n const valueLower = value?.toLowerCase();\n if ( value === undefined ) config.values.push(key.replace(/(^\"|\"$)/g, \"\"));\n else if ( [\"true\", \"false\"].includes(valueLower) ) config[key] = valueLower === \"true\";\n else if ( Number.isNumeric(value) ) config[key] = Number(value);\n else config[key] = value.replace(/(^\"|\"$)/g, \"\");\n }\n return config;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Determine the appropriate rules version based on the config override, provided item's parent's rule version,\n * provided document's rule version, or the system setting (in that order).\n * @param {object} [config={}] Config object for the enrichment.\n * @param {EnrichmentOptions} [options={}] Options provided to customize text enrichment.\n * @returns {\"2014\"|\"2024\"}\n */\nexport function getRulesVersion(config={}, options={}) {\n if ( Number.isNumeric(config.rules) ) return String(config.rules);\n return options.relativeTo?.parent?.system?.source?.rules\n || options.relativeTo?.system?.source?.rules\n || (dnd5e.settings.rulesVersion === \"modern\" ? \"2024\" : \"2014\");\n}\n\n/* -------------------------------------------- */\n/* Attack Enricher */\n/* -------------------------------------------- */\n\n/**\n * Enrich an attack link using a pre-set to hit value.\n * @param {object} config Configuration data.\n * @param {string} [label] Optional label to replace default text.\n * @param {EnrichmentOptions} options Options provided to customize text enrichment.\n * @returns {HTMLElement|null} An HTML link if the attack could be built, otherwise null.\n *\n * @example Create an attack link using a fixed to hit:\n * ```[[/attack +5]]``` or ```[[/attack formula=5]]```\n * becomes\n * ```html\n * \n * +5\n * \n * ```\n *\n * @example Create an attack link using a specific attack mode:\n * ```[[/attack +5]]``` or ```[[/attack formula=5 attackMode=thrown]]```\n * becomes\n * ```html\n * \n * +5\n * \n * ```\n *\n * @example Link an enricher to an attack activity, either explicitly or automatically:\n * ```[[/attack activity=RLQlsLo5InKHZadn]]``` or ```[[/attack]]```\n * becomes\n * ```html\n * \n * +8\n * \n * ```\n *\n * @example Display the full attack section:\n * ```[[/attack format=extended]]``` or ```[[/attack extended]]```\n * becomes\n * ```html\n * \n * Melee Attack Roll :\n * \n * +16 \n * , reach 15 ft\n * \n * ```\n */\nexport async function enrichAttack(config, label, options) {\n if ( config.activity && config.formula ) {\n console.warn(`Activity ID and formula found while enriching ${config._input}, only one is supported.`);\n return null;\n }\n\n const formulaParts = [];\n if ( config.formula ) formulaParts.push(config.formula);\n for ( const value of config.values ) {\n if ( value in CONFIG.DND5E.attackModes ) config.attackMode = value;\n else if ( value === \"extended\" ) config.format = \"extended\";\n else formulaParts.push(value);\n }\n config.formula = Roll.defaultImplementation.replaceFormulaData(\n formulaParts.join(\" \"),\n options.rollData ?? options.relativeTo?.getRollData?.() ?? {}\n );\n\n const activity = config.activity ? options.relativeTo?.system?.activities?.get(config.activity)\n : !config.formula ? options.relativeTo?.system?.activities?.getByType(\"attack\")[0] : null;\n\n if ( activity ) {\n if ( activity.type !== \"attack\" ) {\n console.warn(`Attack enricher linked to non-attack activity when enriching ${config._input}`);\n return null;\n }\n\n config.activityUuid = activity.uuid;\n const attackConfig = activity.getAttackData({ attackMode: config.attackMode });\n config.formula = simplifyRollFormula(\n Roll.defaultImplementation.replaceFormulaData(attackConfig.parts.join(\" + \"), attackConfig.data)\n );\n if ( attackConfig.data.scaling ) config.scaling ??= String(attackConfig.data.scaling.increase);\n delete config.activity;\n }\n\n if ( !config.activityUuid && !config.formula ) {\n console.warn(`No formula or linked activity found while enriching ${config._input}.`);\n return null;\n }\n\n config.type = \"attack\";\n if ( label ) return createRollLink(label, config, { classes: \"roll-link-group roll-link\" });\n\n let displayFormula = simplifyRollFormula(config.formula)?.trim() || \"+0\";\n if ( !displayFormula.startsWith(\"+\") && !displayFormula.startsWith(\"-\") ) displayFormula = `+${displayFormula}`;\n\n const span = document.createElement(\"span\");\n span.className = \"roll-link-group\";\n _addDataset(span, config);\n span.innerHTML = game.i18n.format(`EDITOR.DND5E.Inline.Attack${config._rules === \"2014\" ? \"Long\" : \"Short\"}`, {\n formula: createRollLink(displayFormula).outerHTML\n });\n\n if ( config.format === \"extended\" ) {\n const type = game.i18n.format(`DND5E.ATTACK.Formatted.${config._rules}`, {\n type: game.i18n.getListFormatter({ type: \"disjunction\" }).format(\n Array.from(activity?.validAttackTypes ?? []).map(t => CONFIG.DND5E.attackTypes[t]?.label)\n ),\n classification: CONFIG.DND5E.attackClassifications[activity?.attack.type.classification]?.label ?? \"\"\n }).trim();\n const parts = [span.outerHTML, activity?.getRangeLabel(config.attackMode)];\n if ( config._rules === \"2014\" ) parts.push(activity?.target?.affects.labels?.statblock);\n\n const full = document.createElement(\"span\");\n full.className = \"attack-extended\";\n full.innerHTML = game.i18n.format(\"EDITOR.DND5E.Inline.AttackExtended\", {\n type, parts: game.i18n.getListFormatter({ type: \"unit\" }).format(parts.filter(_ => _))\n });\n return full;\n }\n\n return span;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Perform an attack roll.\n * @param {object} config Configuration data for the roll.\n * @param {Event} [event] The click event triggering the action.\n * @returns {Promise|void}\n */\nasync function rollAttack(config, event) {\n const { activityUuid, attackMode, formula, scaling } = config;\n\n if ( activityUuid ) {\n const activity = await _fetchActivity(activityUuid, Number(scaling ?? 0));\n if ( activity ) return activity.rollAttack({ attackMode, event });\n }\n\n const targets = getTargetDescriptors();\n const rollConfig = {\n attackMode, event,\n hookNames: [\"attack\", \"d20Test\"],\n rolls: [{\n parts: [formula.replace(/^\\s*\\+\\s*/, \"\")],\n options: {\n target: targets.length === 1 ? targets[0].ac : undefined\n }\n }]\n };\n\n const dialogConfig = { applicationClass: AttackRollConfigurationDialog };\n\n const messageConfig = {\n data: {\n flags: {\n dnd5e: {\n messageType: \"roll\",\n roll: { type: \"attack\" }\n }\n },\n flavor: game.i18n.localize(\"DND5E.AttackRoll\"),\n speaker: ChatMessage.implementation.getSpeaker()\n }\n };\n\n const rolls = await CONFIG.Dice.D20Roll.build(rollConfig, dialogConfig, messageConfig);\n if ( rolls?.length ) {\n Hooks.callAll(\"dnd5e.rollAttack\", rolls, { subject: null, ammoUpdate: null });\n Hooks.callAll(\"dnd5e.rollAttackV2\", rolls, { subject: null, ammoUpdate: null });\n Hooks.callAll(\"dnd5e.postRollAttack\", rolls, { subject: null });\n }\n}\n\n/* -------------------------------------------- */\n/* Award Enricher */\n/* -------------------------------------------- */\n\n/**\n * Enrich an award block displaying amounts for each part granted with a GM-control for awarding to the party.\n * @param {object} config Configuration data.\n * @param {string} [label] Optional label to replace default text.\n * @param {EnrichmentOptions} options Options provided to customize text enrichment.\n * @returns {HTMLElement|null} An HTML link if the check could be built, otherwise null.\n */\nexport async function enrichAward(config, label, options) {\n const command = config._config;\n let parsed;\n try {\n parsed = Award.parseAwardCommand(command);\n } catch(err) {\n console.warn(err.message);\n return null;\n }\n\n const block = document.createElement(\"span\");\n block.classList.add(\"award-block\", \"dnd5e2\");\n block.dataset.awardCommand = command;\n\n const entries = [];\n for ( let [key, amount] of Object.entries(parsed.currency) ) {\n const label = CONFIG.DND5E.currencies[key].label;\n amount = Number.isNumeric(amount) ? formatNumber(amount) : amount;\n entries.push(`\n \n ${amount} \n \n `);\n }\n if ( parsed.xp ) entries.push(`\n \n ${formatNumber(parsed.xp)} ${game.i18n.localize(\"DND5E.ExperiencePoints.Abbreviation\")}\n \n `);\n\n let award = game.i18n.getListFormatter({ type: \"unit\" }).format(entries);\n if ( parsed.each ) award = game.i18n.format(\"EDITOR.DND5E.Inline.AwardEach\", { award });\n\n block.innerHTML += `\n ${award}\n \n ${label ?? game.i18n.localize(\"DND5E.Award.Action\")}\n \n `;\n\n return block;\n}\n\n/* -------------------------------------------- */\n/* Check & Save Enrichers */\n/* -------------------------------------------- */\n\n/**\n * Enrich an ability check link to perform a specific ability or skill check. If an ability is provided\n * along with a skill, then the skill check will always use the provided ability. Otherwise it will use\n * the character's default ability for that skill.\n * @param {object} config Configuration data.\n * @param {string} [label] Optional label to replace default text.\n * @param {EnrichmentOptions} options Options provided to customize text enrichment.\n * @returns {HTMLElement|null} An HTML link if the check could be built, otherwise null.\n *\n * @example Create a dexterity check:\n * ```[[/check ability=dex]]```\n * becomes\n * ```html\n * \n * Dexterity\n * \n * ```\n *\n * @example Create an acrobatics check with a DC and default ability:\n * ```[[/check skill=acr dc=20]]```\n * becomes\n * ```html\n * \n * DC 20 Dexterity (Acrobatics)\n * \n * ```\n *\n * @example Create an acrobatics check using strength:\n * ```[[/check ability=str skill=acr]]```\n * becomes\n * ```html\n * \n * Strength (Acrobatics)\n * \n * ```\n *\n * @example Create a tool check:\n * ```[[/check tool=thief ability=int]]```\n * becomes\n * ```html\n * \n * Intelligence (Thieves' Tools)\n * \n * ```\n *\n * @example Create a skill check with a tool (when using the Modern rules):\n * ```[[/check slt thief]]```\n * ```[[/check skill=slt tool=thief]]```\n * becomes\n * ```html\n * \n * Dexterity (Sleight of Hand)\n * check using Thieves' Tools\n * ```\n *\n * @example Formulas used for DCs will be resolved using data provided to the description (not the roller):\n * ```[[/check ability=cha dc=@abilities.int.dc]]```\n * becomes\n * ```html\n * \n * DC 15 Charisma\n * \n * ```\n *\n * @example Use multiple skills in a check using default abilities:\n * ```[[/check skill=acr/ath dc=15]]```\n * ```[[/check acrobatics athletics 15]]```\n * becomes\n * ```html\n * \n * DC 15\n * \n * Dexterity (Acrobatics)\n * or\n * \n * Strength (Athletics)\n * \n * \n * \n * ```\n *\n * @example Use multiple skills with a fixed ability:\n * ```[[/check ability=str skill=dec/per dc=15]]```\n * ```[[/check strength deception persuasion 15]]```\n * becomes\n * ```html\n * \n * DC 15 Strength\n * ( Deception or\n * Persuasion )\n * \n * \n * ```\n *\n * @example Link an enricher to an check activity, either explicitly or automatically\n * ```[[/check activity=RLQlsLo5InKHZadn]]``` or ```[[/check]]```\n * becomes\n * ```html\n * \n * DC 20 Dexterity \n * \n * \n * ```\n */\nexport async function enrichCheck(config, label, options) {\n config.skill = config.skill?.replaceAll(\"/\", \"|\").split(\"|\") ?? [];\n config.tool = config.tool?.replaceAll(\"/\", \"|\").split(\"|\") ?? [];\n for ( let value of config.values ) {\n const slug = foundry.utils.getType(value) === \"string\" ? slugify(value) : value;\n if ( slug in CONFIG.DND5E.enrichmentLookup.abilities ) config.ability = slug;\n else if ( slug in CONFIG.DND5E.enrichmentLookup.skills ) config.skill.push(slug);\n else if ( slug in CONFIG.DND5E.enrichmentLookup.tools ) config.tool.push(slug);\n else if ( Number.isNumeric(value) ) config.dc = Number(value);\n else config[value] = true;\n }\n delete config.values;\n\n const groups = new Map();\n let invalid = false;\n\n const anything = config.ability || config.skill.length || config.tool.length;\n const activity = config.activity ? options.relativeTo?.system?.activities?.get(config.activity)\n : !anything ? options.relativeTo?.system?.activities?.getByType(\"check\")[0] : null;\n\n if ( activity ) {\n if ( activity.type !== \"check\" ) {\n console.warn(`Check enricher linked to non-check activity when enriching ${config._input}.`);\n return null;\n }\n\n if ( activity.check.ability ) config.ability = activity.check.ability;\n config.activityUuid = activity.uuid;\n config.dc = activity.check.dc.value;\n config.skill = [];\n config.tool = [];\n for ( const associated of activity.check.associated ) {\n if ( associated in CONFIG.DND5E.skills ) config.skill.push(associated);\n else if ( associated in CONFIG.DND5E.tools ) config.tool.push(associated);\n }\n delete config.activity;\n }\n\n // TODO: Support \"spellcasting\" ability\n let abilityConfig = CONFIG.DND5E.enrichmentLookup.abilities[slugify(config.ability)];\n if ( config.ability && !abilityConfig ) {\n console.warn(`Ability \"${config.ability}\" not found while enriching ${config._input}.`);\n invalid = true;\n } else if ( abilityConfig?.key ) config.ability = abilityConfig.key;\n\n for ( let [index, skill] of config.skill.entries() ) {\n const skillConfig = CONFIG.DND5E.enrichmentLookup.skills[slugify(skill)];\n if ( skillConfig ) {\n if ( skillConfig.key ) skill = config.skill[index] = skillConfig.key;\n const ability = config.ability || skillConfig.ability;\n if ( !groups.has(ability) ) groups.set(ability, []);\n groups.get(ability).push({ key: skill, type: \"skill\", label: skillConfig.label });\n } else {\n console.warn(`Skill \"${skill}\" not found while enriching ${config._input}.`);\n invalid = true;\n }\n }\n\n let usingTool;\n for ( const tool of config.tool ) {\n const toolConfig = CONFIG.DND5E.tools[slugify(tool)];\n const toolUUID = CONFIG.DND5E.enrichmentLookup.tools[slugify(tool)];\n const toolIndex = toolUUID?.id ? Trait.getBaseItem(toolUUID.id, { indexOnly: true }) : null;\n const toolLabel = toolIndex?.name ?? toolUUID?.label;\n if ( toolLabel ) {\n const ability = config.ability || toolConfig?.ability;\n if ( config.skill.length && (config.tool.length === 1) && (config._rules === \"2024\") ) {\n usingTool = { key: tool, label: toolLabel };\n } else if ( ability ) {\n if ( !groups.has(ability) ) groups.set(ability, []);\n groups.get(ability).push({ key: tool, type: \"tool\", label: toolLabel });\n } else {\n console.warn(`Tool \"${tool}\" found without specified or default ability while enriching ${config._input}.`);\n invalid = true;\n }\n } else {\n console.warn(`Tool \"${tool}\" not found while enriching ${config._input}.`);\n invalid = true;\n }\n }\n\n if ( !abilityConfig && !groups.size ) {\n console.warn(`No ability, skill, tool, or linked activity provided while enriching ${config._input}.`);\n invalid = true;\n }\n\n const complex = (config.skill.length + config.tool.length) > 1;\n if ( config.passive && complex ) {\n console.warn(`Multiple skills or tools and passive flag found while enriching ${config._input}, which aren't supported together.`);\n invalid = true;\n }\n if ( label && complex ) {\n console.warn(`Multiple skills or tools and a custom label found while enriching ${config._input}, which aren't supported together.`);\n invalid = true;\n }\n\n if ( config.dc && !Number.isNumeric(config.dc) ) {\n config.dc = simplifyBonus(config.dc, options.rollData ?? options.relativeTo?.getRollData?.() ?? {});\n }\n\n if ( invalid ) return null;\n\n if ( complex ) {\n const formatter = game.i18n.getListFormatter({ type: \"disjunction\" });\n const parts = [];\n for ( const [ability, associated] of groups.entries() ) {\n const makeConfig = ({ key, type }) => ({ type, [type]: key, ability: groups.size > 1 ? ability : undefined });\n\n // Multiple associated proficiencies, link each individually\n if ( associated.length > 1 ) parts.push(\n game.i18n.format(\"EDITOR.DND5E.Inline.SpecificCheck\", {\n ability: CONFIG.DND5E.enrichmentLookup.abilities[ability].label,\n type: formatter.format(associated.map(a => createRollLink(a.label, makeConfig(a)).outerHTML ))\n })\n );\n\n // Only single associated proficiency, wrap whole thing in roll link\n else {\n const associatedConfig = makeConfig(associated[0]);\n parts.push(createRollLink(createRollLabel({ ...associatedConfig, ability }), associatedConfig).outerHTML);\n }\n }\n\n if ( usingTool ) {\n config.format = \"long\";\n config.usingTool = usingTool.key;\n }\n label = formatter.format(parts);\n if ( config.dc && !config.hideDC ) {\n label = game.i18n.format(\"EDITOR.DND5E.Inline.DC\", { dc: config.dc, check: label });\n }\n label = game.i18n.format(`EDITOR.DND5E.Inline.Check${config.format === \"long\" ? \"Long\" : \"Short\"}`, { check: label });\n if ( usingTool ) label = game.i18n.format(\"EDITOR.DND5E.Inline.CheckUsing\", {\n check: label, tool: usingTool.label\n });\n\n const template = document.createElement(\"template\");\n template.innerHTML = label;\n return createRequestLink(template, {\n type: \"check\", ...config, skill: config.skill.join(\"|\"), tool: config.tool.join(\"|\")\n });\n }\n\n const type = config.skill.length ? \"skill\" : config.tool.length ? \"tool\" : \"check\";\n config = { type, ability: Array.from(groups.keys())[0], ...config, skill: config.skill[0], tool: config.tool[0] };\n if ( !label ) label = createRollLabel(config);\n return config.passive ? createPassiveTag(label, config) : createRequestLink(createRollLink(label), config);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Create the buttons for a check requested in chat.\n * @param {object} dataset\n * @returns {object[]}\n */\nfunction createCheckRequestButtons(dataset) {\n const skills = dataset.skill?.split(\"|\") ?? [];\n const tools = dataset.tool?.split(\"|\") ?? [];\n if ( (skills.length + tools.length) <= 1 ) return [createRequestButton(dataset)];\n const baseDataset = { ...dataset };\n delete baseDataset.skill;\n delete baseDataset.tool;\n return [\n ...skills.map(skill => createRequestButton({\n ability: CONFIG.DND5E.skills[skill].ability, ...baseDataset, format: \"short\", skill, type: \"skill\"\n })),\n ...dataset.usingTool ? [] : tools.map(tool => createRequestButton({\n ability: CONFIG.DND5E.tools[tool]?.ability, ...baseDataset, format: \"short\", tool, type: \"tool\"\n }))\n ];\n}\n\n/* -------------------------------------------- */\n\n/**\n * Enrich a saving throw link.\n * @param {object} config Configuration data.\n * @param {string} [label] Optional label to replace default text.\n * @param {EnrichmentOptions} options Options provided to customize text enrichment.\n * @returns {HTMLElement|null} An HTML link if the save could be built, otherwise null.\n *\n * @example Create a dexterity saving throw:\n * ```[[/save ability=dex]]```\n * becomes\n * ```html\n * \n * Dexterity \n * \n * \n * ```\n *\n * @example Add a DC to the save:\n * ```[[/save ability=dex dc=20]]```\n * becomes\n * ```html\n * \n * DC 20 Dexterity \n * \n * \n * ```\n *\n * @example Specify multiple abilities:\n * ```[[/save ability=str/dex dc=20]]```\n * ```[[/save strength dexterity 20]]```\n * becomes\n * ```html\n * \n * DC 20\n * Strength or\n * Dexterity \n * \n * \n * ```\n *\n * @example Create a concentration saving throw:\n * ```[[/concentration 10]]```\n * becomes\n * ```html\n * \n * DC 10 concentration \n * \n * \n * ```\n *\n * @example Link an enricher to an save activity, either explicitly or automatically\n * ```[[/save activity=RLQlsLo5InKHZadn]]``` or ```[[/save]]```\n * becomes\n * ```html\n * \n * DC 20 Dexterity \n * \n * \n * ```\n */\nexport async function enrichSave(config, label, options) {\n config.ability = config.ability?.replace(\"/\", \"|\").split(\"|\") ?? [];\n for ( let value of config.values ) {\n const slug = foundry.utils.getType(value) === \"string\" ? slugify(value) : value;\n if ( slug in CONFIG.DND5E.enrichmentLookup.abilities ) config.ability.push(slug);\n else if ( Number.isNumeric(value) ) config.dc = Number(value);\n else config[value] = true;\n }\n config.ability = config.ability\n .filter(a => a in CONFIG.DND5E.enrichmentLookup.abilities)\n .map(a => CONFIG.DND5E.enrichmentLookup.abilities[a].key ?? a);\n\n const activity = config.activity ? options.relativeTo?.system?.activities?.get(config.activity)\n : !config.ability.length ? options.relativeTo?.system?.activities?.getByType(\"save\")[0] : null;\n\n if ( activity ) {\n if ( activity.type !== \"save\" ) {\n console.warn(`Save enricher linked to non-save activity when enriching ${config._input}`);\n return null;\n }\n\n config.ability = Array.from(activity.save.ability);\n config.activityUuid = activity.uuid;\n config.dc = activity.save.dc.value;\n delete config.activity;\n }\n\n if ( !config.ability.length && !config._isConcentration ) {\n console.warn(`No ability or linked activity found while enriching ${config._input}.`);\n return null;\n }\n\n if ( config.dc && !Number.isNumeric(config.dc) ) {\n config.dc = simplifyBonus(config.dc, options.rollData ?? options.relativeTo?.getRollData?.() ?? {});\n }\n\n if ( config.ability.length > 1 && label ) {\n console.warn(`Multiple abilities and custom label found while enriching ${config._input}, which aren't supported together.`);\n return null;\n }\n\n config = { type: config._isConcentration ? \"concentration\" : \"save\", ...config };\n if ( label ) label = createRollLink(label);\n else if ( config.ability.length <= 1 ) label = createRollLink(createRollLabel(config));\n else {\n label = game.i18n.getListFormatter({ type: \"disjunction\" }).format(config.ability.map(ability =>\n createRollLink(createRollLabel({ type: \"save\", ability }), { ability }).outerHTML\n ));\n if ( config.dc && !config.hideDC ) {\n label = game.i18n.format(\"EDITOR.DND5E.Inline.DC\", { dc: config.dc, check: label });\n }\n label = game.i18n.format(`EDITOR.DND5E.Inline.Save${config.format === \"long\" ? \"Long\" : \"Short\"}`, { save: label });\n const template = document.createElement(\"template\");\n template.innerHTML = label;\n label = template;\n }\n return createRequestLink(label, { ...config, ability: config.ability.join(\"|\") });\n}\n\n/* -------------------------------------------- */\n\n/**\n * Create the buttons for a save requested in chat.\n * @param {object} dataset\n * @returns {object[]}\n */\nfunction createSaveRequestButtons(dataset) {\n return (dataset.ability?.split(\"|\") ?? [])\n .map(ability => createRequestButton({ ...dataset, format: \"long\", ability }));\n}\n\n/* -------------------------------------------- */\n\n/**\n * Perform a check or save.\n * @param {object} config Configuration data for the roll.\n * @param {Event} [event] The click event triggering the action.\n * @returns {Promise}\n */\nasync function rollCheckSave(config, event) {\n const { type, ability, skill, tool, dc } = config;\n const options = { event };\n if ( ability in CONFIG.DND5E.abilities ) options.ability = ability;\n if ( dc ) options.target = Number(dc);\n\n const actors = getSceneTargets().map(t => t.actor);\n if ( !actors.length && game.user.character ) actors.push(game.user.character);\n if ( !actors.length ) {\n ui.notifications.warn(\"EDITOR.DND5E.Inline.Warning.NoActor\", { localize: true });\n return;\n }\n\n for ( const actor of actors ) {\n switch ( type ) {\n case \"check\":\n await actor.rollAbilityCheck(options);\n break;\n case \"concentration\":\n await actor.rollConcentration(options);\n break;\n case \"save\":\n await actor.rollSavingThrow(options);\n break;\n case \"skill\":\n await actor.rollSkill({ skill, tool: config.usingTool, ...options });\n break;\n case \"tool\":\n await actor.rollToolCheck({ tool, ...options });\n break;\n }\n }\n}\n\n/* -------------------------------------------- */\n/* Damage Enricher */\n/* -------------------------------------------- */\n\n/**\n * Enrich a damage link.\n * @param {object[]} configs Configuration data.\n * @param {string} [label] Optional label to replace default text.\n * @param {EnrichmentOptions} options Options provided to customize text enrichment.\n * @returns {HTMLElement|null} An HTML link if the save could be built, otherwise null.\n *\n * @example Create a damage link:\n * ```[[/damage 2d6 type=bludgeoning]]``\n * becomes\n * ```html\n * \n * 2d6 bludgeoning\n * \n * ````\n *\n * @example Display the average:\n * ```[[/damage 2d6 type=bludgeoning average=true]]``\n * becomes\n * ```html\n * 7 (\n * 2d6 \n * ) bludgeoning\n * ````\n *\n * @example Manually set the average & don't prefix the type:\n * ```[[/damage 8d4dl force average=666]]``\n * becomes\n * ```html\n * 666 (\n * 8d4dl \n * force\n * ````\n *\n * @example Create a healing link:\n * ```[[/heal 2d6]]``` or ```[[/damage 2d6 healing]]```\n * becomes\n * ```html\n * \n * 2d6 \n * healing\n * ```\n *\n * @example Specify variable damage types:\n * ```[[/damage 2d6 type=fire|cold]]``` or ```[[/damage 2d6 type=fire/cold]]```\n * becomes\n * ```html\n * \n * 2d6 \n * fire or cold\n * ```\n *\n * @example Add multiple damage parts\n * ```[[/damage 1d6 fire & 1d6 cold]]```\n * becomes\n * ```html\n * \n * 1d6 fire and\n * 1d6 cold\n * \n * ```\n *\n * @example Link an enricher to an damage activity, either explicitly or automatically\n * ```[[/damage activity=RLQlsLo5InKHZadn]]``` or ```[[/damage]]```\n * becomes\n * ```html\n * \n * 1d6 fire and\n * 1d6 cold\n * \n * ```\n *\n * @example Displaying the full hit section:\n * ```[[/damage extended]]``\n * becomes\n * ```html\n * \n * Hit: \n * \n * 7 ( 2d6 ) Bludgeoning damage\n * \n * \n * ````\n */\nexport async function enrichDamage(configs, label, options) {\n const config = { type: \"damage\", formulas: [], damageTypes: [], rollType: configs._isHealing ? \"healing\" : \"damage\" };\n for ( const c of configs ) {\n const formulaParts = [];\n if ( c.activity ) config.activity = c.activity;\n if ( c.attackMode ) config.attackMode = c.attackMode;\n if ( c.average ) config.average = c.average;\n if ( c.format ) config.format = c.format;\n if ( c.formula ) formulaParts.push(c.formula);\n c.type = c.type?.replaceAll(\"/\", \"|\").split(\"|\") ?? [];\n for ( const value of c.values ) {\n if ( value in CONFIG.DND5E.damageTypes ) c.type.push(value);\n else if ( value in CONFIG.DND5E.healingTypes ) c.type.push(value);\n else if ( value in CONFIG.DND5E.attackModes ) config.attackMode = value;\n else if ( value === \"average\" ) config.average = true;\n else if ( value === \"extended\" ) config.format = \"extended\";\n else if ( value === \"temp\" ) c.type.push(\"temphp\");\n else formulaParts.push(value);\n }\n c.formula = Roll.defaultImplementation.replaceFormulaData(\n formulaParts.join(\" \"),\n options.rollData ?? options.relativeTo?.getRollData?.() ?? {}\n );\n if ( configs._isHealing && !c.type.length ) c.type.push(\"healing\");\n if ( c.formula ) {\n config.formulas.push(c.formula);\n config.damageTypes.push(c.type.join(\"|\"));\n }\n }\n config.damageTypes = config.damageTypes.map(t => t?.replace(\"/\", \"|\"));\n if ( config.format === \"extended\" ) config.average ??= true;\n\n if ( config.activity && config.formulas.length ) {\n console.warn(`Activity ID and formulas found while enriching ${config._input}, only one is supported.`);\n return null;\n }\n\n let activity = options.relativeTo?.system?.activities?.get(config.activity);\n if ( !activity && !config.formulas.length ) {\n const types = configs._isHealing ? [\"heal\"] : [\"attack\", \"damage\", \"save\"];\n for ( const a of options.relativeTo?.system?.activities?.getByTypes(...types) ?? [] ) {\n if ( a.damage?.parts.length || a.healing?.formula ) {\n activity = a;\n break;\n }\n }\n }\n\n if ( activity ) {\n config.activityUuid = activity.uuid;\n const damageConfig = activity.getDamageConfig({ attackMode: config.attackMode });\n for ( const roll of damageConfig.rolls ) {\n config.formulas.push(simplifyRollFormula(\n Roll.defaultImplementation.replaceFormulaData(roll.parts.join(\" + \"), roll.data)\n ));\n if ( roll.data.scaling ) config.scaling ??= String(roll.data.scaling.increase);\n config.damageTypes.push(roll.options.types?.join(\"|\") ?? roll.options.type);\n }\n delete config.activity;\n }\n\n if ( !config.activityUuid && !config.formulas.length ) {\n console.warn(`No formula or linked activity found while enriching ${config._input}.`);\n return null;\n }\n\n const formulas = config.formulas.join(\"&\");\n const damageTypes = config.damageTypes.join(\"&\");\n\n if ( !config.formulas.length ) return null;\n if ( label ) {\n return createRollLink(label, { ...config, formulas, damageTypes }, { classes: \"roll-link-group roll-link\" });\n }\n\n const parts = [];\n for ( const [idx, formula] of config.formulas.entries() ) {\n const type = config.damageTypes[idx];\n const types = type?.split(\"|\")\n .map(t => CONFIG.DND5E.damageTypes[t]?.label ?? CONFIG.DND5E.healingTypes[t]?.label)\n .filter(_ => _);\n const localizationData = {\n formula: createRollLink(formula, {}, { tag: \"span\" }).outerHTML,\n type: game.i18n.getListFormatter({ type: \"disjunction\" }).format(types)\n };\n if ( configs._rules === \"2014\" ) localizationData.type = localizationData.type.toLowerCase();\n\n let localizationType = \"Short\";\n if ( config.average ) {\n localizationType = \"Long\";\n if ( config.average === true ) {\n const minRoll = Roll.create(formula).evaluate({ minimize: true });\n const maxRoll = Roll.create(formula).evaluate({ maximize: true });\n localizationData.average = Math.floor(((await minRoll).total + (await maxRoll).total) / 2);\n } else if ( Number.isNumeric(config.average) ) {\n localizationData.average = config.average;\n } else {\n localizationType = \"Short\";\n }\n if ( String(localizationData.average) === formula ) localizationType = \"Short\";\n }\n\n parts.push(game.i18n.format(`EDITOR.DND5E.Inline.Damage${localizationType}`, localizationData));\n }\n\n const link = document.createElement(\"a\");\n link.className = \"roll-link-group\";\n link.dataset.action = \"roll\";\n _addDataset(link, { ...config, formulas, damageTypes });\n if ( config.average && (parts.length === 2) ) {\n link.innerHTML = game.i18n.format(\"EDITOR.DND5E.Inline.DamageDouble\", { first: parts[0], second: parts[1] });\n } else {\n link.innerHTML = game.i18n.getListFormatter().format(parts);\n }\n\n if ( config.format === \"extended\" ) {\n const span = document.createElement(\"span\");\n span.className = \"damage-extended\";\n span.innerHTML = game.i18n.format(\"EDITOR.DND5E.Inline.DamageExtended\", { damage: link.outerHTML });\n return span;\n }\n\n return link;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Perform a damage roll.\n * @param {object} config Configuration data for the roll.\n * @param {Event} [event] The click event triggering the action.\n * @returns {Promise}\n */\nasync function rollDamage(config, event) {\n let { activityUuid, attackMode, formulas, damageTypes, rollType, scaling } = config;\n\n if ( activityUuid ) {\n const activity = await _fetchActivity(activityUuid, Number(scaling ?? 0));\n if ( activity ) return activity.rollDamage({ attackMode, event });\n }\n\n formulas = formulas?.split(\"&\") ?? [];\n damageTypes = damageTypes?.split(\"&\") ?? [];\n\n const rollConfig = {\n attackMode, event,\n hookNames: [\"damage\"],\n rolls: formulas.map((formula, idx) => {\n const types = damageTypes[idx]?.split(\"|\") ?? [];\n return {\n parts: [formula],\n options: { type: types[0], types }\n };\n })\n };\n\n const messageConfig = {\n create: true,\n data: {\n flags: {\n dnd5e: {\n messageType: \"roll\",\n roll: { type: rollType },\n targets: getTargetDescriptors()\n }\n },\n flavor: game.i18n.localize(`DND5E.${rollType === \"healing\" ? \"Healing\" : \"Damage\"}Roll`),\n speaker: ChatMessage.implementation.getSpeaker()\n }\n };\n\n const rolls = await CONFIG.Dice.DamageRoll.build(rollConfig, {}, messageConfig);\n if ( !rolls?.length ) return;\n Hooks.callAll(\"dnd5e.rollDamage\", rolls);\n Hooks.callAll(\"dnd5e.rollDamageV2\", rolls);\n}\n\n/* -------------------------------------------- */\n/* Language Enricher */\n/* -------------------------------------------- */\n\n/**\n * Enrich a language reference.\n * @param {object} config Configuration data.\n * @param {string} [label] Optional label to replace default text.\n * @param {EnrichmentOptions} options Options provided to customize text enrichment.\n * @returns {HTMLElement|null} An HTML element if language link could be built, otherwise null.\n */\nexport function enrichLanguage(config, label, options) {\n for ( const value of config.values ) {\n const slug = foundry.utils.getType(value) === \"string\" ? slugify(value) : value;\n if ( slug in CONFIG.DND5E.enrichmentLookup.languages ) config.language = slug;\n }\n delete config.values;\n\n if ( !(config.language in CONFIG.DND5E.enrichmentLookup.languages) ) {\n console.warn(`No language found while enriching ${config._input}.`);\n return null;\n }\n\n config.type = \"language\";\n return createPassiveTag(label ?? CONFIG.DND5E.enrichmentLookup.languages[config.language], config);\n}\n\n/* -------------------------------------------- */\n/* Lookup Enricher */\n/* -------------------------------------------- */\n\n/**\n * Enrich a property lookup.\n * @param {object} config Configuration data.\n * @param {string} [fallback] Optional fallback if the value couldn't be found.\n * @param {EnrichmentOptions} options Options provided to customize text enrichment.\n * @returns {HTMLElement|null} An HTML element if the lookup could be built, otherwise null.\n *\n * @example Include a creature's name in its description:\n * ```[[lookup @name]]```\n * becomes\n * ```html\n * Adult Black Dragon \n * ```\n *\n * @example Lookup a property within an activity:\n * ```[[lookup @target.template.size activity=dnd5eactivity000]]```\n * becomes\n * ```html\n * 120 \n * ```\n */\nexport function enrichLookup(config, fallback, options) {\n let keyPath = config.path;\n let style = config.style;\n for ( const value of config.values ) {\n if ( value === \"capitalize\" ) style ??= \"capitalize\";\n else if ( value === \"lowercase\" ) style ??= \"lowercase\";\n else if ( value === \"uppercase\" ) style ??= \"uppercase\";\n else if ( value.startsWith(\"@\") ) keyPath ??= value;\n }\n\n let activity = options.relativeTo?.system?.activities?.get(config.activity);\n if ( config.activity && !activity ) {\n console.warn(`Activity not found when enriching ${config._input}.`);\n return null;\n }\n\n if ( !keyPath ) {\n console.warn(`Lookup path must be defined to enrich ${config._input}.`);\n return null;\n }\n\n const data = activity ? activity.getRollData().activity : options.rollData\n ?? options.relativeTo?.getRollData?.() ?? {};\n let value = foundry.utils.getProperty(data, keyPath.substring(1)) ?? fallback;\n if ( value !== undefined ) value = String(value);\n if ( value && style ) {\n if ( style === \"capitalize\" ) value = value.capitalize();\n else if ( style === \"lowercase\" ) value = value.toLowerCase();\n else if ( style === \"uppercase\" ) value = value.toUpperCase();\n }\n\n const span = document.createElement(\"span\");\n span.classList.add(\"lookup-value\");\n if ( !value && (options.documents === false) ) return null;\n if ( !value ) span.classList.add(\"not-found\");\n span.innerText = value ?? keyPath;\n return span;\n}\n\n/* -------------------------------------------- */\n/* Reference Enricher */\n/* -------------------------------------------- */\n\n/**\n * Enrich a reference link.\n * @param {object} config Configuration data.\n * @param {string} [label] Optional label to replace default text.\n * @param {EnrichmentOptions} options Options provided to customize text enrichment.\n * @returns {HTMLElement|null} An HTML link to the Journal Entry Page for the given reference.\n *\n * @example Create a content link to the relevant reference:\n * ```&Reference[condition=unconscious]{Label}```\n * becomes\n * ```html\n * \n * \n * Label\n * \n * \n * \n * \n * \n * ```\n */\nexport async function enrichReference(config, label, options) {\n let key;\n let source;\n let type = Object.keys(config).find(k => k in CONFIG.DND5E.ruleTypes);\n if ( type ) {\n key = slugify(config[type]);\n const { references } = CONFIG.DND5E.ruleTypes[type] ?? {};\n source = foundry.utils.getProperty(CONFIG.DND5E, references)?.[key];\n } else if ( config.values.length ) {\n key = slugify(config.values.join(\"\"));\n for ( const [t, { references }] of Object.entries(CONFIG.DND5E.ruleTypes) ) {\n source = foundry.utils.getProperty(CONFIG.DND5E, references)?.[key];\n if ( source ) {\n type = t;\n break;\n }\n }\n }\n if ( !source ) {\n console.warn(`No valid rule found while enriching ${config._input}.`);\n return null;\n }\n const uuid = foundry.utils.getType(source) === \"Object\" ? source.reference : source;\n if ( !uuid ) return null;\n const doc = await fromUuid(uuid);\n const span = document.createElement(\"span\");\n span.classList.add(\"reference-link\");\n span.append(doc.toAnchor({ name: label || doc.name }));\n if ( (type === \"condition\") && (config.apply !== false) ) {\n const apply = document.createElement(\"a\");\n apply.classList.add(\"enricher-action\");\n apply.dataset.action = \"applyStatus\";\n apply.dataset.status = key;\n apply.dataset.tooltip = \"\";\n apply.setAttribute(\"aria-label\", game.i18n.localize(\"EDITOR.DND5E.Inline.ApplyStatus\"));\n apply.innerHTML = ' ';\n span.append(apply);\n }\n return span;\n}\n\n/* -------------------------------------------- */\n/* Use Item Enricher */\n/* -------------------------------------------- */\n\n/**\n * Enrich an item use link to roll an item on the selected token.\n * @param {string[]} config Configuration data.\n * @param {string} [label] Optional label to replace default text.\n * @param {EnrichmentOptions} options Options provided to customize text enrichment.\n * @returns {Promise} An HTML link if the item link could be built, otherwise null.\n *\n * @example Use an Item from a name:\n * ```[[/item Heavy Crossbow]]```\n * becomes\n * ```html\n * \n * Heavy Crossbow\n * \n * ```\n *\n * @example Use an Item from a UUID:\n * ```[[/item Actor.M4eX4Mu5IHCr3TMf.Item.amUUCouL69OK1GZU]]```\n * becomes\n * ```html\n * \n * Bite\n * \n * ```\n *\n * @example Use an Item from an ID:\n * ```[[/item amUUCouL69OK1GZU]]```\n * becomes\n * ```html\n * \n * Bite\n * \n * ```\n *\n * @example Use an Activity on an Item from a name:\n * ```[[/item Heavy Crossbow activity=Poison]]```\n * becomes\n * ```html\n * \n * Heavy Crossbow: Poison\n * \n * ```\n *\n * @example Use an Activity on an Item:\n * ```[[/item amUUCouL69OK1GZU activity=G8ng63Tjqy5W52OP]]```\n * becomes\n * ```html\n * \n * Bite: Save\n * \n * ```\n */\nexport async function enrichItem(config, label, options) {\n const givenItem = config.values.join(\" \");\n let parsed = foundry.utils.parseUuid(givenItem);\n\n const makeLink = (label, dataset) => {\n const span = document.createElement(\"span\");\n span.classList.add(\"roll-link-group\");\n _addDataset(span, dataset);\n span.append(createRollLink(label));\n return span;\n };\n\n if ( [\"Activity\", \"Item\"].includes(parsed?.type) ) {\n const ownerActor = parsed.primaryType === \"Actor\" ? parsed.primaryId\n : parsed.embedded.includes(\"Actor\") ? parsed.embedded[parsed.embedded.findIndex(e => e === \"Actor\") + 1] : null;\n let doc = await fromUuid(parsed.uuid);\n if ( !doc ) {\n console.warn(`Item not found while enriching ${config._input}.`);\n return null;\n }\n if ( (doc instanceof Item) && config.activity ) {\n doc = doc.system.activities?.get(config.activity) ?? doc.system.activities?.getName(config.activity);\n if ( !doc ) {\n console.warn(`Activity not found while enriching ${config._input}.`);\n return null;\n }\n }\n if ( !label ) {\n if ( doc instanceof Item ) label = doc.name;\n else label = game.i18n.format(\"EDITOR.DND5E.Inline.ItemActivity\", { item: doc.item.name, activity: doc.name });\n }\n return makeLink(label, { type: \"item\", rollItemActor: ownerActor, [`roll${doc.documentName}Uuid`]: doc.uuid });\n }\n\n const foundActor = options.relativeTo instanceof Item\n ? options.relativeTo.parent\n : options.relativeTo instanceof Actor ? options.relativeTo : null;\n let foundItem = foundActor?.items.get(givenItem);\n let foundActivity;\n\n // If config is a relative UUID\n if ( givenItem.startsWith(\".\") ) {\n try {\n foundItem = await fromUuid(givenItem, { relative: options.relativeTo });\n } catch(err) { return null; }\n }\n\n if ( !foundItem && !givenItem && (options.relativeTo instanceof Item) ) foundItem = options.relativeTo;\n\n if ( foundItem ) {\n if ( config.activity ) {\n foundActivity = foundItem.system.activities?.get(config.activity)\n ?? foundItem.system.activities?.getName(config.activity);\n if ( !foundActivity ) {\n console.warn(`Activity ${config.activity} not found on ${foundItem.name} while enriching ${config._input}.`);\n return null;\n }\n if ( !label ) label = game.i18n.format(\"EDITOR.DND5E.Inline.ItemActivity\", {\n item: foundItem.name, activity: foundActivity.name\n });\n return makeLink(label, { type: \"item\", rollActivityUuid: foundActivity.uuid });\n }\n\n if ( !label ) label = foundItem.name;\n return makeLink(label, { type: \"item\", rollItemUuid: foundItem.uuid });\n }\n\n // Finally, if config is an item name\n if ( !label ) label = config.activity ? game.i18n.format(\"EDITOR.DND5E.Inline.ItemActivity\", {\n item: foundItem?.name ?? givenItem, activity: foundActivity?.name ?? config.activity\n }) : givenItem;\n return makeLink(label, {\n type: \"item\", rollItemActor: foundActor?.uuid, rollItemName: givenItem, rollActivityName: config.activity\n });\n}\n\n/* -------------------------------------------- */\n\n/**\n * Use an Item from an Item enricher.\n * @param {object} config\n * @param {string} [config.rollActivityUuid] Lookup the Activity by UUID.\n * @param {string} [config.rollActivityName] Lookup the Activity by name.\n * @param {string} [config.rollItemUuid] Lookup the Item by UUID.\n * @param {string} [config.rollItemName] Lookup the Item by name.\n * @param {string} [config.rollItemActor] The UUID of a specific Actor that should use the Item.\n * @param {Event} event The click event triggering the action.\n * @returns {Promise}\n */\nasync function useItem({ rollActivityUuid, rollActivityName, rollItemUuid, rollItemName, rollItemActor }, event) {\n // If UUID is provided, always roll that item directly\n if ( rollActivityUuid ) return (await fromUuid(rollActivityUuid))?.use({ event });\n if ( rollItemUuid ) return (await fromUuid(rollItemUuid))?.use({ event });\n\n if ( !rollItemName ) return;\n const actor = rollItemActor ? await fromUuid(rollItemActor) : null;\n\n // If no actor is specified or player isn't owner, fall back to the macro rolling logic\n if ( !actor?.isOwner ) return rollItem(rollItemName, { activityName: rollActivityName });\n const token = canvas.tokens.controlled[0];\n\n // If a token is controlled, and it has an item with the correct name, activate it\n let item = token?.actor.items.getName(rollItemName);\n\n // Otherwise check the specified actor for the item\n if ( !item ) {\n item = actor.items.getName(rollItemName);\n\n // Display a warning to indicate the item wasn't rolled from the controlled actor\n if ( item && canvas.tokens.controlled.length ) ui.notifications.warn(\n game.i18n.format(\"MACRO.5eMissingTargetWarn\", {\n actor: token.name, name: rollItemName, type: game.i18n.localize(\"DOCUMENT.Item\")\n })\n );\n }\n\n if ( item ) {\n if ( rollActivityName ) {\n const activity = item.system.activities?.getName(rollActivityName);\n if ( activity ) return activity.use({ event });\n\n // If no activity could be found at all, display a warning\n else ui.notifications.warn(game.i18n.format(\"EDITOR.DND5E.Inline.Warning.NoActivityOnItem\", {\n activity: rollActivityName, actor: actor.name, item: rollItemName\n }));\n }\n\n else return item.use({ event });\n }\n\n // If no item could be found at all, display a warning\n else ui.notifications.warn(game.i18n.format(\"EDITOR.DND5E.Inline.Warning.NoItemOnActor\", {\n actor: actor.name, item: rollItemName\n }));\n}\n\n/* -------------------------------------------- */\n/* Labels & Links */\n/* -------------------------------------------- */\n\n/**\n * Create a passive skill tag.\n * @param {string} label Label to display.\n * @param {object} dataset Data that will be added to the tag.\n * @returns {HTMLElement}\n */\nfunction createPassiveTag(label, dataset) {\n const span = document.createElement(\"span\");\n span.classList.add(\"passive-check\");\n _addDataset(span, {\n ...dataset,\n tooltip: `\n \n `\n });\n span.innerText = label;\n return span;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Create a label for a roll message.\n * @param {object} config Configuration data.\n * @returns {string}\n */\nexport function createRollLabel(config) {\n const { label: ability, abbreviation } = CONFIG.DND5E.abilities[config.ability] ?? {};\n const skill = CONFIG.DND5E.skills[config.skill]?.label;\n const toolUUID = CONFIG.DND5E.enrichmentLookup.tools[config.tool];\n const tool = toolUUID?.id ? Trait.getBaseItem(toolUUID.id, { indexOnly: true })?.name : toolUUID?.label ?? null;\n const longSuffix = config.format === \"long\" ? \"Long\" : \"Short\";\n const showDC = config.dc && !config.hideDC;\n\n let label;\n switch ( config.type ) {\n case \"check\":\n case \"skill\":\n case \"tool\":\n if ( ability && (skill || tool) ) {\n label = game.i18n.format(\"EDITOR.DND5E.Inline.SpecificCheck\", { ability, type: skill ?? tool });\n } else {\n label = ability;\n }\n if ( config.passive ) {\n label = game.i18n.format(\n `EDITOR.DND5E.Inline.${showDC ? \"DC\" : \"\"}Passive${longSuffix}`, { dc: config.dc, check: label }\n );\n } else {\n if ( showDC ) label = game.i18n.format(\"EDITOR.DND5E.Inline.DC\", { dc: config.dc, check: label });\n label = game.i18n.format(`EDITOR.DND5E.Inline.Check${longSuffix}`, { check: label });\n }\n break;\n case \"concentration\":\n case \"save\":\n if ( config.type === \"save\" ) label = ability;\n else label = `${game.i18n.localize(\"DND5E.Concentration\")} ${ability ? `(${abbreviation})` : \"\"}`;\n if ( showDC ) label = game.i18n.format(\"EDITOR.DND5E.Inline.DC\", { dc: config.dc, check: label });\n label = game.i18n.format(`EDITOR.DND5E.Inline.Save${longSuffix}`, { save: label });\n break;\n default:\n return \"\";\n }\n\n if ( config.icon ) {\n switch ( config.type ) {\n case \"check\":\n case \"skill\":\n label = ` ${label}`;\n break;\n case \"tool\":\n label = ` ${label}`;\n break;\n case \"concentration\":\n case \"save\":\n label = ` ${label}`;\n break;\n }\n }\n\n return label;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Create a rollable link with a request section for GMs.\n * @param {HTMLElement|string} label Label to display\n * @param {object} dataset Data that will be added to the link for the rolling method.\n * @returns {HTMLElement}\n */\nfunction createRequestLink(label, dataset) {\n const span = document.createElement(\"span\");\n span.classList.add(\"roll-link-group\");\n _addDataset(span, dataset);\n if ( label instanceof HTMLTemplateElement ) span.append(label.content);\n else span.append(label);\n\n // Add chat request link for GMs\n if ( game.user.isGM ) {\n const gmLink = document.createElement(\"a\");\n gmLink.classList.add(\"enricher-action\");\n gmLink.dataset.action = \"postRequest\";\n gmLink.dataset.tooltip = \"EDITOR.DND5E.Inline.RequestRoll\";\n gmLink.setAttribute(\"aria-label\", game.i18n.localize(gmLink.dataset.tooltip));\n gmLink.insertAdjacentHTML(\"afterbegin\", ' ');\n span.insertAdjacentElement(\"beforeend\", gmLink);\n }\n\n return span;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Create a rollable link.\n * @param {string} label Label to display.\n * @param {object} [dataset={}] Data that will be added to the link for the rolling method.\n * @param {object} [options={}]\n * @param {boolean} [options.classes=\"roll-link\"] Class to add to the link.\n * @param {string} [options.tag=\"a\"] Tag to use for the main link.\n * @returns {HTMLElement}\n */\nfunction createRollLink(label, dataset={}, { classes=\"roll-link\", tag=\"a\" }={}) {\n const link = document.createElement(tag);\n link.className = classes;\n link.insertAdjacentHTML(\"afterbegin\", ' ');\n link.append(label);\n _addDataset(link, dataset);\n if ( tag === \"a\" ) link.dataset.action = \"roll\";\n return link;\n}\n\n/* -------------------------------------------- */\n/* Actions */\n/* -------------------------------------------- */\n\n/**\n * Attach actions to chat message for requested rolls.\n * @param {ChatMessage5e} message\n * @param {HTMLElement} element\n */\nexport function activateChatListeners(message, element) {\n _addListeners(element.querySelectorAll('[data-action=\"concentration\"]'), handleRoll);\n _addListeners(element.querySelectorAll('[data-action=\"rollRequest\"]'), handleRoll);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Attach actions to enrichers when they are rendered.\n * @param {HTMLEnrichedContentElement} element\n */\nfunction onRenderEnricher(element) {\n _addListeners(element.querySelectorAll('[data-action=\"applyStatus\"]'), handleApplyStatus);\n _addListeners(element.querySelectorAll('[data-action=\"awardRequest\"]'), handleAward);\n _addListeners(element.querySelectorAll('[data-action=\"postRequest\"]'), handlePostRequest);\n _addListeners(element.querySelectorAll('[data-action=\"roll\"]'), handleRoll);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Create the combined dataset for the target button and any parent groups.\n * @param {HTMLElement} target Button that was clicked.\n * @returns {object}\n */\nfunction getRollActionDataset(target) {\n return {\n ...((target.closest(\".roll-link-group\") ?? target)?.dataset ?? {}),\n ...(target.closest(\".roll-link\")?.dataset ?? {})\n };\n}\n\n/* -------------------------------------------- */\n\n/**\n * Toggle status effects on selected tokens.\n * @param {Event} event Triggering click event.\n * @param {HTMLElement} target Button that was clicked.\n */\nasync function handleApplyStatus(event, target) {\n const status = target.dataset.status;\n if ( !status ) return;\n window.getSelection().empty();\n const actors = new Set();\n for ( const { actor } of canvas.tokens.controlled ) {\n if ( !actor || actors.has(actor) ) continue;\n await actor.toggleStatusEffect(status);\n actors.add(actor);\n }\n}\n\n/* -------------------------------------------- */\n\n/**\n * Forward clicks on award requests to the Award application.\n * @param {Event} event Triggering click event.\n * @param {HTMLElement} target Button that was clicked.\n */\nasync function handleAward(event, target) {\n const command = target?.closest(\"[data-award-command]\")?.dataset.awardCommand;\n if ( !command ) return;\n window.getSelection().empty();\n Award.handleAward(command);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Handle creating a roll request chat message.\n * @param {Event} event Triggering click event.\n * @param {HTMLElement} target Button that was clicked.\n */\nasync function handlePostRequest(event, target) {\n window.getSelection().empty();\n const dataset = getRollActionDataset(target);\n\n let buttons;\n if ( dataset.type === \"check\" ) buttons = createCheckRequestButtons(dataset);\n else if ( dataset.type === \"save\" ) buttons = createSaveRequestButtons(dataset);\n else buttons = [createRequestButton({ ...dataset, format: \"short\" })];\n\n const MessageClass = getDocumentClass(\"ChatMessage\");\n const chatData = {\n user: game.user.id,\n content: await foundry.applications.handlebars.renderTemplate(\n \"systems/dnd5e/templates/chat/roll-request-card.hbs\", { buttons }\n ),\n flavor: game.i18n.localize(\"EDITOR.DND5E.Inline.RollRequest\"),\n speaker: MessageClass.getSpeaker({ user: game.user })\n };\n MessageClass.create(chatData);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Create a button for a chat request.\n * @param {object} dataset\n * @returns {object}\n */\nfunction createRequestButton(dataset) {\n return {\n buttonLabel: createRollLabel({ ...dataset, icon: true }),\n hiddenLabel: createRollLabel({ ...dataset, icon: true, hideDC: true }),\n dataset: { ...dataset, action: \"rollRequest\", visibility: \"all\" }\n };\n}\n\n/* -------------------------------------------- */\n\n/**\n * Handle performing a roll.\n * @param {Event} event Triggering click event.\n * @param {HTMLElement} target Button that was clicked.\n * @returns {Promise}\n */\nasync function handleRoll(event, target) {\n const dataset = getRollActionDataset(target);\n const link = target.closest(\"a\") ?? target;\n link.disabled = true;\n window.getSelection().empty();\n try {\n switch ( dataset.type ) {\n case \"attack\": return await rollAttack(dataset, event);\n case \"damage\": return await rollDamage(dataset, event);\n case \"item\": return await useItem(dataset, event);\n default: return await rollCheckSave(dataset, event);\n }\n } finally {\n link.disabled = false;\n }\n}\n\n/* -------------------------------------------- */\n/* Helpers */\n/* -------------------------------------------- */\n\n/**\n * Add a dataset object to the provided element.\n * @param {HTMLElement} element Element to modify.\n * @param {object} dataset Data properties to add.\n * @private\n */\nfunction _addDataset(element, dataset) {\n for ( const [key, value] of Object.entries(dataset) ) {\n if ( !key.startsWith(\"_\") && (key !== \"values\") && value ) element.dataset[key] = value;\n }\n}\n\n/* -------------------------------------------- */\n\nconst LISTENER = Symbol(\"enricherListener\");\n\n/**\n * Add click listeners for each of the provided buttons, passing the event and target to the handler.\n * @param {HTMLButtonElement[]} buttons Buttons to attach the listeners to.\n * @param {Function} handler Click handler to call.\n * @private\n */\nfunction _addListeners(buttons, handler) {\n buttons.forEach(button => {\n // TODO: Remove this fix in DnD5e 6.0 when https://github.com/foundryvtt/foundryvtt/issues/13558 is fixed\n button.removeEventListener(\"click\", button[LISTENER]);\n button[LISTENER] = event => handler(event, event.currentTarget);\n button.addEventListener(\"click\", button[LISTENER]);\n });\n}\n\n/* -------------------------------------------- */\n\n/**\n * Fetch an activity with scaling applied.\n * @param {string} uuid Activity UUID.\n * @param {number} scaling Scaling increase to apply.\n * @returns {Activity|void}\n */\nasync function _fetchActivity(uuid, scaling) {\n const activity = await fromUuid(uuid);\n if ( !activity || !scaling ) return activity;\n const item = activity.item.clone({ \"flags.dnd5e.scaling\": scaling }, { keepId: true });\n return item.system.activities.get(activity.id);\n}\n","import { getRulesVersion } from \"../../enrichers.mjs\";\nimport { filteredKeys, formatNumber } from \"../../utils.mjs\";\nimport ItemDataModel from \"../abstract/item-data-model.mjs\";\nimport IdentifierField from \"../fields/identifier-field.mjs\";\nimport ActivationField from \"../shared/activation-field.mjs\";\nimport DurationField from \"../shared/duration-field.mjs\";\nimport RangeField from \"../shared/range-field.mjs\";\nimport TargetField from \"../shared/target-field.mjs\";\nimport ActivitiesTemplate from \"./templates/activities.mjs\";\nimport ItemDescriptionTemplate from \"./templates/item-description.mjs\";\n\nconst { BooleanField, NumberField, SchemaField, SetField, StringField } = foundry.data.fields;\n\n/**\n * @import { SpellItemSystemData } from \"./_types.mjs\";\n * @import { ActivitiesTemplateData ItemDescriptionTemplateData } from \"./templates/_types.mjs\";\n */\n\n/**\n * Data definition for Spell items.\n * @extends {ItemDataModel}\n * @mixes ActivitiesTemplateData\n * @mixes ItemDescriptionTemplateData\n * @mixes SpellItemSystemData\n */\nexport default class SpellData extends ItemDataModel.mixin(ActivitiesTemplate, ItemDescriptionTemplate) {\n\n /* -------------------------------------------- */\n /* Model Configuration */\n /* -------------------------------------------- */\n\n /** @override */\n static LOCALIZATION_PREFIXES = [\n \"DND5E.ACTIVATION\", \"DND5E.DURATION\", \"DND5E.RANGE\", \"DND5E.SOURCE\", \"DND5E.TARGET\"\n ];\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static defineSchema() {\n return this.mergeSchema(super.defineSchema(), {\n ability: new StringField({ label: \"DND5E.SpellAbility\" }),\n activation: new ActivationField(),\n duration: new DurationField(),\n level: new NumberField({ required: true, integer: true, initial: 1, min: 0, label: \"DND5E.SpellLevel\" }),\n materials: new SchemaField({\n value: new StringField({ required: true, label: \"DND5E.SpellMaterialsDescription\" }),\n consumed: new BooleanField({ required: true, label: \"DND5E.SpellMaterialsConsumed\" }),\n cost: new NumberField({ required: true, initial: 0, min: 0, label: \"DND5E.SpellMaterialsCost\" }),\n supply: new NumberField({ required: true, initial: 0, min: 0, label: \"DND5E.SpellMaterialsSupply\" })\n }, { label: \"DND5E.SpellMaterials\" }),\n method: new StringField({ required: true, initial: \"\", label: \"DND5E.SpellPreparation.Method\" }),\n prepared: new NumberField({ required: true, nullable: false, integer: true, min: 0, initial: 0 }),\n properties: new SetField(new StringField(), { label: \"DND5E.SpellComponents\" }),\n range: new RangeField(),\n school: new StringField({ required: true, label: \"DND5E.SpellSchool\" }),\n sourceItem: new IdentifierField({ allowType: true, label: \"DND5E.SourceItem.Label\" }),\n target: new TargetField()\n });\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static metadata = Object.freeze(foundry.utils.mergeObject(super.metadata, {\n enchantable: true,\n hasEffects: true\n }, { inplace: false }));\n\n /* -------------------------------------------- */\n\n /** @override */\n static get compendiumBrowserFilters() {\n return new Map([\n [\"level\", {\n label: \"DND5E.Level\",\n type: \"range\",\n config: {\n keyPath: \"system.level\",\n min: 0,\n max: Object.keys(CONFIG.DND5E.spellLevels).length - 1\n }\n }],\n [\"school\", {\n label: \"DND5E.School\",\n type: \"set\",\n config: {\n choices: CONFIG.DND5E.spellSchools,\n keyPath: \"system.school\"\n }\n }],\n [\"spelllist\", {\n label: \"TYPES.JournalEntryPage.spells\",\n type: \"set\",\n createFilter: (filters, value, def) => {\n let include = new Set();\n let exclude = new Set();\n for ( const [k, v] of Object.entries(value ?? {}) ) {\n const list = dnd5e.registry.spellLists.forType(k);\n if ( !list || (v === 0) ) continue;\n if ( v === 1 ) include = include.union(list.identifiers);\n else if ( v === -1 ) exclude = exclude.union(list.identifiers);\n }\n if ( include.size ) filters.push({ k: \"system.identifier\", o: \"in\", v: include });\n if ( exclude.size ) filters.push({ o: \"NOT\", v: { k: \"system.identifier\", o: \"in\", v: exclude } });\n },\n config: {\n choices: dnd5e.registry.spellLists.options.reduce((obj, entry) => {\n const [type, identifier] = entry.value.split(\":\");\n const list = dnd5e.registry.spellLists.forType(type, identifier);\n if ( list?.identifiers.size ) obj[entry.value] = {\n label: entry.label, group: CONFIG.DND5E.spellListTypes[type]\n };\n return obj;\n }, {}),\n collapseGroup: group => group !== CONFIG.DND5E.spellListTypes.class\n }\n }],\n [\"properties\", this.compendiumBrowserPropertiesFilter(\"spell\")]\n ]);\n }\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /**\n * Attack classification of this spell.\n * @type {\"spell\"}\n */\n get attackClassification() {\n return \"spell\";\n }\n\n /* -------------------------------------------- */\n\n /**\n * The identifier of the spellcasting class associated with this spell, resolved through subclass parentage where\n * necessary. Returns an empty string if the spell was not granted by a class or subclass item.\n * @type {string}\n */\n get classIdentifier() {\n if ( !this.sourceItem ) return \"\";\n const sourceItem = this.parent?.actor?.identifiedItems.get(this.sourceItem)?.first();\n if ( sourceItem?.type === \"class\" ) return sourceItem.identifier;\n if ( sourceItem?.type === \"subclass\" ) return sourceItem.system.classIdentifier ?? \"\";\n return \"\";\n }\n\n /* -------------------------------------------- */\n\n /**\n * @deprecated since 5.3\n * @ignore\n */\n get sourceClass() {\n foundry.utils.logCompatibilityWarning(\"SpellData#sourceClass is deprecated. Please use SpellData#sourceItem \"\n + \"instead.\", { since: \"DnD5e 5.3\", until: \"DnD5e 6.0\" });\n return this.classIdentifier ?? \"\";\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n get availableAbilities() {\n if ( this.ability ) return new Set([this.ability]);\n\n const spellcasting = this.parent?.actor?.spellcastingClasses[this.classIdentifier]?.spellcasting.ability\n ?? this.parent?.actor?.system.attributes?.spellcasting;\n return new Set(spellcasting ? [spellcasting] : []);\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n get canConfigureScaling() {\n return this.level > 0;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Whether the spell can be prepared.\n * @type {boolean}\n */\n get canPrepare() {\n return !!CONFIG.DND5E.spellcasting[this.method]?.prepares;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n get canScale() {\n return (this.level > 0) && !!CONFIG.DND5E.spellcasting[this.method]?.slots;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n get canScaleDamage() {\n return true;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Properties displayed in chat.\n * @type {string[]}\n */\n get chatProperties() {\n return [\n this.parent.labels.level,\n this.parent.labels.components.vsm + (this.parent.labels.materials ? ` (${this.parent.labels.materials})` : \"\"),\n ...this.parent.labels.components.tags,\n this.parent.labels.duration\n ];\n }\n\n /* -------------------------------------------- */\n\n /**\n * Whether this spell counts towards a class' number of prepared spells.\n * @type {boolean}\n */\n get countsPrepared() {\n return !!CONFIG.DND5E.spellcasting[this.method]?.prepares\n && (this.level > 0)\n && (this.prepared === CONFIG.DND5E.spellPreparationStates.prepared.value);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n get _typeAbilityMod() {\n return this.availableAbilities.first() ?? \"int\";\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n get criticalThreshold() {\n return this.parent?.actor?.flags.dnd5e?.spellCriticalThreshold ?? Infinity;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Retrieve a linked activity that granted this spell using the stored `cachedFor` value.\n * @returns {Activity|null}\n */\n get linkedActivity() {\n const relative = this.parent.actor;\n const uuid = this.parent.getFlag(\"dnd5e\", \"cachedFor\");\n if ( !relative || !uuid ) return null;\n const data = foundry.utils.parseUuid(uuid, { relative });\n const [itemId, , activityId] = (data?.embedded ?? []).slice(-3);\n return relative.items.get(itemId)?.system.activities?.get(activityId) ?? null;\n // TODO: Swap back to fromUuidSync once https://github.com/foundryvtt/foundryvtt/issues/11214 is resolved\n // return fromUuidSync(this.parent.getFlag(\"dnd5e\", \"cachedFor\"), { relative, strict: false }) ?? null;\n }\n\n /* -------------------------------------------- */\n\n /**\n * The proficiency multiplier for this item.\n * @returns {number}\n */\n get proficiencyMultiplier() {\n return 1;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n get scalingIncrease() {\n if ( this.level === 0 ) return Math.floor(((this.parent.actor?.system.cantripLevel?.(this.parent) ?? 0) + 1) / 6);\n const activity = this.linkedActivity;\n if ( !activity?.spell?.level || (activity.spell.level <= this.level) ) return null;\n return activity.spell.level - this.level;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n get tooltipSubtitle() {\n return [this.parent.labels.level, CONFIG.DND5E.spellSchools[this.school]?.label];\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /**\n * @deprecated since 5.1\n * @ignore\n */\n get preparation() {\n foundry.utils.logCompatibilityWarning(\"SpellData#preparation is deprecated. Please use SpellData#method in \"\n + \"place of preparation.mode and SpellData#prepared in place of preparation.prepared.\",\n { since: \"DnD5e 5.1\", until: \"DnD5e 6.0\" });\n if ( this.prepared === 2 ) return { mode: \"always\", prepared: 1 };\n if ( this.method === \"spell\" ) return { mode: \"prepared\", prepared: Boolean(this.prepared) };\n return { mode: this.method, prepared: Boolean(this.prepared) };\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static _migrateData(source) {\n super._migrateData(source);\n ActivitiesTemplate.migrateActivities(source);\n SpellData.#migrateActivation(source);\n SpellData.#migrateTarget(source);\n SpellData.#migratePreparation(source);\n SpellData.#migrateSourceItem(source);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Migrate the component object to be 'properties' instead.\n * @param {object} source The candidate source data from which the model will be constructed.\n */\n static _migrateComponentData(source) {\n const components = filteredKeys(source.system?.components ?? {});\n if ( components.length ) {\n foundry.utils.setProperty(source, \"flags.dnd5e.migratedProperties\", components);\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Migrate activation data.\n * Added in DnD5e 4.0.0.\n * @param {object} source The candidate source data from which the model will be constructed.\n */\n static #migrateActivation(source) {\n if ( source.activation?.cost ) source.activation.value = source.activation.cost;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Migrate target data.\n * Added in DnD5e 4.0.0.\n * @param {object} source The candidate source data from which the model will be constructed.\n */\n static #migrateTarget(source) {\n if ( !(\"target\" in source) ) return;\n source.target.affects ??= {};\n source.target.template ??= {};\n\n if ( \"units\" in source.target ) source.target.template.units = source.target.units;\n if ( \"width\" in source.target ) source.target.template.width = source.target.width;\n\n const type = source.target.type ?? source.target.template.type ?? source.target.affects.type;\n if ( type in CONFIG.DND5E.areaTargetTypes ) {\n if ( \"type\" in source.target ) source.target.template.type = type;\n if ( \"value\" in source.target ) source.target.template.size = source.target.value;\n } else if ( type in CONFIG.DND5E.individualTargetTypes ) {\n if ( \"type\" in source.target ) source.target.affects.type = type;\n if ( \"value\" in source.target ) source.target.affects.count = source.target.value;\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Migrate preparation data.\n * @since 5.1.0\n * @param {object} source The candidate source data from which the model will be constructed.\n */\n static #migratePreparation(source) {\n if ( source.preparation === undefined ) return;\n if ( source.preparation.mode === \"always\" ) {\n if ( !(\"method\" in source) ) source.method = \"spell\";\n if ( !(\"prepared\" in source) ) source.prepared = 2;\n } else {\n if ( !(\"method\" in source) ) {\n if ( source.preparation.mode === \"prepared\" ) source.method = \"spell\";\n else if ( source.preparation.mode ) source.method = source.preparation.mode;\n }\n if ( (typeof source.preparation.prepared === \"boolean\") && !(\"prepared\" in source) ) {\n source.prepared = Number(source.preparation.prepared);\n }\n }\n delete source.preparation;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Migrate sourceClass to sourceItem.\n * @since 5.3.0\n * @param {object} source The candidate source data from which the model will be constructed.\n */\n static #migrateSourceItem(source) {\n if ( \"sourceClass\" in source ) {\n if ( source.sourceClass ) source.sourceItem = `class:${source.sourceClass}`;\n delete source.sourceClass;\n }\n }\n\n /* -------------------------------------------- */\n /* Data Preparation */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareDerivedData() {\n super.prepareDerivedData();\n this.prepareDescriptionData();\n this.properties.add(\"mgc\");\n this.duration.concentration = this.properties.has(\"concentration\");\n\n const labels = this.parent.labels ??= {};\n labels.level = CONFIG.DND5E.spellLevels[this.level];\n labels.school = CONFIG.DND5E.spellSchools[this.school]?.label;\n if ( this.properties.has(\"material\") ) labels.materials = this.materials.value;\n\n labels.components = this.properties.reduce((obj, c) => {\n const config = this.validProperties.has(c) ? CONFIG.DND5E.itemProperties[c] : null;\n if ( !config ) return obj;\n const { abbreviation: abbr, label, icon } = config;\n // Only add properties to display arrays if they have displayable content\n if ( config.isTag ) {\n // Tag properties: add to tags if has label\n if ( label ) obj.tags.push(label);\n if ( abbr || icon ) obj.all.push({ abbr, icon, tag: true });\n } else if ( abbr ) {\n // VSM properties: only add if has abbreviation\n obj.vsm.push(abbr);\n obj.all.push({ abbr, icon, tag: false });\n }\n // Properties with neither abbreviation nor isTag are silently ignored for display\n return obj;\n }, { all: [], vsm: [], tags: [] });\n labels.components.vsm = game.i18n.getListFormatter({ style: \"narrow\" }).format(labels.components.vsm);\n labels.components.full = labels.materials ? game.i18n.format(\"DND5E.SpellComponentsMaterial\", {\n components: labels.components.vsm, materials: labels.materials\n }) : labels.components.vsm;\n\n const uuid = this.parent._stats.compendiumSource ?? this.parent.uuid;\n Object.defineProperty(labels, \"classes\", {\n get() {\n return Array.from(dnd5e.registry.spellLists.forSpell(uuid))\n .filter(list => list.metadata.type === \"class\")\n .map(list => list.name)\n .sort((lhs, rhs) => lhs.localeCompare(rhs, game.i18n.lang));\n },\n configurable: true\n });\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareFinalData() {\n const rollData = this.parent.getRollData({ deterministic: true });\n const labels = this.parent.labels ??= {};\n this.prepareFinalActivityData(rollData);\n ActivationField.prepareData.call(this, rollData, labels);\n DurationField.prepareData.call(this, rollData, labels);\n RangeField.prepareData.call(this, rollData, labels);\n TargetField.prepareData.call(this, rollData, labels);\n\n // Count preparations.\n if ( this.classIdentifier && this.countsPrepared ) {\n const sourceClass = this.parent.actor.spellcastingClasses[this.classIdentifier];\n const sourceSubclass = sourceClass?.subclass;\n if ( sourceClass ) sourceClass.system.spellcasting.preparation.value++;\n if ( sourceSubclass ) sourceSubclass.system.spellcasting.preparation.value++;\n }\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async getCardData(enrichmentOptions={}) {\n const context = await super.getCardData(enrichmentOptions);\n context.isSpell = true;\n const { activation, components, duration, range, target } = this.parent.labels;\n context.properties = [components?.vsm, activation, duration, range, target].filter(_ => _);\n if ( !this.properties.has(\"material\") ) delete context.materials;\n return context;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async getFavoriteData() {\n return foundry.utils.mergeObject(await super.getFavoriteData(), {\n subtitle: [this.parent.labels.components.vsm, this.parent.labels.activation],\n modifier: this.parent.labels.modifier,\n range: this.range,\n save: this.activities.getByType(\"save\")[0]?.save\n });\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async getSheetData(context) {\n context.properties.active = [...(this.parent.labels?.components?.tags ?? []), ...(context.labels.classes ?? [])];\n context.subtitles = [\n { label: context.labels.level },\n { label: context.labels.school },\n { label: CONFIG.DND5E.spellcasting[this.method]?.label }\n ];\n\n context.parts = [\"dnd5e.details-spell\", \"dnd5e.field-uses\"];\n context.sourceItemLocked = false;\n\n // Default Ability & Spellcasting Classes\n if ( this.parent.actor ) {\n // Get spell source item.\n const sourceItem = this.sourceItem\n ? this.parent.actor.identifiedItems.get(this.sourceItem)?.first()\n : null;\n\n const ability = CONFIG.DND5E.abilities[\n this.parent.actor.spellcastingClasses[this.classIdentifier]?.spellcasting.ability\n ?? this.parent.actor.system.attributes?.spellcasting\n ]?.label?.toLowerCase();\n if ( ability ) context.defaultAbility = game.i18n.format(\"DND5E.DefaultSpecific\", { default: ability });\n else context.defaultAbility = game.i18n.localize(\"DND5E.Default\");\n context.spellcastingClasses = Object.entries(this.parent.actor.spellcastingClasses ?? {})\n .map(([value, cls]) => ({ value: `class:${value}`, label: cls.name }));\n\n // Spells granted by non-class Items are locked.\n if ( sourceItem?.type !== \"class\" ) {\n let grantingItem = sourceItem;\n\n // Fallback to detecting from flags.\n if ( !grantingItem ) {\n // Check for advancement-granted spells.\n const advancementOrigin = this.parent.getFlag(\"dnd5e\", \"advancementOrigin\");\n if ( advancementOrigin ) {\n const [itemId] = advancementOrigin.split(\".\");\n grantingItem = this.parent.actor.items.get(itemId);\n }\n\n // Check for item-granted spells.\n grantingItem ??= this.linkedActivity?.item;\n }\n\n if ( grantingItem ) {\n context.spellcastingClasses.push({\n value: `${grantingItem.type}:${grantingItem.identifier}`,\n label: grantingItem.name\n });\n\n if ( !this.sourceItem ) context.source.sourceItem = `${grantingItem.type}:${grantingItem.identifier}`;\n\n context.sourceItemLocked = true;\n context.sourceItemHint = \"DND5E.SourceItem.LockedHint\";\n }\n }\n }\n\n // Activation\n context.activationTypes = [\n ...Object.entries(CONFIG.DND5E.activityActivationTypes).map(([value, { label, group }]) => {\n return { value, label, group };\n }),\n { value: \"\", label: \"DND5E.NoneActionLabel\" }\n ];\n\n // Duration\n context.durationUnits = [\n ...Object.entries(CONFIG.DND5E.specialTimePeriods).map(([value, label]) => ({ value, label })),\n ...Object.entries(CONFIG.DND5E.scalarTimePeriods).map(([value, label]) => {\n return { value, label, group: \"DND5E.DurationTime\" };\n }),\n ...Object.entries(CONFIG.DND5E.permanentTimePeriods).map(([value, label]) => {\n return { value, label, group: \"DND5E.DurationPermanent\" };\n })\n ];\n\n // Targets\n context.targetTypes = [\n ...Object.entries(CONFIG.DND5E.individualTargetTypes).map(([value, { label }]) => {\n return { value, label, group: \"DND5E.TargetTypeIndividual\" };\n }),\n ...Object.entries(CONFIG.DND5E.areaTargetTypes).map(([value, { label }]) => {\n return { value, label, group: \"DND5E.TargetTypeArea\" };\n })\n ];\n context.scalarTarget = this.target.affects.type\n && (CONFIG.DND5E.individualTargetTypes[this.target.affects.type]?.scalar !== false);\n context.affectsPlaceholder = game.i18n.localize(`DND5E.TARGET.Count.${\n this.target?.template?.type ? \"Every\" : \"Any\"}`);\n context.dimensions = this.target.template.dimensions;\n // TODO: Ensure this behaves properly with enchantments, will probably need source target data\n\n // Range\n context.rangeTypes = [\n ...Object.entries(CONFIG.DND5E.rangeTypes).map(([value, label]) => ({ value, label })),\n ...Object.entries(CONFIG.DND5E.movementUnits).map(([value, { label }]) => {\n return { value, label, group: \"DND5E.RangeDistance\" };\n })\n ];\n\n // Spellcasting\n context.canPrepare = this.canPrepare;\n context.spellcastingMethods = Object.values(CONFIG.DND5E.spellcasting).map(({ key, label }) => {\n return { label, value: key };\n });\n if ( this.method && !(this.method in CONFIG.DND5E.spellcasting) ) {\n context.spellcastingMethods.push({ label: this.method, value: this.method });\n }\n }\n\n /* -------------------------------------------- */\n /* Drag & Drop */\n /* -------------------------------------------- */\n\n /** @override */\n static onDropCreate(event, actor, itemData) {\n if ( !actor?.system.isCreature ) return;\n\n // Determine the section it is dropped on, if any.\n let header = event.target.closest(\".items-header\"); // Dropped directly on the header.\n if ( !header ) {\n const list = event.target.closest(\".item-list\"); // Dropped inside an existing list.\n header = list?.previousElementSibling;\n }\n const { method } = header?.closest(\"[data-level]\")?.dataset ?? {};\n\n // Determine the actor's spell slot progressions, if any.\n const spellcastKeys = Object.keys(CONFIG.DND5E.spellcasting);\n const progs = Object.values(actor.classes).reduce((acc, cls) => {\n const type = cls.spellcasting?.type;\n if ( spellcastKeys.includes(type) ) acc.add(type);\n return acc;\n }, new Set());\n\n const { system } = itemData;\n const methods = CONFIG.DND5E.spellcasting;\n if ( methods[method] ) system.method = method;\n else if ( progs.size ) system.method = progs.first();\n else if ( actor.system.attributes.spell?.level ) system.method = \"spell\";\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n getRollData(...options) {\n const data = super.getRollData(...options);\n data.item.level = data.item.level + (this.parent.getFlag(\"dnd5e\", \"scaling\")\n ?? (this.level !== 0 ? this.scalingIncrease : 0));\n return data;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n async toEmbed(config, options={}) {\n const description = await super.toEmbed(config, options);\n config.details ??= config.values.includes(\"details\");\n if ( !config.details ) return description;\n\n const details = document.createElement(\"div\");\n details.classList.add(\"item-entry-details\");\n const labels = this.parent.labels;\n const rulesVersion = getRulesVersion(config, { ...options, relativeTo: this.parent });\n\n const tag = document.createElement(\"p\");\n tag.classList.add(\"item-entry-tag\");\n const classes = labels.classes;\n tag.innerText = game.i18n.format(\n `DND5E.SPELL.Embed.Tag.${!this.level ? \"Cantrip\" : \"Leveled\"}${rulesVersion === \"2014\" ? \"Legacy\" : \"\"}`,\n {\n level: formatNumber(this.level),\n levelOrdinal: formatNumber(this.level, { ordinal: true }),\n school: CONFIG.DND5E.spellSchools[this.school]?.label ?? \"\"\n }\n );\n if ( (rulesVersion === \"2014\") && this.properties.has(\"ritual\") ) {\n tag.innerText = game.i18n.format(\"DND5E.SPELL.Embed.Tag.Ritual\", { levelSchool: tag.innerText });\n } else if ( (rulesVersion === \"2024\") && classes?.length ) {\n tag.innerText = game.i18n.format(\"DND5E.SPELL.Embed.Tag.Classes\", {\n classes: game.i18n.getListFormatter({ type: \"unit\" }).format(classes),\n levelSchool: tag.innerText\n });\n }\n details.append(tag);\n\n let castingTime = rulesVersion === \"2014\" ? labels.legacyActivation : labels.ritualActivation;\n if ( (this.activation.type === \"reaction\") && this.activation.condition ) castingTime = game.i18n.format(\n \"DND5E.SPELL.Embed.CastingTimeTrigger\", { castingTime, trigger: this.activation.condition }\n );\n const specifics = [\n [\"DND5E.SpellCastTime\", castingTime],\n [\"DND5E.SpellHeader.Range\", labels.description.range || labels.range],\n [\"DND5E.Components\", labels.components.full],\n [\"DND5E.Duration\", labels.concentrationDuration]\n ];\n const dl = document.createElement(\"dl\");\n dl.classList.add(\"item-entry-specifics\");\n for ( const [label, description] of specifics ) {\n const div = document.createElement(\"div\");\n const dt = document.createElement(\"dt\");\n dt.innerText = game.i18n.localize(label);\n const dd = document.createElement(\"dd\");\n dd.innerText = description;\n div.append(dt, dd);\n dl.append(div);\n }\n details.append(dl);\n\n const template = document.createElement(\"template\");\n template.append(details, ...description);\n\n /**\n * A hook event that fires after an embedded spell with details is rendered.\n * @function dnd5e.renderEmbeddedSpell\n * @memberof hookEvents\n * @param {Item5e} item Spell being embedded.\n * @param {HTMLTemplateElement} template Template whose children will be embedded.\n * @param {DocumentHTMLEmbedConfig} config Configuration for embedding behavior.\n * @param {EnrichmentOptions} options Original enrichment options.\n */\n Hooks.call(\"dnd5e.renderEmbeddedSpell\", this.parent, template, config, options);\n\n return template.children;\n }\n\n /* -------------------------------------------- */\n /* Socket Event Handlers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _preCreate(data, options, user) {\n if ( (await super._preCreate(data, options, user)) === false ) return false;\n if ( !this.parent.isEmbedded ) return;\n const system = data.system ?? {};\n\n // Set as prepared for NPCs, and not prepared for PCs\n if ( this.parent.actor?.system.isCreature && !(\"prepared\" in system) ) {\n this.updateSource({ prepared: Number(this.parent.actor.system.isNPC || (this.level < 1)) });\n }\n\n if ( [\"atwill\", \"innate\"].includes(system.method) || this.sourceItem ) return;\n const classes = new Set(Object.keys(this.parent.actor.spellcastingClasses));\n if ( !classes.size ) return;\n\n // Set the source class, and ensure the preparation mode matches if adding a prepared spell to an alt class\n const setClass = cls => {\n this.updateSource({ sourceItem: `class:${cls}`, method: this.parent.actor.classes[cls].spellcasting.type });\n };\n\n // If preparation mode matches an alt spellcasting type and matching class exists, set as that class\n if ( (system.method !== \"spell\") && (system.method in CONFIG.DND5E.spellcasting) ) {\n const altClasses = classes.filter(i => this.parent.actor.classes[i].spellcasting.type === system.method);\n if ( altClasses.size === 1 ) setClass(altClasses.first());\n return;\n }\n\n // If only a single spellcasting class is present, use that\n if ( classes.size === 1 ) {\n setClass(classes.first());\n return;\n }\n\n // Create intersection of spellcasting classes and classes that offer the spell\n const spellClasses = new Set(\n dnd5e.registry.spellLists.forSpell(this.parent._stats.compendiumSource).map(l => l.metadata.identifier)\n );\n const intersection = classes.intersection(spellClasses);\n if ( intersection.size === 1 ) setClass(intersection.first());\n }\n}\n","/**\n * Lightweight class containing scaling information for an item that is used in roll data to ensure it is available\n * in the correct format in roll formulas: `@scaling` is the scaling value, and `@scaling.increase` as the scaling\n * steps above baseline.\n *\n * @param {number} increase Scaling steps above baseline.\n */\nexport default class Scaling {\n constructor(increase) {\n this.#increase = increase;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Scaling steps above baseline.\n * @type {number}\n */\n #increase;\n\n get increase() {\n return this.#increase;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Value of the scaling starting 1.\n * @type {string}\n */\n get value() {\n return this.#increase + 1;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n toString() {\n return this.value;\n }\n}\n","/**\n * Object describing the proficiency for a specific ability or skill.\n *\n * @param {number} proficiency Actor's flat proficiency bonus based on their current level.\n * @param {number} multiplier Value by which to multiply the actor's base proficiency value.\n * @param {boolean} [roundDown] Should half-values be rounded up or down?\n */\nexport default class Proficiency {\n constructor(proficiency, multiplier, roundDown=true) {\n\n /**\n * Base proficiency value of the actor.\n * @type {number}\n * @private\n */\n this._baseProficiency = Number(proficiency ?? 0);\n\n /**\n * Value by which to multiply the actor's base proficiency value.\n * @type {number}\n */\n this.multiplier = Number(multiplier ?? 0);\n\n /**\n * Direction decimal results should be rounded (\"up\" or \"down\").\n * @type {string}\n */\n this.rounding = roundDown ? \"down\" : \"up\";\n }\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /**\n * Should only deterministic proficiency be returned, regardless of system settings?\n * @type {boolean}\n */\n deterministic = false;\n\n /* -------------------------------------------- */\n\n /**\n * Flat proficiency value regardless of proficiency mode.\n * @type {number}\n */\n get flat() {\n const roundMethod = (this.rounding === \"down\") ? Math.floor : Math.ceil;\n return roundMethod(this.multiplier * this._baseProficiency);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Dice-based proficiency value regardless of proficiency mode.\n * @type {string}\n */\n get dice() {\n if ( (this._baseProficiency === 0) || (this.multiplier === 0) ) return \"0\";\n const roundTerm = (this.rounding === \"down\") ? \"floor\" : \"ceil\";\n if ( this.multiplier === 0.5 ) {\n return `${roundTerm}(1d${this._baseProficiency * 2} / 2)`;\n } else {\n return `${this.multiplier}d${this._baseProficiency * 2}`;\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Either flat or dice proficiency term based on configured setting.\n * @type {string}\n */\n get term() {\n return (dnd5e.settings.proficiencyModifier === \"dice\") && !this.deterministic\n ? this.dice : String(this.flat);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Whether the proficiency is greater than zero.\n * @type {boolean}\n */\n get hasProficiency() {\n return (this._baseProficiency > 0) && (this.multiplier > 0);\n }\n\n /* -------------------------------------------- */\n /* Methods */\n /* -------------------------------------------- */\n\n /**\n * Calculate an actor's proficiency modifier based on level or CR.\n * @param {number} level Level or CR To use for calculating proficiency modifier.\n * @returns {number} Proficiency modifier.\n */\n static calculateMod(level) {\n return Math.floor((level + 7) / 4);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Return a clone of this proficiency with any changes applied.\n * @param {object} [updates={}]\n * @param {number} updates.proficiency Actor's flat proficiency bonus based on their current level.\n * @param {number} updates.multiplier Value by which to multiply the actor's base proficiency value.\n * @param {boolean} updates.roundDown Should half-values be rounded up or down?\n * @returns {Proficiency}\n */\n clone({ proficiency, multiplier, roundDown }={}) {\n proficiency ??= this._baseProficiency;\n multiplier ??= this.multiplier;\n roundDown ??= this.rounding === \"down\";\n return new this.constructor(proficiency, multiplier, roundDown);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Override the default `toString` method to return flat proficiency for backwards compatibility in formula.\n * @returns {string} Either flat or dice proficiency term based on configured setting.\n */\n toString() {\n return this.term;\n }\n}\n","/**\n * Mixin used to add system flags enforcement to types.\n * @template {foundry.abstract.Document} T\n * @param {typeof T} Base The base document class to wrap.\n * @returns {typeof SystemFlags}\n * @mixin\n */\nexport default function SystemFlagsMixin(Base) {\n class SystemFlags extends Base {\n /**\n * Get the data model that represents system flags.\n * @type {typeof DataModel|null}\n * @abstract\n */\n get _systemFlagsDataModel() {\n return null;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareData() {\n super.prepareData();\n if ( (\"dnd5e\" in this.flags) && this._systemFlagsDataModel ) {\n this.flags.dnd5e = new this._systemFlagsDataModel(this._source.flags.dnd5e, { parent: this });\n }\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async setFlag(scope, key, value) {\n if ( (scope === \"dnd5e\") && this._systemFlagsDataModel ) {\n let diff;\n const changes = foundry.utils.expandObject({ [key]: value });\n if ( this.flags.dnd5e ) diff = this.flags.dnd5e.updateSource(changes, { dryRun: true });\n else diff = new this._systemFlagsDataModel(changes, { parent: this }).toObject();\n return this.update({ flags: { dnd5e: diff } });\n }\n return super.setFlag(scope, key, value);\n }\n }\n return SystemFlags;\n}\n","import DependentDocumentMixin from \"./dependent.mjs\";\nimport SystemFlagsMixin from \"./flags.mjs\";\n\n/**\n * Mixin used to share some logic between Actor & Item documents.\n * @template {foundry.abstract.Document} T\n * @param {typeof T} Base The base document class to wrap.\n * @returns {typeof SystemDocument}\n * @mixin\n */\nexport default function SystemDocumentMixin(Base) {\n class SystemDocument extends DependentDocumentMixin(SystemFlagsMixin(Base)) {\n /** @inheritDoc */\n get _systemFlagsDataModel() {\n return this.system?.metadata?.systemFlagsModel ?? null;\n }\n }\n return SystemDocument;\n}\n","import ActivityChoiceDialog from \"../applications/activity/activity-choice-dialog.mjs\";\nimport AdvancementManager from \"../applications/advancement/advancement-manager.mjs\";\nimport AdvancementConfirmationDialog from \"../applications/advancement/advancement-confirmation-dialog.mjs\";\nimport ContextMenu5e from \"../applications/context-menu.mjs\";\nimport CreateDocumentDialog from \"../applications/create-document-dialog.mjs\";\nimport CreateScrollDialog from \"../applications/item/create-scroll-dialog.mjs\";\nimport ClassData from \"../data/item/class.mjs\";\nimport ContainerData from \"../data/item/container.mjs\";\nimport EquipmentData from \"../data/item/equipment.mjs\";\nimport SpellData from \"../data/item/spell.mjs\";\nimport ActivitiesTemplate from \"../data/item/templates/activities.mjs\";\nimport PhysicalItemTemplate from \"../data/item/templates/physical-item.mjs\";\nimport { formatIdentifier, staticID } from \"../utils.mjs\";\nimport Scaling from \"./scaling.mjs\";\nimport Proficiency from \"./actor/proficiency.mjs\";\nimport SelectChoices from \"./actor/select-choices.mjs\";\nimport Advancement from \"./advancement/advancement.mjs\";\nimport SystemDocumentMixin from \"./mixins/document.mjs\";\n\nconst TextEditor = foundry.applications.ux.TextEditor.implementation;\n\n/**\n * @import { D20RollConfiguration } from \"../dice/_types.mjs\";\n * @import {\n * ItemContentsTransformer, ItemRollData, RollDataOptions, SpellcastingDescription, SpellScrollConfiguration\n * } from \"./_types.mjs\";\n * @import {\n * ActivityDialogConfiguration, ActivityMessageConfiguration, ActivityUsageResults, ActivityUseConfiguration\n * } from \"./activity/_types.mjs\";\n */\n\n/**\n * Override and extend the basic Item implementation.\n */\nexport default class Item5e extends SystemDocumentMixin(Item) {\n\n /** @override */\n static DEFAULT_ICON = \"systems/dnd5e/icons/svg/documents/item.svg\";\n\n /* -------------------------------------------- */\n\n /**\n * Caches an item linked to this one, such as a subclass associated with a class.\n * @type {Item5e}\n * @private\n */\n _classLink;\n\n /* -------------------------------------------- */\n\n /**\n * An object that tracks which tracks the changes to the data model which were applied by active effects\n * @type {object}\n */\n overrides = this.overrides ?? {};\n\n /* -------------------------------------------- */\n\n /**\n * Types that can be selected within the compendium browser.\n * @param {object} [options={}]\n * @param {Set} [options.chosen] Types that have been selected.\n * @returns {SelectChoices}\n */\n static compendiumBrowserTypes({ chosen=new Set() }={}) {\n const [generalTypes, physicalTypes] = Item.TYPES.reduce(([g, p], t) => {\n if ( ![CONST.BASE_DOCUMENT_TYPE, \"backpack\"].includes(t) ) {\n if ( \"inventorySection\" in (CONFIG.Item.dataModels[t] ?? {}) ) p.push(t);\n else g.push(t);\n }\n return [g, p];\n }, [[], []]);\n\n const makeChoices = (types, categoryChosen) => types.reduce((obj, type) => {\n obj[type] = {\n label: CONFIG.Item.typeLabels[type],\n chosen: chosen.has(type) || categoryChosen\n };\n return obj;\n }, {});\n const choices = makeChoices(generalTypes);\n choices.physical = {\n label: game.i18n.localize(\"DND5E.ITEM.Category.Physical\"),\n children: makeChoices(physicalTypes, chosen.has(\"physical\"))\n };\n return new SelectChoices(choices);\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _initializeSource(data, options={}) {\n if ( data instanceof foundry.abstract.DataModel ) data = data.toObject();\n\n // Migrate backpack -> container.\n if ( data.type === \"backpack\" ) {\n data.type = \"container\";\n foundry.utils.setProperty(data, \"flags.dnd5e.persistSourceMigration\", true);\n }\n\n /**\n * A hook event that fires before source data is initialized for an Item in a compendium.\n * @function dnd5e.initializeItemSource\n * @memberof hookEvents\n * @param {Item5e} item Item for which the data is being initialized.\n * @param {object} source Source data being initialized.\n * @param {object} options Additional data initialization options.\n */\n if ( options.pack || options.parent?.pack ) Hooks.callAll(\"dnd5e.initializeItemSource\", this, data, options);\n\n if ( data.type === \"spell\" ) {\n return super._initializeSource(new Proxy(data, {\n set(target, prop, value, receiver) {\n if ( prop === \"preparation\" ) console.trace(value);\n return Reflect.set(target, prop, value, receiver);\n },\n\n defineProperty(target, prop, attributes) {\n if ( prop === \"preparation\" ) console.trace(attributes);\n return Reflect.defineProperty(target, prop, attributes);\n }\n }), options);\n }\n\n Object.defineProperty(this, \"_needsAdvancementMigration\", { value: Array.isArray(data.system?.advancement) });\n\n return super._initializeSource(data, options);\n }\n\n /* -------------------------------------------- */\n /* Item Properties */\n /* -------------------------------------------- */\n\n /**\n * Which ability score modifier is used by this item?\n * @type {string|null}\n * @see {@link ActionTemplate#abilityMod}\n */\n get abilityMod() {\n return this.system.abilityMod ?? null;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Should deletion of this item be allowed? Doesn't prevent programatic deletion, but affects UI controls.\n * @type {boolean}\n */\n get canDelete() {\n return !this.flags.dnd5e?.cachedFor;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Should duplication of this item be allowed? Doesn't prevent programatic duplication, but affects UI controls.\n * @type {boolean}\n */\n get canDuplicate() {\n return !this.system.metadata?.singleton && ![\"class\", \"subclass\"].includes(this.type)\n && !this.flags.dnd5e?.cachedFor;\n }\n\n /* --------------------------------------------- */\n\n /**\n * The item that contains this item, if it is in a container. Returns a promise if the item is located\n * in a compendium pack.\n * @type {Item5e|Promise|void}\n */\n get container() {\n if ( !this.system.container ) return;\n if ( this.isEmbedded ) return this.actor.items.get(this.system.container);\n if ( this.pack ) return game.packs.get(this.pack).getDocument(this.system.container);\n return game.items.get(this.system.container);\n }\n\n /* -------------------------------------------- */\n\n /**\n * What is the critical hit threshold for this item, if applicable?\n * @type {number|null}\n * @see {@link ActionTemplate#criticalThreshold}\n */\n get criticalThreshold() {\n return this.system.criticalThreshold ?? null;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Active effect that granted this item as a rider.\n * @type {ActiveEffect5e|null}\n */\n get dependentOrigin() {\n return fromUuidSync(this.flags.dnd5e?.dependentOn, { relative: this, strict: false }) ?? null;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Does this item support advancement and have advancements defined?\n * @type {boolean}\n */\n get hasAdvancement() {\n return !!this.system.advancement?.size;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Does the Item implement an attack roll as part of its usage?\n * @type {boolean}\n * @see {@link ActionTemplate#hasAttack}\n */\n get hasAttack() {\n return this.system.hasAttack ?? false;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Is this Item limited in its ability to be used by charges or by recharge?\n * @type {boolean}\n * @see {@link ActivatedEffectTemplate#hasLimitedUses}\n * @see {@link FeatData#hasLimitedUses}\n */\n get hasLimitedUses() {\n return this.system.hasLimitedUses ?? false;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Does the Item implement a saving throw as part of its usage?\n * @type {boolean}\n * @see {@link ActionTemplate#hasSave}\n */\n get hasSave() {\n return this.system.hasSave ?? false;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Return an item's identifier.\n * @type {string}\n */\n get identifier() {\n if ( this.system.identifier ) return this.system.identifier;\n return formatIdentifier(this.name);\n }\n\n /* --------------------------------------------- */\n\n /**\n * Is this Item an activatable item?\n * @type {boolean}\n */\n get isActive() {\n return this.system.isActive ?? false;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Is this item any of the armor subtypes?\n * @type {boolean}\n * @see {@link EquipmentTemplate#isArmor}\n */\n get isArmor() {\n return this.system.isArmor ?? false;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Does the item provide an amount of healing instead of conventional damage?\n * @type {boolean}\n * @see {@link ActionTemplate#isHealing}\n */\n get isHealing() {\n return this.system.isHealing ?? false;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Is this item a separate large object like a siege engine or vehicle component that is\n * usually mounted on fixtures rather than equipped, and has its own AC and HP?\n * @type {boolean}\n * @see {@link EquipmentData#isMountable}\n * @see {@link WeaponData#isMountable}\n */\n get isMountable() {\n return this.system.isMountable ?? false;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Is this class item the original class for the containing actor? If the item is not a class or it is not\n * embedded in an actor then this will return `null`.\n * @type {boolean|null}\n */\n get isOriginalClass() {\n if ( this.type !== \"class\" || !this.isEmbedded || !this.parent.system.details?.originalClass ) return null;\n return this.id === this.parent.system.details.originalClass;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Does the Item implement a versatile damage roll as part of its usage?\n * @type {boolean}\n * @see {@link ActionTemplate#isVersatile}\n */\n get isVersatile() {\n return this.system.isVersatile ?? false;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Is the item rechargeable?\n * @type {boolean}\n */\n get hasRecharge() {\n return this.hasLimitedUses && (this.system.uses?.recovery[0]?.period === \"recharge\");\n }\n\n /* --------------------------------------------- */\n\n /**\n * Is the item on recharge cooldown?\n * @type {boolean}\n */\n get isOnCooldown() {\n return this.hasRecharge && (this.system.uses.value < 1);\n }\n\n /* --------------------------------------------- */\n\n /**\n * Does this item require concentration?\n * @type {boolean}\n */\n get requiresConcentration() {\n if ( this.system.validProperties.has(\"concentration\") && this.system.properties.has(\"concentration\") ) return true;\n return this.system.activities?.contents[0]?.duration.concentration ?? false;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Class associated with this subclass. Always returns null on non-subclass or non-embedded items.\n * @type {Item5e|null}\n */\n get class() {\n if ( !this.isEmbedded || (this.type !== \"subclass\") ) return null;\n const cid = this.system.classIdentifier;\n return this._classLink ??= this.parent.items.find(i => (i.type === \"class\") && (i.identifier === cid));\n }\n\n /* -------------------------------------------- */\n\n /**\n * Subclass associated with this class. Always returns null on non-class or non-embedded items.\n * @type {Item5e|null}\n */\n get subclass() {\n if ( !this.isEmbedded || (this.type !== \"class\") ) return null;\n const items = this.parent.items;\n const cid = this.identifier;\n return this._classLink ??= items.find(i => (i.type === \"subclass\") && (i.system.classIdentifier === cid));\n }\n\n /* -------------------------------------------- */\n\n /**\n * Retrieve scale values for current level from advancement data.\n * @type {Record}\n */\n get scaleValues() {\n if ( !this.advancement.byType.ScaleValue ) return {};\n const item = [\"class\", \"subclass\"].includes(this.advancementRootItem?.type) ? this.advancementRootItem : this;\n const level = item.type === \"class\" ? item.system.levels : item.type === \"subclass\" ? item.class?.system.levels\n : item.system.advancementLevel ?? this.parent?.system.details.level ?? 0;\n return this.advancement.byType.ScaleValue.reduce((obj, advancement) => {\n if ( (advancement.classRestriction === \"primary\") && !this.isOriginalClass ) return obj;\n if ( (advancement.classRestriction === \"secondary\") && this.isOriginalClass ) return obj;\n obj[advancement.identifier] = advancement.valueForLevel(level);\n return obj;\n }, {});\n }\n\n /* -------------------------------------------- */\n\n /**\n * Scaling increase for this item based on flag or item-type specific details.\n * @type {number}\n */\n get scalingIncrease() {\n return this.system?.scalingIncrease ?? this.getFlag(\"dnd5e\", \"scaling\") ?? 0;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Retrieve the spellcasting for a class or subclass. For classes, this will return the spellcasting\n * of the subclass if it overrides the class. For subclasses, this will return the class's spellcasting\n * if no spellcasting is defined on the subclass.\n * @type {SpellcastingDescription|null} Spellcasting object containing progression & ability.\n */\n get spellcasting() {\n const spellcasting = this.system.spellcasting;\n if ( !spellcasting ) return null;\n const isSubclass = this.type === \"subclass\";\n const classSC = isSubclass ? this.class?.system.spellcasting : spellcasting;\n const subclassSC = isSubclass ? spellcasting : this.subclass?.system.spellcasting;\n const finalSC = foundry.utils.deepClone(\n ( subclassSC && (subclassSC.progression !== \"none\") ) ? subclassSC : classSC\n );\n return finalSC ?? null;\n }\n\n /* -------------------------------------------- */\n /* Active Effects */\n /* -------------------------------------------- */\n\n /**\n * Get all ActiveEffects that may apply to this Item.\n * @yields {ActiveEffect5e}\n * @returns {Generator}\n */\n *allApplicableEffects() {\n for ( const effect of this.effects ) {\n if ( effect.isAppliedEnchantment ) yield effect;\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Apply any transformation to the Item data which are caused by enchantment Effects.\n */\n applyActiveEffects() {\n // Organize non-disabled effects by their application priority\n const changes = [];\n for ( const effect of this.allApplicableEffects() ) {\n if ( !effect.active ) continue;\n changes.push(...effect.changes.map(change => {\n const c = foundry.utils.deepClone(change);\n c.effect = effect;\n c.priority ??= c.mode * 10;\n return c;\n }));\n }\n changes.sort((a, b) => a.priority - b.priority);\n if ( game.release.generation > 13 ) foundry.documents.ActiveEffect._shimChanges?.(changes);\n\n // Apply all changes\n const overrides = {};\n const replacementData = this.getRollData();\n for ( const change of changes ) {\n if ( !change.key ) continue;\n const changes = (game.release.generation > 13)\n ? change.effect.constructor.applyChange(this, change, { replacementData })\n : change.effect.apply(this, change);\n Object.assign(overrides, changes);\n }\n\n // Expand the set of final overrides\n foundry.utils.mergeObject(this.overrides, foundry.utils.expandObject(overrides));\n }\n\n /* -------------------------------------------- */\n\n /**\n * Should this item's active effects be suppressed.\n * @type {boolean}\n */\n get areEffectsSuppressed() {\n const requireEquipped = (this.type !== \"consumable\")\n || [\"rod\", \"trinket\", \"wand\"].includes(this.system.type.value);\n if ( requireEquipped && (this.system.equipped === false) ) return true;\n return !this.system.attuned && (this.system.attunement === \"required\");\n }\n\n /* -------------------------------------------- */\n /* Data Initialization */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n clone(data={}, options={}) {\n if ( options.save ) return super.clone(data, options);\n if ( this.parent ) this.parent._embeddedPreparation = true;\n const item = super.clone(data, options);\n if ( item.parent ) {\n delete item.parent._embeddedPreparation;\n item.prepareFinalAttributes();\n }\n return item;\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static migrateData(source) {\n source = super.migrateData(source);\n ActivitiesTemplate.initializeActivities(source);\n if ( source.type === \"class\" ) ClassData._migrateTraitAdvancement(source);\n else if ( source.type === \"container\" ) ContainerData._migrateWeightlessData(source);\n else if ( source.type === \"equipment\" ) EquipmentData._migrateStealth(source);\n else if ( source.type === \"spell\" ) SpellData._migrateComponentData(source);\n return source;\n }\n\n /* -------------------------------------------- */\n /* Data Preparation */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareBaseData() {\n this._clearData();\n }\n\n /* -------------------------------------------- */\n\n /**\n * Clear or replace properties not automatically reset by upstream initialization.\n * @protected\n */\n _clearData() {\n this.labels = {};\n this.overrides = {};\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareEmbeddedDocuments() {\n super.prepareEmbeddedDocuments();\n for ( const activity of this.system.activities ?? [] ) activity.prepareData();\n for ( const advancement of this.system.advancement ?? [] ) {\n if ( !(advancement instanceof Advancement) ) continue;\n advancement.prepareData();\n }\n if ( !this.actor || this.actor._embeddedPreparation ) this.applyActiveEffects();\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareDerivedData() {\n this.labels ??= {};\n super.prepareDerivedData();\n\n // Clear out linked item cache\n this._classLink = undefined;\n\n // Advancement\n this._prepareAdvancement();\n\n // Item Properties\n if ( this.system.properties ) {\n this.labels.properties = this.system.properties.reduce((acc, prop) => {\n if ( (prop === \"concentration\") && !this.requiresConcentration ) return acc;\n acc.push({\n abbr: prop,\n label: CONFIG.DND5E.itemProperties[prop]?.label,\n icon: CONFIG.DND5E.itemProperties[prop]?.icon\n });\n return acc;\n }, []);\n }\n\n // Un-owned items can have their final preparation done here, otherwise this needs to happen in the owning Actor\n if ( !this.isOwned ) this.prepareFinalAttributes();\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare advancement objects from stored advancement data.\n * @protected\n */\n _prepareAdvancement() {\n const minAdvancementLevel = [\"class\", \"subclass\"].includes(this.type) ? 1 : 0;\n this.advancement = {\n byId: {},\n byLevel: Object.fromEntries(\n Array.fromRange(CONFIG.DND5E.maxLevel + 1).slice(minAdvancementLevel).map(l => [l, []])\n ),\n byType: {},\n needingConfiguration: []\n };\n for ( const advancement of this.system.advancement ?? [] ) {\n if ( !(advancement instanceof Advancement) ) continue;\n this.advancement.byId[advancement.id] = advancement;\n this.advancement.byType[advancement.type] ??= [];\n this.advancement.byType[advancement.type].push(advancement);\n advancement.levels.forEach(l => this.advancement.byLevel[l]?.push(advancement));\n if ( !advancement.levels.length\n || ((advancement.levels.length === 1) && (advancement.levels[0] < minAdvancementLevel)) ) {\n this.advancement.needingConfiguration.push(advancement);\n }\n }\n Object.entries(this.advancement.byLevel).forEach(([lvl, data]) => data.sort((a, b) => {\n return a.sortingValueForLevel(lvl).localeCompare(b.sortingValueForLevel(lvl), game.i18n.lang);\n }));\n }\n\n /* -------------------------------------------- */\n\n /**\n * Determine an item's proficiency level based on its parent actor's proficiencies.\n * @protected\n */\n _prepareProficiency() {\n if ( ![\"spell\", \"weapon\", \"equipment\", \"tool\", \"feat\", \"consumable\"].includes(this.type) ) return;\n if ( !this.actor?.system.attributes?.prof ) {\n this.system.prof = new Proficiency(0, 0);\n return;\n }\n\n this.system.prof = new Proficiency(this.actor.system.attributes.prof, this.system.proficiencyMultiplier ?? 0);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Compute item attributes which might depend on prepared actor data. If this item is embedded this method will\n * be called after the actor's data is prepared.\n * Otherwise, it will be called at the end of `Item5e#prepareDerivedData`.\n */\n prepareFinalAttributes() {\n this._prepareProficiency();\n this.system.prepareFinalData?.();\n this._prepareLabels();\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare top-level summary labels based on configured activities.\n * @protected\n */\n _prepareLabels() {\n const activations = this.labels.activations = [];\n const attacks = this.labels.attacks = [];\n const damages = this.labels.damages = [];\n if ( !this.system.activities?.size ) return;\n const existingDamageLabels = new Set();\n let firstDamage = true;\n for ( const activity of this.system.activities ) {\n if ( !(\"activation\" in activity) || !activity.canUse ) continue;\n const activationLabels = activity.activationLabels;\n if ( activationLabels ) activations.push({\n ...activationLabels,\n concentrationDuration: activity.labels.concentrationDuration,\n ritualActivation: activity.labels.ritualActivation\n });\n if ( activity.type === \"attack\" ) {\n const { toHit, modifier } = activity.labels;\n attacks.push({ toHit, modifier });\n }\n for ( const damage of activity.labels?.damage ?? [] ) {\n if ( existingDamageLabels.has(damage.label) ) continue;\n existingDamageLabels.add(damage.label);\n damages.push({ ...damage, firstDamage });\n }\n if ( activity.labels?.damage?.length ) firstDamage = false;\n }\n if ( activations.length ) {\n Object.assign(this.labels, activations[0]);\n delete activations[0].concentrationDuration;\n delete activations[0].ritualActivation;\n }\n if ( attacks.length ) Object.assign(this.labels, attacks[0]);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Render a rich tooltip for this item.\n * @param {EnrichmentOptions} [enrichmentOptions={}] Options for text enrichment.\n * @returns {Promise<{content: string, classes: string[]}>|null}\n */\n richTooltip(enrichmentOptions={}) {\n return this.system.richTooltip?.() ?? null;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Trigger an Item usage, optionally creating a chat message with followup actions.\n * @param {ActivityUseConfiguration} config Configuration info for the activation.\n * @param {boolean} [config.chooseActivity=false] Force the activity selection prompt unless the fast-forward modifier\n * is held.\n * @param {ActivityDialogConfiguration} dialog Configuration info for the usage dialog.\n * @param {ActivityMessageConfiguration} message Configuration info for the created chat message.\n * @returns {Promise} Returns the usage results for the triggered\n * activity, or the chat message if the Item had no\n * activities and was posted directly to chat.\n */\n async use(config={}, dialog={}, message={}) {\n if ( this.pack ) return;\n\n let event = config.event;\n const activities = this.system.activities?.filter(a => a.canUse);\n if ( activities?.length ) {\n const { chooseActivity, ...activityConfig } = config;\n let usageConfig = activityConfig;\n let dialogConfig = dialog;\n let messageConfig = message;\n let activity = activities[0];\n if ( ((activities.length > 1) || chooseActivity) && !event?.shiftKey ) {\n activity = await ActivityChoiceDialog.create(this, { sheet: dialog.options?.sheet });\n }\n if ( !activity ) return;\n return activity.use(usageConfig, dialogConfig, messageConfig);\n }\n if ( this.actor ) return this.displayCard(message);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Display the chat card for an Item as a Chat Message\n * @param {Partial} [message] Configuration info for the created chat message.\n * @returns {Promise}\n */\n async displayCard(message={}) {\n const context = {\n actor: this.actor,\n config: CONFIG.DND5E,\n tokenId: this.actor.token?.uuid || null,\n item: this,\n data: await this.system.getCardData(),\n isSpell: this.type === \"spell\"\n };\n\n const messageConfig = foundry.utils.mergeObject({\n create: message?.createMessage ?? true,\n data: {\n content: await foundry.applications.handlebars.renderTemplate(\n \"systems/dnd5e/templates/chat/item-card.hbs\", context\n ),\n flags: {\n \"dnd5e.item\": { id: this.id, uuid: this.uuid, type: this.type }\n },\n speaker: ChatMessage.getSpeaker({ actor: this.actor, token: this.actor.token }),\n title: this.name\n },\n rollMode: CONFIG.Dice.BasicRoll.getMessageMode()\n }, message);\n\n // Merge in the flags from options\n if ( foundry.utils.getType(message.flags) === \"Object\" ) {\n foundry.utils.mergeObject(messageConfig.data.flags, message.flags);\n delete messageConfig.flags;\n }\n\n /**\n * A hook event that fires before an item chat card is created without using an activity.\n * @function dnd5e.preDisplayCard\n * @memberof hookEvents\n * @param {Item5e} item Item for which the card will be created.\n * @param {ActivityMessageConfiguration} message Configuration for the roll message.\n * @returns {boolean} Return `false` to prevent the card from being displayed.\n */\n if ( Hooks.call(\"dnd5e.preDisplayCard\", this, messageConfig) === false ) return;\n if ( Hooks.call(\"dnd5e.preDisplayCardV2\", this, messageConfig) === false ) return;\n\n ChatMessage.applyRollMode(messageConfig.data, messageConfig.rollMode);\n const card = messageConfig.create === false ? messageConfig.data : await ChatMessage.create(messageConfig.data);\n\n /**\n * A hook event that fires after an item chat card is created.\n * @function dnd5e.displayCard\n * @memberof hookEvents\n * @param {Item5e} item Item for which the chat card is being displayed.\n * @param {ChatMessage5e|object} card The created ChatMessage instance or ChatMessageData depending on whether\n * options.createMessage was set to `true`.\n */\n Hooks.callAll(\"dnd5e.displayCard\", this, card);\n\n return card;\n }\n\n /* -------------------------------------------- */\n /* Chat Cards */\n /* -------------------------------------------- */\n\n /**\n * Prepare an object of chat data used to display a card for the Item in the chat log.\n * @param {object} htmlOptions Options used by the TextEditor.enrichHTML function.\n * @returns {object} An object of chat data to render.\n */\n async getChatData(htmlOptions={}) {\n const context = {};\n let { identified, unidentified, description } = this.system;\n\n // Rich text description\n const isIdentified = identified !== false;\n description = game.user.isGM || isIdentified ? description.value : unidentified?.description;\n context.description = await TextEditor.enrichHTML(description ?? \"\", {\n relativeTo: this,\n rollData: this.getRollData(),\n ...htmlOptions\n });\n\n // Type specific properties\n context.properties = [\n ...this.system.chatProperties ?? [],\n ...this.system.equippableItemCardProperties ?? [],\n ...Object.values(this.labels.activations?.[0] ?? {})\n ].filter(p => p);\n\n return context;\n }\n\n /* -------------------------------------------- */\n /* Item Rolls - Attack, Damage, Saves, Checks */\n /* -------------------------------------------- */\n\n /**\n * Prepare data needed to roll a tool check and then pass it off to `d20Roll`.\n * @param {D20RollConfiguration} [options] Roll configuration options provided to the d20Roll function.\n * @returns {Promise} A Promise which resolves to the created Roll instance.\n */\n async rollToolCheck(options={}) {\n if ( this.type !== \"tool\" ) throw new Error(\"Wrong item type!\");\n return this.actor?.rollToolCheck({\n ability: this.system.ability,\n bonus: this.system.bonus,\n prof: this.system.prof,\n item: this,\n tool: this.system.type.baseItem,\n ...options\n });\n }\n\n /* -------------------------------------------- */\n\n /**\n * @inheritdoc\n * @param {RollDataOptions} [options]\n * @returns {ItemRollData}\n */\n getRollData({ deterministic=false }={}) {\n let data;\n if ( this.system.getRollData ) data = this.system.getRollData({ deterministic });\n else data = { ...(this.actor?.getRollData({ deterministic }) ?? {}), item: { ...this.system } };\n if ( data?.item ) {\n data.item.flags = { ...this.flags };\n data.item.name = this.name;\n }\n data.labels = this.labels;\n data.scaling = new Scaling(this.scalingIncrease);\n return data;\n }\n\n /* -------------------------------------------- */\n /* Chat Message Helpers */\n /* -------------------------------------------- */\n\n /**\n * Apply listeners to chat messages.\n * @param {HTMLElement} html Rendered chat message.\n */\n static chatListeners(html) {\n html.addEventListener(\"click\", event => {\n if ( event.target.closest(\"[data-context-menu]\") ) ContextMenu5e.triggerEvent(event);\n else if ( event.target.closest(\".collapsible\") ) this._onChatCardToggleContent(event);\n });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle toggling the visibility of chat card content when the name is clicked\n * @param {Event} event The originating click event\n * @private\n */\n static _onChatCardToggleContent(event) {\n const header = event.target.closest(\".collapsible\");\n if ( !event.target.closest(\".collapsible-content.card-content\") ) {\n event.preventDefault();\n header.classList.toggle(\"collapsed\");\n\n // Clear the height from the chat popout container so that it appropriately resizes.\n const popout = header.closest(\".chat-popout\");\n if ( popout ) popout.style.height = \"\";\n }\n }\n\n /* -------------------------------------------- */\n /* Activities & Advancements */\n /* -------------------------------------------- */\n\n /**\n * Create a new activity of the specified type.\n * @param {string} type Type of activity to create.\n * @param {object} [data] Data to use when creating the activity.\n * @param {object} [options={}]\n * @param {boolean} [options.renderSheet=true] Should the sheet be rendered after creation?\n * @returns {Promise}\n */\n async createActivity(type, data={}, { renderSheet=true }={}) {\n if ( !this.system.activities ) return;\n\n const config = CONFIG.DND5E.activityTypes[type];\n if ( !config ) throw new Error(`${type} not found in CONFIG.DND5E.activityTypes`);\n const cls = config.documentClass;\n\n const createData = foundry.utils.deepClone(data);\n const activity = new cls({ type, ...data }, { parent: this });\n if ( activity._preCreate(createData) === false ) return;\n\n const sort = this.system.activities.size\n ? Math.max(...this.system.activities.map(a => a.sort)) + CONST.SORT_INTEGER_DENSITY\n : 0;\n await this.update({ [`system.activities.${activity.id}`]: { ...activity.toObject(), sort } });\n const created = this.system.activities.get(activity.id);\n if ( renderSheet ) return created.sheet?.render({ force: true });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Update an activity belonging to this item.\n * @param {string} id ID of the activity to update.\n * @param {object} updates Updates to apply to this activity.\n * @returns {Promise} This item with the changes applied.\n */\n updateActivity(id, updates) {\n if ( !this.system.activities ) return this;\n if ( !this.system.activities.has(id) ) throw new Error(`Activity of ID ${id} could not be found to update`);\n return this.update({ [`system.activities.${id}`]: updates });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Remove an activity from this item.\n * @param {string} id ID of the activity to remove.\n * @returns {Promise} This item with the changes applied.\n */\n async deleteActivity(id) {\n const activity = this.system.activities?.get(id);\n if ( !activity ) return this;\n await Promise.allSettled(activity.constructor._apps.get(activity.uuid)?.map(a => a.close()) ?? []);\n if ( game.release.generation < 14 ) return this.update({ [`system.activities.-=${id}`]: null });\n return this.update({ [`system.activities.${id}`]: _del });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Create a new advancement of the specified type.\n * @param {string} type Type of advancement to create.\n * @param {object} [data] Data to use when creating the advancement.\n * @param {object} [options]\n * @param {boolean} [options.renderSheet] Should the sheet be rendered after creation?\n * @param {boolean} [options.showConfig] Deprecated, use `renderSheet` instead.\n * @param {boolean} [options.source=false] Should a source-only update be performed?\n * @returns {Promise|Item5e} Promise for advancement config for new advancement if source\n * is `false`, or item with newly added advancement.\n */\n createAdvancement(type, data={}, { renderSheet=true, showConfig, source=false }={}) {\n if ( showConfig !== undefined ) {\n foundry.utils.logCompatibilityWarning(\n \"The `showConfig` options in `createAdvancement` has been deprecated and replaced with `renderSheet`.\",\n { since: \"DnD5e 5.2\", until: \"DnD5e 6.0\" }\n );\n renderSheet = showConfig;\n }\n\n if ( !this.system.advancement ) return this;\n\n const config = CONFIG.DND5E.advancementTypes[type];\n if ( !config ) throw new Error(`${type} not found in CONFIG.DND5E.advancementTypes`);\n const cls = config.documentClass;\n\n if ( !config.validItemTypes.has(this.type) || !cls.availableForItem(this) ) {\n throw new Error(`${type} advancement cannot be added to ${this.name}`);\n }\n\n const createData = foundry.utils.deepClone(data);\n const advancement = new cls(data, { parent: this });\n if ( advancement._preCreate(createData) === false ) return;\n\n let update = { [`system.advancement.${advancement.id}`]: advancement.toObject() };\n if ( !source && this._needsAdvancementMigration ) update = {\n \"system.==advancement\": foundry.utils.mergeObject(\n this.system.toObject().advancement, { [advancement.id]: advancement.toObject() }\n )\n };\n if ( source ) return this.updateSource(update);\n return this.update(update).then(() => {\n if ( renderSheet ) return this.system.advancement.get(advancement.id)?.sheet?.render({ force: true });\n return this;\n });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Update an advancement belonging to this item.\n * @param {string} id ID of the advancement to update.\n * @param {object} updates Updates to apply to this advancement.\n * @param {object} [options={}]\n * @param {boolean} [options.source=false] Should a source-only update be performed?\n * @returns {Promise|Item5e} This item with the changes applied, promised if source is `false`.\n */\n updateAdvancement(id, updates, { source=false }={}) {\n if ( !this.system.advancement ) return this;\n if ( !this.system.advancement.has(id) ) throw new Error(`Advancement of ID ${id} could not be found to update`);\n\n const advancement = this.system.advancement.get(id);\n let update = { [`system.advancement.${id}`]: updates };\n if ( !source && this._needsAdvancementMigration ) update = {\n \"system.==advancement\": foundry.utils.mergeObject(\n this.system.toObject().advancement, { [id]: updates }, { performDeletions: true }\n )\n };\n if ( source ) {\n advancement.updateSource(updates);\n advancement.render();\n return this;\n }\n\n return this.update(update).then(() => {\n advancement.render({ height: \"auto\" });\n return this;\n });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Remove an advancement from this item.\n * @param {string} id ID of the advancement to remove.\n * @param {object} [options={}]\n * @param {boolean} [options.source=false] Should a source-only update be performed?\n * @returns {Promise|Item5e} This item with the changes applied.\n */\n deleteAdvancement(id, { source=false }={}) {\n const advancement = this.system.advancement?.get(id);\n if ( !advancement ) return this;\n\n let update = game.release.generation < 14\n ? { [`system.advancement.-=${id}`]: null }\n : { [`system.advancement.${id}`]: _del };\n if ( !source && this._needsAdvancementMigration ) {\n const data = this.system.toObject().advancement;\n delete data[id];\n update = { \"system.==advancement\": data };\n }\n if ( source ) return this.updateSource(update);\n\n return Promise.allSettled(advancement.constructor._apps.get(advancement.uuid)?.map(a => a.close()) ?? [])\n .then(() => this.update(update));\n }\n\n /* -------------------------------------------- */\n\n /**\n * Duplicate an advancement, resetting its value to default and giving it a new ID.\n * @param {string} id ID of the advancement to duplicate.\n * @param {object} [options]\n * @param {boolean} [options.showConfig=true] Should the new advancement's configuration application be shown?\n * @param {boolean} [options.source=false] Should a source-only update be performed?\n * @returns {Promise|Item5e} Promise for advancement config for duplicate advancement if source\n * is `false`, or item with newly duplicated advancement.\n */\n duplicateAdvancement(id, options) {\n const original = this.system.advancement?.get(id);\n if ( !original ) return this;\n const duplicate = original.toObject();\n delete duplicate._id;\n if ( original.constructor.metadata.dataModels?.value ) {\n duplicate.value = (new original.constructor.metadata.dataModels.value()).toObject();\n } else {\n duplicate.value = original.constructor.metadata.defaults?.value ?? {};\n }\n return this.createAdvancement(original.constructor.typeName, duplicate, options);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n getEmbeddedDocument(embeddedName, id, options) {\n let doc;\n switch ( embeddedName ) {\n case \"Activity\": doc = this.system.activities?.get(id); break;\n case \"Advancement\": doc = this.system.advancement?.get(id); break;\n default: return super.getEmbeddedDocument(embeddedName, id, options);\n }\n if ( options?.strict && (doc === undefined) ) {\n throw new Error(`The key ${id} does not exist in the ${embeddedName} Collection`);\n }\n return doc;\n }\n\n /* -------------------------------------------- */\n /* Socket Event Handlers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _preCreate(data, options, user) {\n if ( (await super._preCreate(data, options, user)) === false ) return false;\n\n const isPhysical = this.system.constructor._schemaTemplates?.includes(PhysicalItemTemplate);\n if ( this.parent?.system?.isGroup && !isPhysical ) return false;\n\n // Create identifier based on name\n if ( this.system.hasOwnProperty(\"identifier\") && !data.system?.identifier ) {\n this.updateSource({ \"system.identifier\": this.identifier });\n }\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _onCreate(data, options, userId) {\n super._onCreate(data, options, userId);\n await this.system.onCreateActivities?.(data, options, userId);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _preUpdate(changed, options, user) {\n if ( (await super._preUpdate(changed, options, user)) === false ) return false;\n await this.system.preUpdateActivities?.(changed, options, user);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _onUpdate(changed, options, userId) {\n super._onUpdate(changed, options, userId);\n await this.system.onUpdateActivities?.(changed, options, userId);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _onDelete(options, userId) {\n super._onDelete(options, userId);\n await this.system.onDeleteActivities?.(options, userId);\n if ( game.user.isActiveGM ) this.effects.forEach(e => e.getDependents().forEach(e => e.delete()));\n if ( userId !== game.user.id ) return;\n this.parent?.endConcentration?.(this);\n }\n\n /* -------------------------------------------- */\n /* Event Handlers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async deleteDialog({ sheet, ...dialogOptions }={}, operation={}) {\n // If item has advancement, handle it separately\n if ( this.actor?.system.metadata?.supportsAdvancement && !game.settings.get(\"dnd5e\", \"disableAdvancements\") ) {\n const manager = AdvancementManager.forDeletedItem(this.actor, this.id);\n if ( manager.steps.length ) {\n try {\n const shouldRemoveAdvancements = await AdvancementConfirmationDialog.forDelete(this, { sheet });\n if ( shouldRemoveAdvancements ) {\n if ( sheet ) sheet._renderChild(manager);\n else manager.render({ force: true });\n return;\n }\n return this.delete({ shouldRemoveAdvancements });\n } catch(err) {\n return;\n }\n }\n }\n\n // Display custom delete dialog when deleting a container with contents\n const count = await this.system.contentsCount;\n if ( count ) {\n const type = game.i18n.localize(\"DND5E.Container\");\n const config = foundry.utils.mergeObject({\n window: {\n icon: \"fa-solid fa-trash\",\n title: `${game.i18n.format(\"DOCUMENT.Delete\", { type })}: ${this.name}`\n },\n position: { width: 400 },\n content: `\n \n ${game.i18n.localize(\"AreYouSure\")} \n ${game.i18n.format(\"DND5E.ContainerDeleteMessage\", { count })}\n
\n \n ${game.i18n.localize(\"DND5E.ContainerDeleteContents\")} \n \n \n `,\n yes: { callback: (event, button) => {\n const deleteContents = button.form.elements.deleteContents.checked;\n this.delete({ ...operation, deleteContents });\n }}\n }, dialogOptions);\n if ( sheet ) return sheet._confirmDialog(config);\n return foundry.applications.api.DialogV2.confirm(config);\n }\n\n if ( sheet ) {\n const type = game.i18n.localize(this.constructor.metadata.label);\n return sheet._confirmDialog(foundry.utils.mergeObject({\n window: { title: `${game.i18n.format(\"DOCUMENT.Delete\", { type })}: ${this.name}` },\n position: { width: 400 },\n content: `\n \n ${game.i18n.localize(\"AreYouSure\")} ${game.i18n.format(\"SIDEBAR.DeleteWarning\", { type })}\n
\n `,\n yes: { callback: () => this.delete(operation) }\n }, dialogOptions));\n }\n return super.deleteDialog(dialogOptions, operation);\n }\n\n /* -------------------------------------------- */\n /* Factory Methods */\n /* -------------------------------------------- */\n\n /**\n * Add additional system-specific sidebar directory context menu options for Item documents.\n * @param {ItemDirectory} app The sidebar application.\n * @param {object[]} entryOptions The default array of context menu options.\n */\n static addDirectoryContextOptions(app, entryOptions) {\n entryOptions.push({\n name: \"DND5E.Scroll.CreateScroll\",\n icon: ' ',\n callback: async li => {\n let spell = game.items.get(li.dataset.entryId);\n if ( app.collection instanceof foundry.documents.collections.CompendiumCollection ) {\n spell = await app.collection.getDocument(li.dataset.entryId);\n }\n const scroll = await Item5e.createScrollFromSpell(spell);\n if ( scroll ) Item5e.create(scroll);\n },\n condition: li => {\n let item = game.items.get(li.dataset.documentId ?? li.dataset.entryId);\n if ( app.collection instanceof foundry.documents.collections.CompendiumCollection ) {\n item = app.collection.index.get(li.dataset.entryId);\n }\n return (item.type === \"spell\") && game.user.hasPermission(\"ITEM_CREATE\");\n },\n group: \"system\"\n });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare creation data for the provided items and any items contained within them. The data created by this method\n * can be passed to `createDocuments` with `keepId` always set to true to maintain links to container contents.\n * @param {Item5e[]} items Items to create.\n * @param {object} [context={}] Context for the item's creation.\n * @param {Item5e} [context.container] Container in which to create the item.\n * @param {boolean} [context.keepId=false] Should IDs be maintained?\n * @param {ItemContentsTransformer} [context.transformAll] Method called on provided items and their contents.\n * @param {ItemContentsTransformer} [context.transformFirst] Method called only on provided items.\n * @returns {Promise} Data for items to be created.\n */\n static async createWithContents(items, { container, keepId=false, transformAll, transformFirst }={}) {\n let initialDepth = 0;\n if ( container ) {\n initialDepth = 1 + (await container.system.allContainers()).length;\n if ( initialDepth > PhysicalItemTemplate.MAX_DEPTH ) {\n ui.notifications.warn(game.i18n.format(\"DND5E.ContainerMaxDepth\", { depth: PhysicalItemTemplate.MAX_DEPTH }));\n return;\n }\n }\n\n const createItemData = async (item, containerId, depth) => {\n const o = { container: containerId, depth };\n let newItemData = transformAll ? await transformAll(item, o) : item;\n if ( transformFirst && (depth === initialDepth) ) newItemData = await transformFirst(newItemData, o);\n if ( !newItemData ) return;\n if ( newItemData instanceof Item ) newItemData = game.items.fromCompendium(newItemData, {\n clearSort: false, keepId: true, clearOwnership: false\n });\n foundry.utils.mergeObject(newItemData, {\"system.container\": containerId} );\n if ( !keepId ) newItemData._id = foundry.utils.randomID();\n\n created.push(newItemData);\n\n const contents = await item.system.contents;\n if ( contents && (depth < PhysicalItemTemplate.MAX_DEPTH) ) {\n for ( const doc of contents ) await createItemData(doc, newItemData._id, depth + 1);\n }\n };\n\n const created = [];\n for ( const item of items ) await createItemData(item, container?.id, initialDepth);\n return created;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Create a consumable spell scroll Item from a spell Item.\n * @param {Item5e|object} spell The spell or item data to be made into a scroll.\n * @param {object} [options] Additional options that modify the created scroll.\n * @param {SpellScrollConfiguration} [config={}] Configuration options for scroll creation.\n * @returns {Promise} The created scroll consumable item.\n */\n static async createScrollFromSpell(spell, options={}, config={}) {\n if ( spell.pack ) return this.createScrollFromCompendiumSpell(spell.uuid, config);\n\n const values = {};\n if ( (spell instanceof Item5e) && spell.isOwned && (dnd5e.settings.rulesVersion === \"modern\") ) {\n const spellcastingClass = spell.actor.spellcastingClasses?.[spell.system.classIdentifier];\n if ( spellcastingClass ) {\n values.bonus = spellcastingClass.spellcasting.attack;\n values.dc = spellcastingClass.spellcasting.save;\n } else {\n values.bonus = spell.actor.system.attributes?.spell?.mod;\n values.dc = spell.actor.system.attributes?.spell?.dc;\n }\n }\n\n config = foundry.utils.mergeObject({\n explanation: game.user.getFlag(\"dnd5e\", \"creation.scrollExplanation\") ?? \"reference\",\n level: spell.system.level,\n values\n }, config);\n\n if ( config.dialog !== false ) {\n const result = await CreateScrollDialog.create(spell, config);\n if ( !result ) return;\n foundry.utils.mergeObject(config, result);\n await game.user.setFlag(\"dnd5e\", \"creation.scrollExplanation\", config.explanation);\n }\n\n // Get spell data\n const itemData = (spell instanceof Item5e) ? spell.toObject() : spell;\n const flags = itemData.flags ?? {};\n if ( Number.isNumeric(config.level) ) {\n flags.dnd5e ??= {};\n flags.dnd5e.scaling = Math.max(0, config.level - spell.system.level);\n flags.dnd5e.spellLevel = {\n value: config.level,\n base: spell.system.level\n };\n itemData.system.level = config.level;\n }\n\n /**\n * A hook event that fires before the item data for a scroll is created.\n * @function dnd5e.preCreateScrollFromSpell\n * @memberof hookEvents\n * @param {object} itemData The initial item data of the spell to convert to a scroll.\n * @param {object} options Additional options that modify the created scroll.\n * @param {SpellScrollConfiguration} config Configuration options for scroll creation.\n * @returns {boolean} Explicitly return false to prevent the scroll to be created.\n */\n if ( Hooks.call(\"dnd5e.preCreateScrollFromSpell\", itemData, options, config) === false ) return;\n\n let { activities, level, properties, source } = itemData.system;\n\n // Get scroll data\n let scrollUuid;\n const id = CONFIG.DND5E.spellScrollIds[level];\n if ( foundry.data.validators.isValidId(id) ) {\n scrollUuid = game.packs.get(CONFIG.DND5E.sourcePacks.ITEMS).index.get(id).uuid;\n } else {\n scrollUuid = id;\n }\n const scrollItem = await fromUuid(scrollUuid);\n const scrollData = game.items.fromCompendium(scrollItem);\n\n // Create a composite description from the scroll description and the spell details\n const desc = this._createScrollDescription(scrollItem, itemData, null, config);\n\n for ( const level of Array.fromRange(itemData.system.level + 1).reverse() ) {\n const values = CONFIG.DND5E.spellScrollValues[level];\n if ( values ) {\n config.values.bonus ??= values.bonus;\n config.values.dc ??= values.dc;\n break;\n }\n }\n\n // Apply inferred spell activation, duration, range, and target data to activities\n for ( const activity of Object.values(activities) ) {\n for ( const key of [\"activation\", \"duration\", \"range\", \"target\"] ) {\n if ( activity[key]?.override !== false ) continue;\n activity[key].override = true;\n foundry.utils.mergeObject(activity[key], itemData.system[key]);\n }\n activity.consumption.targets.push({ type: \"itemUses\", target: \"\", value: \"1\" });\n if ( activity.type === \"attack\" ) {\n activity.attack.flat = true;\n activity.attack.bonus = values.bonus;\n } else if ( activity.type === \"save\" ) {\n activity.save.dc.calculation = \"\";\n activity.save.dc.formula = values.dc;\n }\n }\n\n // Create the spell scroll data\n const spellScrollData = foundry.utils.mergeObject(scrollData, {\n name: `${game.i18n.localize(\"DND5E.SpellScroll\")}: ${itemData.name}`,\n effects: itemData.effects ?? [],\n flags,\n system: {\n activities, description: { value: desc.trim() }, properties, source\n }\n });\n foundry.utils.mergeObject(spellScrollData, options);\n spellScrollData.system.properties = [\n \"mgc\",\n ...scrollData.system.properties,\n ...properties ?? [],\n ...options.system?.properties ?? []\n ];\n\n /**\n * A hook event that fires after the item data for a scroll is created but before the item is returned.\n * @function dnd5e.createScrollFromSpell\n * @memberof hookEvents\n * @param {Item5e|object} spell The spell or item data to be made into a scroll.\n * @param {object} spellScrollData The final item data used to make the scroll.\n * @param {SpellScrollConfiguration} config Configuration options for scroll creation.\n */\n Hooks.callAll(\"dnd5e.createScrollFromSpell\", spell, spellScrollData, config);\n\n return new this(spellScrollData);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Create a consumable spell scroll Item from a spell Item.\n * @param {string} uuid UUID of the spell to add to the scroll.\n * @param {SpellScrollConfiguration} [config={}] Configuration options for scroll creation.\n * @returns {Promise} The created scroll consumable item.\n */\n static async createScrollFromCompendiumSpell(uuid, config={}) {\n const spell = await fromUuid(uuid);\n if ( !spell ) return;\n\n const values = {};\n\n config = foundry.utils.mergeObject({\n explanation: game.user.getFlag(\"dnd5e\", \"creation.scrollExplanation\") ?? \"reference\",\n level: spell.system.level,\n values\n }, config);\n\n if ( config.dialog !== false ) {\n const result = await CreateScrollDialog.create(spell, config);\n if ( !result ) return;\n foundry.utils.mergeObject(config, result);\n await game.user.setFlag(\"dnd5e\", \"creation.scrollExplanation\", config.explanation);\n }\n\n /**\n * A hook event that fires before the item data for a scroll is created for a compendium spell.\n * @function dnd5e.preCreateScrollFromCompendiumSpell\n * @memberof hookEvents\n * @param {Item5e} spell Spell to add to the scroll.\n * @param {SpellScrollConfiguration} config Configuration options for scroll creation.\n * @returns {boolean} Explicitly return `false` to prevent the scroll to be created.\n */\n if ( Hooks.call(\"dnd5e.preCreateScrollFromCompendiumSpell\", spell, config) === false ) return;\n\n // Get scroll data\n let scrollUuid;\n const id = CONFIG.DND5E.spellScrollIds[spell.system.level];\n if ( foundry.data.validators.isValidId(id) ) {\n scrollUuid = game.packs.get(CONFIG.DND5E.sourcePacks.ITEMS).index.get(id).uuid;\n } else {\n scrollUuid = id;\n }\n const scrollItem = await fromUuid(scrollUuid);\n const scrollData = game.items.fromCompendium(scrollItem);\n\n for ( const level of Array.fromRange(spell.system.level + 1).reverse() ) {\n const values = CONFIG.DND5E.spellScrollValues[level];\n if ( values ) {\n config.values.bonus ??= values.bonus;\n config.values.dc ??= values.dc;\n break;\n }\n }\n\n const activity = {\n _id: staticID(\"dnd5escrollspell\"),\n type: \"cast\",\n consumption: {\n targets: [{ type: \"itemUses\", value: \"1\" }]\n },\n spell: {\n challenge: {\n attack: config.values.bonus,\n save: config.values.dc,\n override: true\n },\n level: config.level,\n uuid\n }\n };\n\n // Create the spell scroll data\n const spellScrollData = foundry.utils.mergeObject(scrollData, {\n name: `${game.i18n.localize(\"DND5E.SpellScroll\")}: ${spell.name}`,\n system: {\n activities: { ...(scrollData.system.activities ?? {}), [activity._id]: activity },\n description: {\n value: this._createScrollDescription(scrollItem, spell, `@Embed[${uuid} inline]
`, config).trim()\n }\n }\n });\n\n /**\n * A hook event that fires after the item data for a scroll is created but before the item is returned.\n * @function dnd5e.createScrollFromSpell\n * @memberof hookEvents\n * @param {Item5e} spell The spell or item data to be made into a scroll.\n * @param {object} spellScrollData The final item data used to make the scroll.\n * @param {SpellScrollConfiguration} config Configuration options for scroll creation.\n */\n Hooks.callAll(\"dnd5e.createScrollFromSpell\", spell, spellScrollData, config);\n\n return new this(spellScrollData);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Create the description for a spell scroll.\n * @param {Item5e} scroll Base spell scroll.\n * @param {Item5e|object} spell Spell being added to the scroll.\n * @param {string} [spellDescription] Description from the spell being added.\n * @param {SpellScrollConfiguration} [config={}] Configuration options for scroll creation.\n * @returns {string}\n * @protected\n */\n static _createScrollDescription(scroll, spell, spellDescription, config={}) {\n spellDescription ??= spell.system.description.value;\n const isConc = spell.system.properties[spell instanceof Item5e ? \"has\" : \"includes\"](\"concentration\");\n const level = spell.system.level;\n switch ( config.explanation ) {\n case \"full\":\n // Split the scroll description into an intro paragraph and the remaining details\n const scrollDescription = scroll.system.description.value;\n const pdel = \"
\";\n const scrollIntroEnd = scrollDescription.indexOf(pdel);\n const scrollIntro = scrollDescription.slice(0, scrollIntroEnd + pdel.length);\n const scrollDetails = scrollDescription.slice(scrollIntroEnd + pdel.length);\n return [\n scrollDetails ? scrollIntro : null,\n `${spell.name} (${game.i18n.format(\"DND5E.LevelNumber\", { level })}) `,\n isConc ? `${game.i18n.localize(\"DND5E.Scroll.RequiresConcentration\")}
` : null,\n spellDescription,\n `${game.i18n.localize(\"DND5E.Scroll.Details\")} `,\n scrollDetails || scrollIntro\n ].filterJoin(\"\");\n case \"reference\":\n return [\n \"\",\n CONFIG.DND5E.spellLevels[level] ?? level,\n \" &Reference[Spell Scroll]\",\n isConc ? `, ${game.i18n.localize(\"DND5E.Scroll.RequiresConcentration\")}` : null,\n \"
\",\n spellDescription\n ].filterJoin(\"\");\n }\n return spellDescription;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n static async createDialog(data={}, createOptions={}, dialogOptions={}) {\n CreateDocumentDialog.migrateOptions(createOptions, dialogOptions);\n return CreateDocumentDialog.prompt(this, data, createOptions, dialogOptions);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare default list of types if none are specified.\n * @param {Actor5e} parent Parent document within which this Item will be created.\n * @returns {string[]}\n * @protected\n */\n static _createDialogTypes(parent) {\n return this.TYPES.filter(t => t !== \"backpack\");\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static getDefaultArtwork(itemData={}) {\n const { type } = itemData;\n const { img } = super.getDefaultArtwork(itemData);\n return { img: CONFIG.DND5E.defaultArtwork.Item[type] ?? img };\n }\n}\n","import CreateDocumentDialog from \"../applications/create-document-dialog.mjs\";\nimport FormulaField from \"../data/fields/formula-field.mjs\";\nimport MappingField from \"../data/fields/mapping-field.mjs\";\nimport { parseOrString, staticID } from \"../utils.mjs\";\nimport Item5e from \"./item.mjs\";\nimport DependentDocumentMixin from \"./mixins/dependent.mjs\";\n\nconst TextEditor = foundry.applications.ux.TextEditor.implementation;\nconst { ObjectField, SchemaField, SetField, StringField } = foundry.data.fields;\n\n/**\n * @import { FavoriteData5e } from \"../data/abstract/_types.mjs\";\n */\n\n/**\n * Extend the base ActiveEffect class to implement system-specific logic.\n */\nexport default class ActiveEffect5e extends DependentDocumentMixin(ActiveEffect) {\n\n /**\n * The default icon used for newly created Active Effect documents.\n * @type {string}\n */\n static DEFAULT_ICON = \"systems/dnd5e/icons/svg/documents/active-effect.svg\";\n\n /* -------------------------------------------- */\n\n /**\n * Static ActiveEffect ID for various conditions.\n * @type {Record}\n */\n static ID = {\n BLOODIED: staticID(\"dnd5ebloodied\"),\n ENCUMBERED: staticID(\"dnd5eencumbered\"),\n EXHAUSTION: staticID(\"dnd5eexhaustion\")\n };\n\n /* -------------------------------------------- */\n\n /**\n * Additional key paths to properties added during base data preparation that should be treated as formula fields.\n * @type {Set}\n */\n static FORMULA_FIELDS = new Set([\n \"system.attributes.ac.bonus\",\n \"system.attributes.ac.min\",\n \"system.attributes.encumbrance.bonuses.encumbered\",\n \"system.attributes.encumbrance.bonuses.heavilyEncumbered\",\n \"system.attributes.encumbrance.bonuses.maximum\",\n \"system.attributes.encumbrance.bonuses.overall\",\n \"system.attributes.encumbrance.multipliers.encumbered\",\n \"system.attributes.encumbrance.multipliers.heavilyEncumbered\",\n \"system.attributes.encumbrance.multipliers.maximum\",\n \"system.attributes.encumbrance.multipliers.overall\",\n \"system.damageBonus\",\n \"save.dc.bonus\"\n ]);\n\n /* -------------------------------------------- */\n\n /**\n * Active effect fields that should be redirected to another field, optionally with a compatibility warning.\n * Optional warning object contains options passed to `foundry.utils.logCompatibilityWarning`.\n * @type {Record}\n */\n static SHIM_FIELDS = {\n \"system.attributes.movement.speed\": { key: \"system.attributes.movement.walk\" },\n \"system.attributes.senses.darkvision\": { key: \"system.attributes.senses.ranges.darkvision\" },\n \"system.attributes.senses.blindsight\": { key: \"system.attributes.senses.ranges.blindsight\" },\n \"system.attributes.senses.tremorsense\": { key: \"system.attributes.senses.ranges.tremorsense\" },\n \"system.attributes.senses.truesight\": { key: \"system.attributes.senses.ranges.truesight\" }\n };\n\n /* -------------------------------------------- */\n\n /** @inheritdoc */\n static LOCALIZATION_PREFIXES = [...super.LOCALIZATION_PREFIXES, \"DND5E.ACTIVEEFFECT\"];\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /**\n * Another effect that granted this effect as a rider.\n * @type {ActiveEffect5e|null}\n */\n get dependentOrigin() {\n if ( !(this.parent instanceof Item) ) return null;\n return this.parent.effects.get(this.flags.dnd5e?.dependentOn) ?? null;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Is this effect an enchantment on an item that accepts enchantment?\n * @type {boolean}\n */\n get isAppliedEnchantment() {\n return (this.type === \"enchantment\") && !!this.origin && (this.origin !== this.parent.uuid);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Should this status effect be hidden from the current user?\n * @type {boolean}\n */\n get isConcealed() {\n if ( this.target?.testUserPermission(game.user, \"OBSERVER\") ) return false;\n\n // Hide bloodied status effect from players unless the token is friendly\n if ( (this.id === this.constructor.ID.BLOODIED) && (game.settings.get(\"dnd5e\", \"bloodied\") === \"player\") ) {\n return this.target?.token?.disposition !== foundry.CONST.TOKEN_DISPOSITIONS.FRIENDLY;\n }\n\n return false;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n get isSuppressed() {\n if ( super.isSuppressed ) return true;\n if ( this.type === \"enchantment\" ) return false;\n if ( this.parent instanceof dnd5e.documents.Item5e ) {\n if ( this.parent.areEffectsSuppressed ) return true;\n if ( this.dependentOrigin?.active === false ) return true;\n }\n return false;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n get isTemporary() {\n return !this.isConcealed && (super.isTemporary || this.getFlag(\"dnd5e\", \"isTemporary\"));\n }\n\n /* -------------------------------------------- */\n\n /**\n * Retrieve the source Actor or Item, or null if it could not be determined.\n * @returns {Promise}\n */\n async getSource() {\n if ( (this.target instanceof dnd5e.documents.Actor5e) && (this.parent instanceof dnd5e.documents.Item5e) ) {\n return this.parent;\n }\n return fromUuid(this.origin);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static async _fromStatusEffect(statusId, { reference, ...effectData }, options) {\n if ( !(\"description\" in effectData) && reference ) effectData.description = `@Embed[${reference} inline]`;\n return super._fromStatusEffect?.(statusId, effectData, options) ?? new this(effectData, options);\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _initializeSource(data, options={}) {\n if ( data instanceof foundry.abstract.DataModel ) data = data.toObject();\n\n if ( data.flags?.dnd5e?.type === \"enchantment\" ) {\n data.type = \"enchantment\";\n delete data.flags.dnd5e.type;\n foundry.utils.setProperty(data, \"flags.dnd5e.persistSourceMigration\", true);\n }\n\n return super._initializeSource(data, options);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static migrateData(data) {\n data = super.migrateData(data);\n for ( const change of data.changes ?? [] ) {\n if ( change.key === \"flags.dnd5e.initiativeAdv\" ) {\n change.key = \"system.attributes.init.roll.mode\";\n change.mode = CONST.ACTIVE_EFFECT_MODES.ADD;\n change.value = 1;\n }\n }\n return data;\n }\n\n /* -------------------------------------------- */\n /* Effect Application */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n apply(doc, change) {\n // Apply shims to moved fields\n change = this._applyChangeShim(change);\n\n // Ensure changes targeting flags use the proper types\n if ( change.key.startsWith(\"flags.dnd5e.\") ) change = this._prepareFlagChange(doc, change);\n\n // Properly handle formulas that don't exist as part of the data model\n if ( ActiveEffect5e.FORMULA_FIELDS.has(change.key) ) {\n const field = new FormulaField({ deterministic: change.key !== \"system.damageBonus\" });\n return { [change.key]: game.release.generation < 14\n ? this.constructor.applyField(doc, change, field)\n : this.constructor.applyChangeField(doc, change, { field }) };\n }\n\n // Handle activity-targeted changes\n if ( (change.key.startsWith(\"activities[\") || change.key.startsWith(\"system.activities.\"))\n && (doc instanceof Item) ) return this.applyActivity(doc, change);\n\n return super.apply(doc, change);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static applyChange(model, change, options={}) {\n change = change.effect._applyChangeShim(change);\n if ( change.key.startsWith(\"flags.dnd5e.\") ) change = change.effect._prepareFlagChange(model, change);\n if ( ActiveEffect5e.FORMULA_FIELDS.has(change.key) ) {\n const field = new FormulaField({ deterministic: change.key !== \"system.damageBonus\" });\n return { [change.key]: this.applyChangeField(model, change, { field }) };\n }\n if ( (change.key.startsWith(\"activities[\") || change.key.startsWith(\"system.activities.\"))\n && (model instanceof Item) ) return change.effect.applyActivity(model, change);\n return super.applyChange(model, change, options);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Apply a change to activities on this item.\n * @param {Item5e} item The Item to whom this change should be applied.\n * @param {EffectChangeData} change The change data being applied.\n * @returns {Record} An object of property paths and their updated values.\n */\n applyActivity(item, change) {\n const changes = {};\n const apply = (activity, key) => {\n const c = (game.release.generation > 13)\n ? this.constructor.applyChange(activity, { ...change, key })\n : this.apply(activity, { ...change, key });\n Object.entries(c).forEach(([k, v]) => changes[`system.activities.${activity.id}.${k}`] = v);\n };\n if ( change.key.startsWith(\"system.activities.\") ) {\n const [, , id, ...keyPath] = change.key.split(\".\");\n const activity = item.system.activities?.get(id);\n if ( activity ) apply(activity, keyPath.join(\".\"));\n } else {\n const { type, key } = change.key.match(/activities\\[(?[^\\]]+)]\\.(?.+)/)?.groups ?? {};\n item.system.activities?.getByType(type)?.forEach(activity => apply(activity, key));\n }\n return changes;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static applyChangeField(model, change, options={}) {\n const current = foundry.utils.getProperty(model, change.key);\n const { field } = options;\n\n // Replace value when using string interpolation syntax\n if ( (field instanceof StringField) && (change.type === \"override\") && change.value?.includes?.(\"{}\") ) {\n change.value = change.value.replace(\"{}\", current ?? \"\");\n }\n\n // If current value is `null`, UPGRADE & DOWNGRADE should always just set the value\n if ( (current === null) && [\"upgrade\", \"downgrade\"].includes(change.type) ) change.type = \"override\";\n\n // Handle removing entries from sets\n if ( (field instanceof SetField) && (change.type === \"add\") && (foundry.utils.getType(current) === \"Set\") ) {\n for ( const value of field._castChangeDelta(change.value) ) {\n const neg = value.replace(/^\\s*-\\s*/, \"\");\n if ( neg !== value ) current.delete(neg);\n else current.add(value);\n }\n return current;\n }\n\n // If attempting to apply active effect to empty MappingField entry, create it\n if ( (current === undefined) && change.key.startsWith(\"system.\") ) {\n let keyPath = change.key;\n let mappingField = field;\n while ( !(mappingField instanceof MappingField) && mappingField ) {\n if ( mappingField.name ) keyPath = keyPath.substring(0, keyPath.length - mappingField.name.length - 1);\n mappingField = mappingField.parent;\n }\n if ( mappingField && (foundry.utils.getProperty(model, keyPath) === undefined) ) {\n const created = mappingField.model.initialize(mappingField.model.getInitialValue(), mappingField);\n foundry.utils.setProperty(model, keyPath, created);\n }\n }\n\n // Parse any JSON provided when targeting an object\n if ( (field instanceof ObjectField) || (field instanceof SchemaField) ) {\n change = { ...change, value: parseOrString(change.value) };\n }\n\n return super.applyChangeField(model, change, options);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static _applyChangeAdd(actor, change, current, delta, changes) {\n if ( current instanceof Set ) {\n const handle = v => {\n const neg = v.replace(/^\\s*-\\s*/, \"\");\n if ( neg !== v ) current.delete(neg);\n else current.add(v);\n };\n if ( Array.isArray(delta) ) delta.forEach(item => handle(item));\n else if ( delta instanceof Set ) {\n for ( const item of delta ) handle(item);\n }\n else handle(delta);\n return;\n }\n super._applyChangeAdd(actor, change, current, delta, changes);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Modify the provided change according to a shim an emit a warning if required.\n * @param {EffectChangeData} change The change being applied.\n * @returns {EffectChangeData}\n * @protected\n */\n _applyChangeShim(change) {\n const shim = ActiveEffect5e.SHIM_FIELDS[change.key];\n if ( !shim ) return change;\n if ( shim.warning ) foundry.utils.logCompatibilityWarning(\n `The active effect key \"${change.key}\" has been deprecated and should be changed to \"${shim.key}\".`,\n shim.warning\n );\n return { ...change, key: shim.key };\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static _applyChangeUnguided(actor, change, changes, { replacementData }={}) {\n if ( change.effect.system._applyLegacy?.(actor, change, changes) === false ) return;\n\n // Double-check whether the target should be treated as a formula if the key has been modified\n if ( ActiveEffect5e.FORMULA_FIELDS.has(change.key) ) {\n const field = new FormulaField({ deterministic: change.key !== \"system.damageBonus\" });\n return { [change.key]: game.release.generation < 14\n ? this.applyField(actor, change, field)\n : this.applyChangeField(actor, change, { field }) };\n }\n\n super._applyChangeUnguided(actor, change, changes, { replacementData });\n }\n\n /* --------------------------------------------- */\n\n /** @inheritDoc */\n static _applyChangeUpgrade(actor, change, current, delta, changes) {\n if ( current === null ) return this._applyChangeOverride(actor, change, current, delta, changes);\n return super._applyChangeUpgrade(actor, change, current, delta, changes);\n }\n\n /* --------------------------------------------- */\n\n /**\n * Transform the data type of the change to match the type expected for flags.\n * @param {Actor5e} actor The Actor to whom this effect should be applied.\n * @param {EffectChangeData} change The change being applied.\n * @returns {EffectChangeData} The change with altered types if necessary.\n */\n _prepareFlagChange(actor, change) {\n const { key, value } = change;\n const data = CONFIG.DND5E.characterFlags[key.replace(\"flags.dnd5e.\", \"\")];\n if ( !data ) return change;\n\n // Set flag to initial value if it isn't present\n const current = foundry.utils.getProperty(actor, key) ?? null;\n if ( current === null ) {\n let initialValue = null;\n if ( data.placeholder ) initialValue = data.placeholder;\n else if ( data.type === Boolean ) initialValue = false;\n else if ( data.type === Number ) initialValue = 0;\n foundry.utils.setProperty(actor, key, initialValue);\n }\n\n // Coerce change data into the correct type\n if ( data.type === Boolean ) {\n if ( value === \"false\" ) change.value = false;\n else change.value = Boolean(value);\n }\n return change;\n }\n\n /* -------------------------------------------- */\n /* Lifecycle */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareBaseData() {\n this.origin = this.getFlag(\"core\", \"originText\") ?? this.origin;\n super.prepareBaseData();\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareDerivedData() {\n super.prepareDerivedData();\n if ( this.id === this.constructor.ID.EXHAUSTION ) this._prepareExhaustionLevel();\n if ( this.isAppliedEnchantment && this.uuid ) dnd5e.registry.enchantments.track(this.origin, this.uuid);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Modify the ActiveEffect's attributes based on the exhaustion level.\n * @protected\n */\n _prepareExhaustionLevel() {\n const config = CONFIG.DND5E.conditionTypes.exhaustion;\n let level = this.getFlag(\"dnd5e\", \"exhaustionLevel\");\n if ( !Number.isFinite(level) ) level = 1;\n this.img = this.constructor._getExhaustionImage(level);\n this.name = `${game.i18n.localize(\"DND5E.Exhaustion\")} ${level}`;\n if ( level >= config.levels ) {\n this.statuses.add(\"dead\");\n CONFIG.DND5E.statusEffects.dead.statuses?.forEach(s => this.statuses.add(s));\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare effect favorite data.\n * @returns {Promise}\n */\n async getFavoriteData() {\n return {\n img: this.img,\n title: this.name,\n subtitle: this.duration.remaining ? this.duration.label : \"\",\n toggle: !this.disabled,\n suppressed: this.isSuppressed\n };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Create conditions that are applied separately from an effect.\n * @returns {Promise} Created rider effects.\n */\n async createRiderConditions() {\n const riders = new Set();\n\n for ( const status of this.getFlag(\"dnd5e\", \"riders.statuses\") ?? [] ) {\n riders.add(status);\n }\n\n for ( const status of this.statuses ) {\n const r = CONFIG.statusEffects.find(e => e.id === status)?.riders ?? [];\n for ( const p of r ) riders.add(p);\n }\n\n if ( !riders.size ) return [];\n\n const createRider = async id => {\n const existing = this.parent.effects.get(staticID(`dnd5e${id}`));\n if ( existing ) return;\n const effect = await ActiveEffect5e.fromStatusEffect(id);\n return effect.toObject();\n };\n\n const effectData = await Promise.all(Array.from(riders).map(createRider));\n return ActiveEffect5e.createDocuments(effectData.filter(_ => _), { keepId: true, parent: this.parent });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Create additional activities, effects, and items that are applied separately from an enchantment.\n * @param {object} options Options passed to the effect creation.\n */\n async createRiderEnchantments(options={}) {\n let item;\n let profile;\n const { chatMessageOrigin } = options;\n const { enchantmentProfile, activityId } = options.dnd5e ?? {};\n\n if ( chatMessageOrigin ) {\n const message = game.messages.get(options?.chatMessageOrigin);\n item = message?.getAssociatedItem();\n const activity = message?.getAssociatedActivity();\n profile = activity?.effects.find(e => e._id === message?.getFlag(\"dnd5e\", \"use.enchantmentProfile\"));\n } else if ( enchantmentProfile && activityId ) {\n let activity;\n const origin = await fromUuid(this.origin);\n if ( origin instanceof dnd5e.documents.activity.EnchantActivity ) {\n activity = origin;\n item = activity.item;\n } else if ( origin instanceof Item ) {\n item = origin;\n activity = item.system.activities?.get(activityId);\n }\n profile = activity?.effects.find(e => e._id === enchantmentProfile);\n }\n\n if ( !profile || !item ) return;\n\n // Create Activities\n const riderActivities = {};\n let riderEffects = [];\n for ( const id of profile.riders.activity ) {\n const activityData = item.system.activities.get(id)?.toObject();\n if ( !activityData ) continue;\n activityData._id = foundry.utils.randomID();\n foundry.utils.setProperty(activityData, \"flags.dnd5e.dependentOn\", this.id);\n riderActivities[activityData._id] = activityData;\n }\n let createdActivities = [];\n if ( !foundry.utils.isEmpty(riderActivities) ) {\n await this.parent.update({ \"system.activities\": riderActivities });\n createdActivities = Object.keys(riderActivities).map(id => this.parent.system.activities?.get(id));\n createdActivities.forEach(a => a.effects?.forEach(e => {\n if ( !this.parent.effects.has(e._id) ) riderEffects.push(item.effects.get(e._id)?.toObject());\n }));\n }\n\n // Create Effects\n riderEffects.push(...profile.riders.effect.map(id => {\n const effectData = item.effects.get(id)?.toObject();\n if ( effectData ) {\n delete effectData._id;\n delete effectData.flags?.dnd5e?.rider;\n effectData.origin = this.origin;\n }\n return effectData;\n }));\n riderEffects = riderEffects.filter(_ => _);\n riderEffects.forEach(e => foundry.utils.setProperty(e, \"flags.dnd5e.dependentOn\", this.id));\n await this.parent.createEmbeddedDocuments(\"ActiveEffect\", riderEffects, { keepId: true });\n\n // Create Items\n if ( this.parent.isEmbedded ) {\n const riderItems = await Item5e.createWithContents(\n (await Promise.all(profile.riders.item.map(uuid => fromUuid(uuid)))).filter(_ => _), {\n transformAll: item => {\n const itemData = item.clone({}, { keepId: true }).toObject();\n foundry.utils.setProperty(itemData, \"flags.dnd5e.dependentOn\", this.uuid);\n foundry.utils.setProperty(itemData, \"flags.dnd5e.enchantment.origin\", this.uuid);\n return itemData;\n }\n }\n );\n await this.parent.actor.createEmbeddedDocuments(\"Item\", riderItems, { keepId: true });\n }\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n toDragData() {\n const data = super.toDragData();\n const activity = this.parent?.system.activities?.getByType(\"enchant\").find(a => {\n return a.effects.some(e => e._id === this.id);\n });\n if ( activity ) data.activityId = activity.id;\n return data;\n }\n\n /* -------------------------------------------- */\n /* Socket Event Handlers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _preCreate(data, options, user) {\n if ( await super._preCreate(data, options, user) === false ) return false;\n if ( options.keepOrigin === false ) this.updateSource({ origin: this.parent.uuid });\n\n // Enchantments cannot be added directly to actors\n if ( (this.type === \"enchantment\") && (this.parent instanceof Actor) ) {\n ui.notifications.error(\"DND5E.ENCHANTMENT.Warning.NotOnActor\", { localize: true });\n return false;\n }\n\n if ( this.isAppliedEnchantment ) {\n const origin = await fromUuid(this.origin);\n const errors = origin?.canEnchant?.(this.parent);\n if ( errors?.length ) {\n errors.forEach(err => console.error(err));\n return false;\n }\n this.updateSource({ disabled: false });\n }\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _onCreate(data, options, userId) {\n super._onCreate(data, options, userId);\n if ( userId === game.userId ) {\n if ( this.active && (this.parent instanceof Actor) ) await this.createRiderConditions();\n if ( this.isAppliedEnchantment ) await this.createRiderEnchantments(options);\n }\n if ( options.chatMessageOrigin ) {\n document.body.querySelectorAll(`[data-message-id=\"${options.chatMessageOrigin}\"] enchantment-application`)\n .forEach(element => element.buildItemList());\n }\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _onUpdate(data, options, userId) {\n super._onUpdate(data, options, userId);\n const originalLevel = foundry.utils.getProperty(options, \"dnd5e.originalExhaustion\");\n const newLevel = foundry.utils.getProperty(data, \"flags.dnd5e.exhaustionLevel\");\n const originalEncumbrance = foundry.utils.getProperty(options, \"dnd5e.originalEncumbrance\");\n const newEncumbrance = data.statuses?.[0];\n const name = this.name;\n\n // Display proper scrolling status effects for exhaustion\n if ( (this.id === this.constructor.ID.EXHAUSTION) && Number.isFinite(newLevel) && Number.isFinite(originalLevel) ) {\n if ( newLevel === originalLevel ) return;\n // Temporarily set the name for the benefit of _displayScrollingTextStatus. We should improve this method to\n // accept a name parameter instead.\n if ( newLevel < originalLevel ) this.name = `Exhaustion ${originalLevel}`;\n this._displayScrollingStatus(newLevel > originalLevel);\n this.name = name;\n }\n\n // Display proper scrolling status effects for encumbrance\n else if ( (this.id === this.constructor.ID.ENCUMBERED) && originalEncumbrance && newEncumbrance ) {\n if ( newEncumbrance === originalEncumbrance ) return;\n const increase = !originalEncumbrance || ((originalEncumbrance === \"encumbered\") && newEncumbrance)\n || (newEncumbrance === \"exceedingCarryingCapacity\");\n if ( !increase ) this.name = CONFIG.DND5E.encumbrance.effects[originalEncumbrance].name;\n this._displayScrollingStatus(increase);\n this.name = name;\n }\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _preDelete(options, user) {\n const dependents = this.getDependents();\n if ( dependents.length && !game.users.activeGM ) {\n ui.notifications.warn(\"DND5E.ConcentrationBreakWarning\", { localize: true });\n return false;\n }\n return super._preDelete(options, user);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _onDelete(options, userId) {\n super._onDelete(options, userId);\n if ( game.user === game.users.activeGM ) this.getDependents().forEach(e => e.delete());\n if ( this.isAppliedEnchantment ) dnd5e.registry.enchantments.untrack(this.origin, this.uuid);\n document.body.querySelectorAll(`enchantment-application:has([data-enchantment-uuid=\"${this.uuid}\"]`)\n .forEach(element => element.buildItemList());\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _displayScrollingStatus(enabled) {\n if ( this.isConcealed ) return;\n super._displayScrollingStatus(enabled);\n }\n\n /* -------------------------------------------- */\n /* Exhaustion and Concentration Handling */\n /* -------------------------------------------- */\n\n /**\n * Create effect data for concentration on an actor.\n * @param {Activity} activity The Activity on which to begin concentrating.\n * @param {object} [data] Additional data provided for the effect instance.\n * @returns {object} Created data for the ActiveEffect.\n */\n static createConcentrationEffectData(activity, data={}) {\n const item = activity?.item;\n if ( !item?.isEmbedded || !activity.duration.concentration ) {\n throw new Error(\"You may not begin concentrating on this item!\");\n }\n\n const statusEffect = CONFIG.statusEffects.find(e => e.id === CONFIG.specialStatusEffects.CONCENTRATING);\n const effectData = foundry.utils.mergeObject({\n ...statusEffect,\n name: `${game.i18n.localize(\"EFFECT.DND5E.StatusConcentrating\")}: ${item.name}`,\n description: `${game.i18n.format(\"DND5E.ConcentratingOn\", {\n name: item.name,\n type: game.i18n.localize(`TYPES.Item.${item.type}`)\n })}
@Embed[${item.uuid} inline]
`,\n duration: activity.duration.getEffectData(),\n \"flags.dnd5e\": {\n activity: {\n type: activity.type, id: activity.id, uuid: activity.uuid\n },\n item: {\n type: item.type, id: item.id, uuid: item.uuid,\n data: !item.actor.items.has(item.id) ? item.toObject() : undefined\n }\n },\n origin: item.uuid,\n statuses: [statusEffect.id].concat(statusEffect.statuses ?? [])\n }, data, {inplace: false});\n delete effectData.id;\n if ( item.type === \"spell\" ) effectData[\"flags.dnd5e.spellLevel\"] = item.system.level;\n\n return effectData;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Register listeners for custom handling in the TokenHUD.\n */\n static registerHUDListeners() {\n Hooks.on(\"renderTokenHUD\", this.onTokenHUDRender);\n document.addEventListener(\"click\", this.onClickTokenHUD.bind(this), { capture: true });\n document.addEventListener(\"contextmenu\", this.onClickTokenHUD.bind(this), { capture: true });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Add modifications to the core ActiveEffect config.\n * @param {ActiveEffectConfig} app The ActiveEffect config.\n * @param {HTMLElement} html The ActiveEffect config element.\n * @param {ApplicationRenderContext} context The app's rendering context.\n */\n static onRenderActiveEffectConfig(app, html, context) {\n const element = new foundry.data.fields.SetField(new foundry.data.fields.StringField(), {}).toFormGroup({\n label: game.i18n.localize(\"DND5E.CONDITIONS.RiderConditions.label\"),\n hint: game.i18n.localize(\"DND5E.CONDITIONS.RiderConditions.hint\")\n }, {\n name: \"flags.dnd5e.riders.statuses\",\n value: app.document.getFlag(\"dnd5e\", \"riders.statuses\") ?? [],\n options: CONFIG.statusEffects.map(se => ({ value: se.id, label: se.name })),\n disabled: !context.editable\n });\n html.querySelector(\"[data-tab=details] > .form-group:has([name=statuses])\")?.after(element);\n\n // Add tooltip with link to wiki for effects/enchantments\n const helpIconElement = document.createElement(\"i\");\n helpIconElement.classList.add(\"fa-solid\", \"fa-circle-question\");\n const tooltipText = game.i18n.format(\"DND5E.ACTIVEEFFECT.AttributeKeyTooltip\", {\n url: app.document.type === \"enchantment\"\n ? \"https://github.com/foundryvtt/dnd5e/wiki/Enchantment\"\n : \"https://github.com/foundryvtt/dnd5e/wiki/Active-Effect-Guide\"\n });\n Object.assign(helpIconElement.dataset, { tooltip: tooltipText, tooltipDirection: \"RIGHT\", locked: \"\" });\n const targetElement = html.querySelector(\"section:is([data-tab='effects'], [data-tab='changes']) .key\");\n if ( targetElement ) targetElement.insertAdjacentElement(\"beforeend\", helpIconElement);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Adjust exhaustion icon display to match current level.\n * @param {Application} app The TokenHUD application.\n * @param {HTMLElement} html The TokenHUD HTML.\n */\n static onTokenHUDRender(app, html) {\n const actor = app.object.actor;\n const level = foundry.utils.getProperty(actor, \"system.attributes.exhaustion\");\n if ( Number.isFinite(level) && (level > 0) ) {\n const img = ActiveEffect5e._getExhaustionImage(level);\n const elem = html.querySelector('[data-status-id=\"exhaustion\"]');\n if ( elem ) {\n elem.style.objectPosition = \"-100px\";\n elem.style.background = `url('${img}') no-repeat center / contain`;\n }\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Get the image used to represent exhaustion at this level.\n * @param {number} level\n * @returns {string}\n */\n static _getExhaustionImage(level) {\n const { img } = CONFIG.DND5E.conditionTypes.exhaustion;\n const split = img.split(\".\");\n const ext = split.pop();\n const path = split.join(\".\");\n return `${path}-${level}.${ext}`;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Implement custom behavior for select conditions on the token HUD.\n * @param {PointerEvent} event The triggering event.\n */\n static onClickTokenHUD(event) {\n const { target } = event;\n if ( !target.classList?.contains(\"effect-control\") ) return;\n\n const actor = canvas.hud.token.object?.actor;\n if ( !actor ) return;\n\n const id = target.dataset?.statusId;\n if ( id === \"exhaustion\" ) ActiveEffect5e._manageExhaustion(event, actor);\n else if ( id === \"concentrating\" ) ActiveEffect5e._manageConcentration(event, actor);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Manage custom exhaustion cycling when interacting with the token HUD.\n * @param {PointerEvent} event The triggering event.\n * @param {Actor5e} actor The actor belonging to the token.\n */\n static _manageExhaustion(event, actor) {\n let level = foundry.utils.getProperty(actor, \"system.attributes.exhaustion\");\n if ( !Number.isFinite(level) ) return;\n event.preventDefault();\n event.stopPropagation();\n if ( event.button === 0 ) level++;\n else level--;\n const max = CONFIG.DND5E.conditionTypes.exhaustion.levels;\n actor.update({ \"system.attributes.exhaustion\": Math.clamp(level, 0, max) });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Manage custom concentration handling when interacting with the token HUD.\n * @param {PointerEvent} event The triggering event.\n * @param {Actor5e} actor The actor belonging to the token.\n */\n static _manageConcentration(event, actor) {\n const { effects } = actor.concentration;\n if ( effects.size < 1 ) return;\n event.preventDefault();\n event.stopPropagation();\n if ( effects.size === 1 ) {\n actor.endConcentration(effects.first());\n return;\n }\n const choices = effects.reduce((acc, effect) => {\n const data = effect.getFlag(\"dnd5e\", \"item\");\n acc[effect.id] = data?.name ?? actor.items.get(data?.id)?.name ?? game.i18n.localize(\"DND5E.ConcentratingItemless\");\n return acc;\n }, {});\n const options = HandlebarsHelpers.selectOptions(choices, { hash: { sort: true } });\n const content = `\n ${game.i18n.localize(\"DND5E.ConcentratingEndChoice\")}
\n `;\n foundry.applications.api.Dialog.prompt({\n content,\n window: { title: game.i18n.localize(\"DND5E.Concentration\") },\n ok: {\n label: game.i18n.localize(\"DND5E.Confirm\"),\n callback: (event, button, dialog) => {\n const source = new foundry.applications.ux.FormDataExtended(button.form).object.source;\n if ( source ) actor.endConcentration(source);\n }\n }\n });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Record another effect as a dependent of this one.\n * @param {...ActiveEffect5e} dependent One or more dependent effects.\n * @returns {Promise}\n */\n addDependent(...dependent) {\n foundry.utils.logCompatibilityWarning(\n \"Dependent documents are now tracked using the `dependentOn` flag on the document itself.\",\n { since: \"DnD5e 5.2\", until: \"DnD5e 6.0\", once: true }\n );\n return Promise.all(dependent.map(d => d.setFlag(\"dnd5e\", \"dependentOn\", this.uuid))).then(() => this);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Retrieve a list of dependent effects.\n * @returns {Array}\n */\n getDependents() {\n const actor = this.parent instanceof Actor ? this.parent : this.parent?.parent;\n const item = this.parent instanceof Item ? this.parent : null;\n return (this.getFlag(\"dnd5e\", \"dependents\") || []).reduce((arr, { uuid }) => {\n let doc;\n // TODO: Remove this special casing once https://github.com/foundryvtt/foundryvtt/issues/11214 is resolved\n if ( this.parent.pack && uuid.includes(this.parent.uuid) ) {\n const [, embeddedName, id] = uuid.replace(this.parent.uuid, \"\").split(\".\");\n doc = this.parent.getEmbeddedDocument(embeddedName, id);\n }\n else doc = fromUuidSync(uuid, { strict: false });\n if ( doc ) {\n const otherActor = doc.parent instanceof Actor ? doc.parent : doc.parent?.parent;\n const otherItem = doc.parent instanceof Item ? doc.parent : null;\n if ( ((doc instanceof ActiveEffect) && (doc.origin === this.uuid))\n || ((actor && (actor === otherActor)) || (item && (item === otherItem)))) arr.push(doc);\n }\n return arr;\n }, []).concat(dnd5e.registry.dependents.get(this));\n }\n\n /* -------------------------------------------- */\n /* Importing and Exporting */\n /* -------------------------------------------- */\n\n /** @override */\n static async createDialog(data={}, createOptions={}, dialogOptions={}) {\n CreateDocumentDialog.migrateOptions(createOptions, dialogOptions);\n return CreateDocumentDialog.prompt(this, data, createOptions, dialogOptions);\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n static _createDialogTypes(parent) {\n return parent\n ? ActiveEffect.TYPES.filter(t => CONFIG.ActiveEffect.dataModels[t]?.availableForItem?.(parent) ?? true)\n : ActiveEffect.TYPES;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Determine default artwork based on the provided effect data.\n * @param {object} effectData The source effect data.\n * @returns {{ img: string }} Candidate effect image.\n */\n static getDefaultArtwork(effectData={}) {\n const type = effectData.type !== \"base\" ? effectData.type : \"standard\";\n return { img: CONFIG.DND5E.defaultArtwork.ActiveEffect[type] ?? this.DEFAULT_ICON };\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /**\n * Helper method to add choices that have been overridden by an active effect. Used to determine what fields might\n * need to be disabled because they are overridden by an active effect in a way not easily determined by looking at\n * the `Document#overrides` data structure.\n * @param {Actor5e|Item5e} doc Document from which to determine the overrides.\n * @param {string} prefix The initial form prefix under which the choices are grouped.\n * @param {string} path Path in document data.\n * @param {string[]} overrides The list of fields that are currently modified by Active Effects. *Will be mutated.*\n */\n static addOverriddenChoices(doc, prefix, path, overrides) {\n const source = new Set(foundry.utils.getProperty(doc._source, path) ?? []);\n const current = foundry.utils.getProperty(doc, path) ?? new Set();\n const delta = current.symmetricDifference(source);\n for ( const choice of delta ) overrides.push(`${prefix}.${choice}`);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Render a rich tooltip for this effect.\n * @param {EnrichmentOptions} [enrichmentOptions={}] Options for text enrichment.\n * @returns {Promise<{content: string, classes: string[]}>}\n */\n async richTooltip(enrichmentOptions={}) {\n let properties = [];\n if ( this.isSuppressed ) properties.push(\"DND5E.EffectType.Unavailable\");\n else if ( this.disabled ) properties.push(\"DND5E.EffectType.Inactive\");\n else if ( this.isTemporary ) properties.push(\"DND5E.EffectType.Temporary\");\n else properties.push(\"DND5E.EffectType.Passive\");\n if ( this.type === \"enchantment\" ) properties.push(\"DND5E.ENCHANTMENT.Label\");\n properties = properties.map(p => game.i18n.localize(p));\n properties.unshift(...this.statuses.map(id => game.release.generation < 14\n ? CONFIG.statusEffects.find(s => s.id === id)?.name\n : CONFIG.statusEffects[id]?.name).filter(_ => _));\n\n return {\n content: await foundry.applications.handlebars.renderTemplate(\n \"systems/dnd5e/templates/effects/parts/effect-tooltip.hbs\", {\n effect: this,\n description: await TextEditor.enrichHTML(this.description ?? \"\", { relativeTo: this, ...enrichmentOptions }),\n durationParts: this.duration.remaining ? this.duration.label.split(\", \") : [],\n showDuration: game.release.generation < 14\n ? !!this.duration.remaining : Number.isFinite(this.duration.value),\n properties\n }\n ),\n classes: [\"dnd5e2\", \"dnd5e-tooltip\", \"effect-tooltip\", \"themed\", \"theme-light\"]\n };\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n async deleteDialog({ sheet, ...dialogOptions }={}, operation={}) {\n const type = game.i18n.localize(this.constructor.metadata.label);\n const config = foundry.utils.mergeObject({\n window: { title: `${game.i18n.format(\"DOCUMENT.Delete\", { type })}: ${this.name}` },\n position: { width: 400 },\n content: `\n \n ${game.i18n.localize(\"AreYouSure\")} ${game.i18n.format(\"SIDEBAR.DeleteWarning\", { type })}\n
\n `,\n yes: { callback: () => this.delete(operation) }\n }, dialogOptions);\n if ( sheet ) return sheet._confirmDialog(config);\n return foundry.applications.api.DialogV2.confirm(config);\n }\n}\n\n/**\n * @deprecated\n * @since 5.3.0\n */\nif ( !(\"applyChange\" in ActiveEffect) ) {\n const original = {\n applyField: ActiveEffect.applyField,\n _applyLegacy: ActiveEffect.prototype._applyLegacy,\n _applyAdd: ActiveEffect.prototype._applyAdd,\n _applyUpgrade: ActiveEffect.prototype._applyUpgrade\n };\n\n /** @ignore */\n ActiveEffect5e.applyField = function(model, change, field) {\n field ??= model.schema?.getField?.(change.key);\n const current = foundry.utils.getProperty(model, change.key);\n const modes = CONST.ACTIVE_EFFECT_MODES;\n if ( (field instanceof StringField) && (change.mode === modes.OVERRIDE) && change.value.includes?.(\"{}\") ) {\n change.value = change.value.replace(\"{}\", current ?? \"\");\n }\n if ( (current === null) && [modes.UPGRADE, modes.DOWNGRADE].includes(change.mode) ) change.mode = modes.OVERRIDE;\n if ( (field instanceof SetField) && (change.mode === modes.ADD) && (foundry.utils.getType(current) === \"Set\") ) {\n for ( const value of field._castChangeDelta(change.value) ) {\n const neg = value.replace(/^\\s*-\\s*/, \"\");\n if ( neg !== value ) current.delete(neg);\n else current.add(value);\n }\n return current;\n }\n if ( (current === undefined) && change.key.startsWith(\"system.\") ) {\n let keyPath = change.key;\n let mappingField = field;\n while ( !(mappingField instanceof MappingField) && mappingField ) {\n if ( mappingField.name ) keyPath = keyPath.substring(0, keyPath.length - mappingField.name.length - 1);\n mappingField = mappingField.parent;\n }\n if ( mappingField && (foundry.utils.getProperty(model, keyPath) === undefined) ) {\n const created = mappingField.model.initialize(mappingField.model.getInitialValue(), mappingField);\n foundry.utils.setProperty(model, keyPath, created);\n }\n }\n if ( (field instanceof ObjectField) || (field instanceof SchemaField) ) {\n change = { ...change, value: parseOrString(change.value) };\n }\n return original.applyField.call(this, model, change, field);\n };\n\n /** @ignore */\n ActiveEffect5e.prototype._applyLegacy = function(actor, change, changes) {\n if ( this.system._applyLegacy?.(actor, change, changes) === false ) return;\n\n // Double-check whether the target should be treated as a formula if the key has been modified\n if ( ActiveEffect5e.FORMULA_FIELDS.has(change.key) ) {\n const field = new FormulaField({ deterministic: change.key !== \"system.damageBonus\" });\n return { [change.key]: this.constructor.applyField(actor, change, field) };\n }\n\n original._applyLegacy.call(this, actor, change, changes);\n };\n\n /** @ignore */\n ActiveEffect5e.prototype._applyAdd = function(actor, change, current, delta, changes) {\n if ( current instanceof Set ) {\n const handle = v => {\n const neg = v.replace(/^\\s*-\\s*/, \"\");\n if ( neg !== v ) current.delete(neg);\n else current.add(v);\n };\n if ( Array.isArray(delta) ) delta.forEach(item => handle(item));\n else if ( delta instanceof Set ) for ( const item of delta ) handle(item);\n else handle(delta);\n return;\n }\n original._applyAdd.call(this, actor, change, current, delta, changes);\n };\n\n /** @ignore */\n ActiveEffect5e.prototype._applyUpgrade = function(actor, change, current, delta, changes) {\n if ( current === null ) return this._applyOverride(actor, change, current, delta, changes);\n original._applyUpgrade.call(this, actor, change, current, delta, changes);\n };\n}\n","import FormulaField from \"../fields/formula-field.mjs\";\n\nconst { BooleanField, SetField, StringField } = foundry.data.fields;\n\n/**\n * @import { TravelPace5e } from \"../actor/fields/_types.mjs\";\n */\n\n/**\n * Field for storing movement data.\n */\nexport default class MovementField extends foundry.data.fields.SchemaField {\n constructor(fields={}, { initialUnits=null, ...options }={}) {\n fields = {\n walk: new FormulaField({ deterministic: true, label: \"DND5E.MOVEMENT.Type.Speed\", speed: true }),\n burrow: new FormulaField({ deterministic: true, label: \"DND5E.MOVEMENT.Type.Burrow\", speed: true }),\n climb: new FormulaField({ deterministic: true, label: \"DND5E.MOVEMENT.Type.Climb\", speed: true }),\n fly: new FormulaField({ deterministic: true, label: \"DND5E.MOVEMENT.Type.Fly\", speed: true }),\n swim: new FormulaField({ deterministic: true, label: \"DND5E.MOVEMENT.Type.Swim\", speed: true }),\n bonus: new FormulaField({ deterministic: true, label: \"DND5E.MOVEMENT.FIELDS.bonus.label\" }),\n special: new StringField({ label: \"DND5E.MOVEMENT.FIELDS.special.label\" }),\n units: new StringField({\n required: true, nullable: true, blank: false, initial: initialUnits, label: \"DND5E.MOVEMENT.FIELDS.units.label\"\n }),\n hover: new BooleanField({ required: true, label: \"DND5E.MOVEMENT.Hover\" }),\n ignoredDifficultTerrain: new SetField(new StringField(), {\n label: \"DND5E.MOVEMENT.FIELDS.ignoredDifficultTerrain.label\"\n }),\n ...fields\n };\n Object.entries(fields).forEach(([k, v]) => !v ? delete fields[k] : null);\n super(fields, { label: \"DND5E.Movement\", ...options });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Apply rules for travel pace to the given skill.\n * @param {TravelPace5e} pace The travel pace.\n * @param {string} skill The skill.\n * @returns {{ advantage: boolean, disadvantage: boolean }}\n */\n static getTravelPaceMode(pace, skill) {\n foundry.utils.logCompatibilityWarning(\n \"The `MovementField#getTravelPaceMode` has been moved to `TravelField#getTravelPaceMode.\",\n { since: \"DnD5e 5.2\", until: \"DnD5e 6.0\", once: true }\n );\n return dnd5e.dataModels.actor.TravelField.getTravelPaceMode(pace, skill);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare movement data.\n * @this {MovementData}\n * @param {DataField} field The movement field.\n */\n static prepareData(field) {\n foundry.utils.logCompatibilityWarning(\n \"The `MovementField#prepareData` is now handled through `TravelField#prepareData`.\",\n { since: \"DnD5e 5.2\", until: \"DnD5e 6.0\", once: true }\n );\n }\n}\n","import AdvantageModeField from \"../fields/advantage-mode-field.mjs\";\n\nconst { StringField, NumberField, SchemaField } = foundry.data.fields;\n\n/**\n * Field for storing data for a specific type of roll.\n */\nexport default class RollConfigField extends foundry.data.fields.SchemaField {\n constructor({ roll={}, ability=\"\", ...fields }={}, options={}) {\n const opts = { initial: null, nullable: true, min: 1, max: 20, integer: true };\n fields = {\n ability: (ability === false) ? null : new StringField({\n required: true,\n initial: ability,\n label: \"DND5E.AbilityModifier\"\n }),\n roll: new SchemaField({\n min: new NumberField({...opts, label: \"DND5E.ROLL.Range.Minimum\"}),\n max: new NumberField({...opts, label: \"DND5E.ROLL.Range.Maximum\"}),\n mode: new AdvantageModeField(),\n ...roll\n }),\n ...fields\n };\n Object.entries(fields).forEach(([k, v]) => !v ? delete fields[k] : null);\n super(fields, options);\n }\n}\n","import MappingField from \"../fields/mapping-field.mjs\";\n\nconst { NumberField, StringField } = foundry.data.fields;\n\n/**\n * Field for storing senses data.\n */\nexport default class SensesField extends foundry.data.fields.SchemaField {\n constructor(fields={}, { initialUnits=null, ...options }={}) {\n fields = {\n ranges: new MappingField(\n new NumberField({ required: true, nullable: true, integer: true, min: 0, initial: null }),\n { initialKeys: CONFIG.DND5E.senses, initialKeysOnly: true }\n ),\n units: new StringField({\n required: true, nullable: true, blank: false, initial: initialUnits, label: \"DND5E.SenseUnits\"\n }),\n special: new StringField({ required: true, label: \"DND5E.SenseSpecial\" }),\n ...fields\n };\n Object.entries(fields).forEach(([k, v]) => !v ? delete fields[k] : null);\n super(fields, { label: \"DND5E.Senses\", ...options });\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /**\n * Default senses that need to be migrated and shimmed.\n * @type {string[]}\n */\n static #DEFAULT_SENSES = [\"darkvision\", \"blindsight\", \"tremorsense\", \"truesight\"];\n\n /* -------------------------------------------- */\n\n /**\n * Migrate senses into mapping field.\n * @param {SensesData} [senses] Senses data object to shim.\n */\n static _migrate(senses) {\n if ( !senses ) return;\n senses.ranges ??= {};\n for ( const key of SensesField.#DEFAULT_SENSES ) {\n if ( !(key in senses) || (key in senses.ranges) ) continue;\n senses.ranges[key] = senses[key];\n delete senses[key];\n }\n }\n\n /* -------------------------------------------- */\n /* Data Shims */\n /* -------------------------------------------- */\n\n /**\n * Apply shims to the senses field so old sense locations still work.\n * @param {SensesData} senses Senses data object to shim.\n */\n static _shim(senses) {\n for ( const key of SensesField.#DEFAULT_SENSES ) {\n Object.defineProperty(senses, key, {\n get() {\n foundry.utils.logCompatibilityWarning(`senses.${key} has moved to \"senses.ranges.${key}\".`, {\n since: \"DnD5e 5.3\", until: \"DnD5e 6.1\", once: true\n });\n return this.ranges[key];\n },\n set(value) {\n foundry.utils.logCompatibilityWarning(`senses.${key} has moved to \"senses.ranges.${key}\".`, {\n since: \"DnD5e 5.3\", until: \"DnD5e 6.1\", once: true\n });\n this.ranges[key] = value;\n },\n enumerable: true\n });\n }\n }\n}\n","import ActiveEffect5e from \"../../../documents/active-effect.mjs\";\nimport Proficiency from \"../../../documents/actor/proficiency.mjs\";\nimport { convertLength, convertWeight, defaultUnits, replaceFormulaData, simplifyBonus } from \"../../../utils.mjs\";\nimport AdvantageModeField from \"../../fields/advantage-mode-field.mjs\";\nimport FormulaField from \"../../fields/formula-field.mjs\";\nimport MovementField from \"../../shared/movement-field.mjs\";\nimport RollConfigField from \"../../shared/roll-config-field.mjs\";\nimport SensesField from \"../../shared/senses-field.mjs\";\n\nconst { NumberField, SchemaField, StringField } = foundry.data.fields;\n\n/**\n * @import { ActorRollData } from \"../../../documents/_types.mjs\";\n * @import { ArmorClassData, AttributesCommonData, AttributesCreatureData, HitPointsData } from \"./_types.mjs\";\n */\n\n/**\n * Shared contents of the attributes schema between various actor types.\n */\nexport default class AttributesFields {\n /**\n * Armor class fields shared between characters, NPCs, and vehicles.\n * @type {ArmorClassData}\n */\n static get armorClass() {\n return {\n calc: new StringField({ initial: \"default\", label: \"DND5E.ArmorClassCalculation\" }),\n flat: new NumberField({ required: true, integer: true, min: 0, label: \"DND5E.ArmorClassFlat\" }),\n formula: new FormulaField({ deterministic: true, label: \"DND5E.ArmorClassFormula\" })\n };\n }\n\n /* -------------------------------------------- */\n /**\n * Hit points fields shared between NPCs, objects, and vehicles.\n * @type {HitPointsData}\n */\n static get hitPoints() {\n return {\n dt: new NumberField({ integer: true, min: 0, label: \"DND5E.DamageThreshold\" }),\n max: new NumberField({ nullable: true, integer: true, min: 0, initial: null, label: \"DND5E.HitPointsMax\" }),\n temp: new NumberField({ integer: true, initial: 0, min: 0, label: \"DND5E.HitPointsTemp\" }),\n tempmax: new NumberField({\n integer: true, initial: 0, label: \"DND5E.HitPointsTempMax\", hint: \"DND5E.HitPointsTempMaxHint\"\n }),\n value: new NumberField({ nullable: true, integer: true, min: 0, initial: null, label: \"DND5E.HitPointsCurrent\" })\n };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Fields shared between characters, NPCs, and vehicles.\n * @type {AttributesCommonData}\n */\n static get common() {\n return {\n ac: new SchemaField(this.armorClass, { label: \"DND5E.ArmorClass\" }),\n init: new RollConfigField({\n ability: \"\",\n bonus: new FormulaField({ required: true, label: \"DND5E.InitiativeBonus\" })\n }, { label: \"DND5E.Initiative\" }),\n movement: new MovementField()\n };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Fields shared between characters and NPCs.\n * @type {AttributesCreatureData}\n */\n static get creature() {\n return {\n attunement: new SchemaField({\n max: new NumberField({\n required: true, nullable: false, integer: true, min: 0, initial: 3, label: \"DND5E.AttunementMax\"\n })\n }, { label: \"DND5E.Attunement\" }),\n senses: new SensesField(),\n spellcasting: new StringField({ required: true, blank: true, label: \"DND5E.SpellAbility\" }),\n exhaustion: new NumberField({\n required: true, nullable: false, integer: true, min: 0, initial: 0, label: \"DND5E.Exhaustion\"\n }),\n concentration: new RollConfigField({\n ability: \"\",\n bonuses: new SchemaField({\n save: new FormulaField({ required: true, label: \"DND5E.ConcentrationBonus\" })\n }),\n limit: new NumberField({ integer: true, min: 0, initial: 1, label: \"DND5E.ConcentrationLimit\" })\n }, { label: \"DND5E.Concentration\" }),\n loyalty: new SchemaField({\n value: new NumberField({ integer: true, min: 0, max: 20, label: \"DND5E.Loyalty\" })\n })\n };\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /**\n * Migrate the old init.value and incorporate it into init.bonus.\n * @param {object} source The source attributes object.\n * @internal\n */\n static _migrateInitiative(source) {\n const init = source?.init;\n if ( !init?.value || (typeof init?.bonus === \"string\") ) return;\n if ( init.bonus ) init.bonus += init.value < 0 ? ` - ${init.value * -1}` : ` + ${init.value}`;\n else init.bonus = `${init.value}`;\n }\n\n /* -------------------------------------------- */\n /* Data Preparation */\n /* -------------------------------------------- */\n\n /**\n * Initialize derived AC fields for Active Effects to target.\n * @this {CharacterData|NPCData|VehicleData}\n */\n static prepareBaseArmorClass() {\n const ac = this.attributes.ac;\n ac.armor = 10;\n ac.shield = ac.cover = 0;\n ac.min = ac.bonus = \"\";\n }\n\n /* -------------------------------------------- */\n\n /**\n * Initialize base encumbrance fields to be targeted by active effects.\n * @this {CharacterData|NPCData|VehicleData}\n */\n static prepareBaseEncumbrance() {\n const encumbrance = this.attributes.encumbrance ??= {};\n encumbrance.multipliers = { encumbered: \"1\", heavilyEncumbered: \"1\", maximum: \"1\", overall: \"1\" };\n encumbrance.bonuses = { encumbered: \"\", heavilyEncumbered: \"\", maximum: \"\", overall: \"\" };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare a character's AC value from their equipped armor and shield.\n * @this {CharacterData|NPCData|VehicleData}\n * @param {ActorRollData} rollData The Actor's roll data.\n */\n static prepareArmorClass(rollData) {\n const ac = this.attributes.ac;\n\n // Apply automatic migrations for older data structures\n let cfg = CONFIG.DND5E.armorClasses[ac.calc];\n if ( !cfg ) {\n ac.calc = \"flat\";\n if ( Number.isNumeric(ac.value) ) ac.flat = Number(ac.value);\n cfg = CONFIG.DND5E.armorClasses.flat;\n }\n\n // Identify Equipped Items\n const { armors, shields } = this.parent.itemTypes.equipment.reduce((obj, equip) => {\n if ( !equip.system.equipped || !(equip.system.type.value in CONFIG.DND5E.armorTypes)) return obj;\n if ( equip.system.type.value === \"shield\" ) obj.shields.push(equip);\n else obj.armors.push(equip);\n return obj;\n }, { armors: [], shields: [] });\n\n // Set stealth disadvantage\n if ( armors[0]?.system.properties.has(\"stealthDisadvantage\") ) {\n AdvantageModeField.setMode(this, \"skills.ste.roll.mode\", -1);\n }\n\n ac.label = ![\"custom\", \"flat\"].includes(ac.calc) ? CONFIG.DND5E.armorClasses[ac.calc]?.label : null;\n\n // Determine base AC\n switch ( ac.calc ) {\n\n // Flat AC (no additional bonuses)\n case \"flat\":\n ac.value = Number(ac.flat);\n return;\n\n // Natural AC (includes bonuses)\n case \"natural\":\n ac.base = Number(ac.flat);\n break;\n\n default:\n let formula = ac.calc === \"custom\" ? ac.formula : cfg.formula;\n if ( armors.length ) {\n if ( armors.length > 1 ) this.parent._preparationWarnings.push({\n message: game.i18n.localize(\"DND5E.WarnMultipleArmor\"), type: \"warning\"\n });\n const armorData = armors[0].system.armor;\n const isHeavy = armors[0].system.type.value === \"heavy\";\n ac.armor = armorData.value ?? ac.armor;\n ac.dex = isHeavy ? 0 : Math.min(armorData.dex ?? Infinity, this.abilities.dex?.mod ?? 0);\n ac.equippedArmor = armors[0];\n }\n else ac.dex = this.abilities.dex?.mod ?? 0;\n\n if ( !ac.equippedArmor ) ac.label = null;\n\n rollData.attributes.ac = ac;\n try {\n const replaced = replaceFormulaData(formula, rollData, {\n actor: this, missing: null, property: game.i18n.localize(\"DND5E.ArmorClass\")\n });\n ac.base = replaced ? new Roll(replaced).evaluateSync().total : 0;\n } catch(err) {\n this.parent._preparationWarnings.push({\n message: game.i18n.format(\"DND5E.WarnBadACFormula\", { formula }), link: \"armor\", type: \"error\"\n });\n const replaced = Roll.replaceFormulaData(CONFIG.DND5E.armorClasses.default.formula, rollData);\n ac.base = new Roll(replaced).evaluateSync().total;\n }\n break;\n }\n\n // Equipped Shield\n if ( shields.length ) {\n if ( shields.length > 1 ) this.parent._preparationWarnings.push({\n message: game.i18n.localize(\"DND5E.WarnMultipleShields\"), type: \"warning\"\n });\n ac.shield = shields[0].system.armor.value ?? 0;\n ac.equippedShield = shields[0];\n }\n\n // Compute cover.\n ac.cover = Math.max(ac.cover, this.parent.coverBonus);\n\n // Compute total AC and return\n ac.min = simplifyBonus(ac.min, rollData);\n ac.bonus = simplifyBonus(ac.bonus, rollData);\n ac.value = Math.max(ac.min, ac.base + ac.shield + ac.bonus + ac.cover);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare concentration data for an Actor.\n * @this {CharacterData|NPCData}\n * @param {ActorRollData} rollData The Actor's roll data.\n */\n static prepareConcentration(rollData) {\n const { concentration } = this.attributes;\n const abilityId = concentration.ability || CONFIG.DND5E.defaultAbilities.concentration;\n const ability = this.abilities?.[abilityId] || {};\n const bonus = simplifyBonus(concentration.bonuses.save, rollData);\n concentration.save = (ability.save?.value ?? 0) + bonus;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Calculate encumbrance details for an Actor.\n * @this {CharacterData|NPCData|VehicleData}\n * @param {ActorRollData} rollData The Actor's roll data.\n * @param {object} [options]\n * @param {Function} [options.validateItem] Determine whether an item's weight should count toward encumbrance.\n */\n static prepareEncumbrance(rollData, { validateItem }={}) {\n const config = CONFIG.DND5E.encumbrance;\n const encumbrance = this.attributes.encumbrance ??= {};\n const baseUnits = CONFIG.DND5E.encumbrance.baseUnits[this.parent.type]\n ?? CONFIG.DND5E.encumbrance.baseUnits.default;\n const unitSystem = game.settings.get(\"dnd5e\", \"metricWeightUnits\") ? \"metric\" : \"imperial\";\n const { attributes } = this;\n\n // Get the total weight from items\n let weight = this.parent.items\n .filter(item => !item.container && (validateItem?.(item) ?? true))\n .reduce((weight, item) => weight + (item.system.totalWeightIn?.(baseUnits[unitSystem]) ?? 0), 0);\n\n // [Optional] add Currency Weight (for non-transformed actors)\n const currency = this.currency;\n if ( game.settings.get(\"dnd5e\", \"currencyWeight\") && currency ) {\n const numCoins = Object.values(currency).reduce((val, denom) => val + Math.max(denom, 0), 0);\n const currencyPerWeight = config.currencyPerWeight[unitSystem];\n weight += convertWeight(\n numCoins / currencyPerWeight,\n config.baseUnits.default[unitSystem],\n baseUnits[unitSystem]\n );\n }\n\n // Determine the Encumbrance size class\n const keys = Object.keys(CONFIG.DND5E.actorSizes);\n const index = keys.findIndex(k => k === this.traits.size);\n const sizeConfig = CONFIG.DND5E.actorSizes[\n keys[this.parent.flags.dnd5e?.powerfulBuild ? Math.min(index + 1, keys.length - 1) : index]\n ];\n const sizeMod = sizeConfig?.capacityMultiplier ?? sizeConfig?.token ?? 1;\n let maximumMultiplier;\n\n const calculateThreshold = threshold => {\n let base = this.abilities.str?.value ?? 10;\n const bonus = simplifyBonus(encumbrance.bonuses?.[threshold], rollData)\n + simplifyBonus(encumbrance.bonuses?.overall, rollData);\n let multiplier = simplifyBonus(encumbrance.multipliers[threshold], rollData)\n * simplifyBonus(encumbrance.multipliers.overall, rollData);\n if ( threshold === \"maximum\" ) maximumMultiplier = multiplier;\n if ( this.isVehicle ) {\n const { cargo } = attributes.capacity;\n base = convertWeight(cargo.value || Infinity, cargo.units, baseUnits[unitSystem]);\n }\n else multiplier *= (config.threshold[threshold]?.[unitSystem] ?? 1) * sizeMod;\n return (base * multiplier).toNearest(0.1) + bonus;\n };\n\n // Populate final Encumbrance values\n encumbrance.value = weight.toNearest(0.1);\n encumbrance.thresholds = {\n encumbered: calculateThreshold(\"encumbered\"),\n heavilyEncumbered: calculateThreshold(\"heavilyEncumbered\"),\n maximum: calculateThreshold(\"maximum\")\n };\n encumbrance.max = encumbrance.thresholds.maximum;\n encumbrance.mod = (sizeMod * maximumMultiplier).toNearest(0.1);\n encumbrance.stops = {\n encumbered: Number.isFinite(encumbrance.max)\n ? Math.clamp((encumbrance.thresholds.encumbered * 100) / encumbrance.max, 0, 100)\n : 0,\n heavilyEncumbered: Number.isFinite(encumbrance.max)\n ? Math.clamp((encumbrance.thresholds.heavilyEncumbered * 100) / encumbrance.max, 0, 100)\n : 0\n };\n encumbrance.pct = Math.clamp((encumbrance.value * 100) / encumbrance.max, 0, 100);\n encumbrance.encumbered = encumbrance.value > encumbrance.heavilyEncumbered;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Adjust exhaustion level based on Active Effects.\n * @this {CharacterData|NPCData}\n */\n static prepareExhaustionLevel() {\n const exhaustion = this.parent.effects.get(ActiveEffect5e.ID.EXHAUSTION);\n const level = exhaustion?.getFlag(\"dnd5e\", \"exhaustionLevel\");\n this.attributes.exhaustion = Number.isFinite(level) ? level : 0;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Calculate maximum hit points, taking an provided advancement into consideration.\n * @param {object} hp HP object to calculate.\n * @param {object} [options={}]\n * @param {HitPointsAdvancement[]} [options.advancement=[]] Advancement items from which to get hit points per-level.\n * @param {number} [options.bonus=0] Additional bonus to add atop the calculated value.\n * @param {number} [options.mod=0] Modifier for the ability to add to hit points from advancement.\n * @this {ActorDataModel}\n */\n static prepareHitPoints(hp, { advancement=[], mod=0, bonus=0 }={}) {\n const base = advancement.reduce((total, advancement) => total + advancement.getAdjustedTotal(mod), 0);\n hp.max = (hp.max ?? 0) + base + bonus;\n if ( this.parent.hasConditionEffect(\"halfHealth\") ) hp.max *= 0.5;\n hp.max = Math.floor(hp.max);\n\n hp.effectiveMax = Math.max(hp.max + (hp.tempmax ?? 0), 0);\n hp.value = Math.min(hp.value, hp.effectiveMax);\n hp.damage = hp.effectiveMax - hp.value;\n hp.pct = Math.clamp(hp.effectiveMax ? (hp.value / hp.effectiveMax) * 100 : 0, 0, 100);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare the initiative data for an actor.\n * @this {CharacterData|NPCData|VehicleData}\n * @param {ActorRollData} rollData The Actor's roll data.\n */\n static prepareInitiative(rollData) {\n const init = this.attributes.init ??= {};\n const flags = this.parent.flags.dnd5e ?? {};\n const globalCheckBonus = simplifyBonus(this.bonuses?.abilities?.check, rollData);\n\n // Compute initiative modifier\n const abilityId = init.ability || CONFIG.DND5E.defaultAbilities.initiative;\n const ability = this.abilities?.[abilityId] || {};\n init.mod = ability.mod ?? 0;\n\n // Initiative proficiency\n const isLegacy = dnd5e.settings.rulesVersion === \"legacy\";\n const prof = this.attributes.prof ?? 0;\n const joat = flags.jackOfAllTrades && isLegacy;\n const ra = this.parent._isRemarkableAthlete(abilityId);\n const alert = flags.initiativeAlert && !isLegacy;\n init.prof = new Proficiency(prof, alert ? 1 : (joat || ra) ? 0.5 : 0, !ra);\n\n // Adjust rolling mode\n if ( (flags.remarkableAthlete && !isLegacy) || this.parent.hasConditionEffect(\"initiativeAdvantage\") ) {\n AdvantageModeField.setMode(this, \"attributes.init.roll.mode\", 1);\n }\n if ( this.parent.hasConditionEffect(\"initiativeDisadvantage\") ) {\n AdvantageModeField.setMode(this, \"attributes.init.roll.mode\", -1);\n }\n\n // Total initiative includes all numeric terms\n const initBonus = simplifyBonus(init.bonus, rollData);\n const abilityBonus = simplifyBonus(ability.bonuses?.check, rollData);\n const quality = this.attributes.quality?.value ?? 0;\n init.total = init.mod + initBonus + abilityBonus + globalCheckBonus + quality\n + (flags.initiativeAlert && isLegacy ? 5 : 0)\n + (Number.isNumeric(init.prof.term) ? init.prof.flat : 0);\n init.score = CONFIG.DND5E.skillPassive.base + init.total + (init.roll.mode * CONFIG.DND5E.skillPassive.modifier);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Modify movement speeds taking exhaustion and any other conditions into account.\n * @this {CharacterData|NPCData}\n * @param {ActorRollData} rollData The Actor's roll data.\n */\n static prepareMovement(rollData=this.parent.getRollData()) {\n const statuses = this.parent.statuses;\n const noMovement = this.parent.hasConditionEffect(\"noMovement\");\n const crawl = this.parent.hasConditionEffect(\"crawl\");\n for ( const type of Object.keys(CONFIG.DND5E.movementTypes) ) {\n if ( noMovement || (crawl && (type !== \"walk\")) ) this.attributes.movement[type] = 0;\n else this.attributes.movement[type] = Math.max(0, simplifyBonus(this.attributes.movement[type], rollData));\n if ( type === \"walk\" ) this.attributes.movement.speed = this.attributes.movement.walk;\n }\n\n const halfMovement = this.parent.hasConditionEffect(\"halfMovement\");\n const encumbered = statuses.has(\"encumbered\");\n const heavilyEncumbered = statuses.has(\"heavilyEncumbered\");\n const exceedingCarryingCapacity = statuses.has(\"exceedingCarryingCapacity\");\n const units = this.attributes.movement.units ??= defaultUnits(\"length\");\n let reduction = dnd5e.settings.rulesVersion === \"modern\" && !this.traits?.ci?.value?.has(\"exhaustion\")\n ? (this.attributes.exhaustion ?? 0) * (CONFIG.DND5E.conditionTypes.exhaustion?.reduction?.speed ?? 0) : 0;\n reduction = convertLength(reduction, CONFIG.DND5E.defaultUnits.length.imperial, units);\n const bonus = simplifyBonus(this.attributes.movement.bonus, rollData);\n this.attributes.movement.max = 0;\n for ( const type of Object.keys(CONFIG.DND5E.movementTypes) ) {\n let speed = Math.max(0, this.attributes.movement[type] - reduction);\n if ( speed ) {\n speed = Math.max(0, speed + bonus);\n if ( halfMovement ) speed *= 0.5;\n if ( heavilyEncumbered ) {\n speed = Math.max(0, speed - (CONFIG.DND5E.encumbrance.speedReduction.heavilyEncumbered[units] ?? 0));\n } else if ( encumbered ) {\n speed = Math.max(0, speed - (CONFIG.DND5E.encumbrance.speedReduction.encumbered[units] ?? 0));\n }\n if ( exceedingCarryingCapacity ) {\n speed = Math.min(speed, CONFIG.DND5E.encumbrance.speedReduction.exceedingCarryingCapacity[units] ?? 0);\n }\n }\n this.attributes.movement[type] = speed;\n this.attributes.movement.max = Math.max(speed, this.attributes.movement.max);\n if ( type === \"walk\" ) this.attributes.movement.speed = speed;\n }\n const baseSpeed = this._source.attributes.movement.walk || this.attributes.movement.fromSpecies?.walk;\n this.attributes.movement.slowed = this.attributes.movement.walk <= (simplifyBonus(baseSpeed, rollData) / 2);\n this.attributes.movement.jump = (this.abilities?.str.value ?? 0) / 2;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Apply movement and sense changes based on a race item. This method should be called during\n * the `prepareEmbeddedData` step of data preparation.\n * @param {Item5e} race Race item from which to get the stats.\n * @param {object} [options={}]\n * @param {boolean} [options.force=false] Override any values on the actor.\n * @this {CharacterData|NPCData}\n */\n static prepareRace(race, { force=false }={}) {\n for ( const key of Object.keys(CONFIG.DND5E.movementTypes) ) {\n if ( !race.system.movement[key] || (!force && this.attributes.movement[key]) ) continue;\n this.attributes.movement.fromSpecies ??= {};\n this.attributes.movement[key] = this.attributes.movement.fromSpecies[key] = race.system.movement[key];\n }\n if ( race.system.movement.hover ) this.attributes.movement.hover = true;\n if ( force && race.system.movement.units ) this.attributes.movement.units = race.system.movement.units;\n else this.attributes.movement.units ??= race.system.movement.units;\n\n for ( const key of Object.keys(CONFIG.DND5E.senses) ) {\n if ( !race.system.senses.ranges[key] || (!force && (this.attributes.senses.ranges[key] !== null)) ) continue;\n this.attributes.senses.ranges[key] = race.system.senses.ranges[key];\n }\n this.attributes.senses.special = [this.attributes.senses.special, race.system.senses.special].filterJoin(\";\");\n if ( force && race.system.senses.units ) this.attributes.senses.units = race.system.senses.units;\n else this.attributes.senses.units ??= race.system.senses.units;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare spellcasting DC & modifier.\n * @this {CharacterData|NPCData}\n */\n static prepareSpellcastingAbility() {\n const ability = this.abilities?.[this.attributes.spellcasting];\n this.attributes.spell ??= {};\n this.attributes.spell.abilityLabel = CONFIG.DND5E.abilities[this.attributes.spellcasting]?.label ?? \"\";\n this.attributes.spell.attack = ability ? ability.attack : this.attributes.prof;\n this.attributes.spell.dc = ability ? ability.dc : 8 + this.attributes.prof;\n this.attributes.spell.mod = ability ? ability.mod : 0;\n }\n\n /* -------------------------------------------- */\n /* Socket Event Handlers */\n /* -------------------------------------------- */\n\n /**\n * Track changes to HP when updated and set death save status.\n * @this {CharacterData|NPCData|VehicleData}\n * @param {object} changes The candidate changes to the Document.\n * @param {object} options Additional options which modify the update request.\n * @param {BaseUser} user The User requesting the document update.\n */\n static async preUpdateHP(changes, options, user) {\n const isDead = this.attributes.hp.value <= 0;\n if ( isDead && (foundry.utils.getProperty(changes, \"system.attributes.hp.value\") > 0) ) {\n foundry.utils.setProperty(changes, \"system.attributes.death.success\", 0);\n foundry.utils.setProperty(changes, \"system.attributes.death.failure\", 0);\n }\n foundry.utils.setProperty(options, \"dnd5e.hp\", { ...this.attributes.hp });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Display concentration challenge if necessary, set bloodied status, and fire damage hook.\n * @this {CharacterData|NPCData|VehicleData}\n * @param {object} changed The differential data that was changed relative to the document's prior values.\n * @param {object} options Additional options which modify the update request.\n * @param {string} userId The id of the User requesting the document update.\n */\n static async onUpdateHP(changed, options, userId) {\n if ( !changed.system?.attributes?.hp ) return;\n if ( userId === game.userId ) await this.parent.updateBloodied(options);\n\n const hp = options.dnd5e?.hp;\n if ( !hp || options.isRest || options.isAdvancement ) return;\n\n const curr = this.attributes.hp;\n const changes = {\n hp: curr.value - hp.value,\n temp: curr.temp - hp.temp\n };\n changes.total = changes.hp + changes.temp;\n if ( !Number.isInteger(changes.total) || (changes.total === 0) ) return;\n\n this.parent._displayTokenEffect(changes);\n if ( !game.settings.get(\"dnd5e\", \"disableConcentration\") && (userId === game.userId)\n && (options.dnd5e?.concentrationCheck !== false)\n && (changes.total < 0) && ((changes.temp < 0) || (curr.value < curr.effectiveMax)) ) {\n this.parent.challengeConcentration({ dc: this.parent.getConcentrationDC(-changes.total) });\n }\n\n /**\n * A hook event that fires when an actor is damaged or healed by any means. The actual name\n * of the hook will depend on the change in hit points.\n * @function dnd5e.damageActor\n * @memberof hookEvents\n * @param {Actor5e} actor The actor that had their hit points reduced.\n * @param {{hp: number, temp: number, total: number}} changes The changes to hit points.\n * @param {object} update The original update delta.\n * @param {string} userId Id of the user that performed the update.\n */\n Hooks.callAll(`dnd5e.${changes.total > 0 ? \"heal\" : \"damage\"}Actor`, this.parent, changes, changed, userId);\n }\n}\n","import Proficiency from \"../../documents/actor/proficiency.mjs\";\nimport SystemDataModel from \"./system-data-model.mjs\";\n\n/**\n * @import { ActorRollData, CombatRecoveryResults, RollDataOptions } from \"../../documents/_types.mjs\";\n * @import { ActorDataModelMetadata } from \"./_types.mjs\";\n */\n\n/**\n * Variant of the SystemDataModel with some extra actor-specific handling.\n */\nexport default class ActorDataModel extends SystemDataModel {\n\n /** @type {ActorDataModelMetadata} */\n static metadata = Object.freeze(foundry.utils.mergeObject(super.metadata, {\n supportsAdvancement: false\n }, { inplace: false }));\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /** @override */\n get embeddedDescriptionKeyPath() {\n return \"details.biography.value\";\n }\n\n /* -------------------------------------------- */\n\n /**\n * Section of the group sheet this actor will render within.\n * @type {string}\n */\n get groupSection() {\n return this.parent.type;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Other actors that are available for currency transfers from this actor.\n * @type {Actor5e[]}\n */\n get transferDestinations() {\n const primaryParty = game.actors.party;\n if ( !primaryParty?.system.members.ids.has(this.parent.id) ) return [];\n const destinations = primaryParty.system.members.map(m => m.actor).filter(a => a.isOwner && a !== this.parent);\n if ( primaryParty.isOwner ) destinations.unshift(primaryParty);\n return destinations;\n }\n\n /* -------------------------------------------- */\n /* Data Preparation */\n /* -------------------------------------------- */\n\n /**\n * Data preparation steps to perform after item data has been prepared, but before active effects are applied.\n */\n prepareEmbeddedData() {\n this._prepareScaleValues();\n }\n\n /* -------------------------------------------- */\n\n /**\n * Derive any values that have been scaled by the Advancement system.\n * Mutates the value of the `system.scale` object.\n * @protected\n */\n _prepareScaleValues() {\n this.scale = this.parent.items.reduce((scale, item) => {\n const scaleValues = item.scaleValues;\n if ( !foundry.utils.isEmpty(scaleValues) ) scale[item.identifier] = scaleValues;\n return scale;\n }, {});\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /**\n * Prepare a data object which defines the data schema used by dice roll commands against this Actor.\n * @param {RollDataOptions} [options]\n * @returns {ActorRollData}\n */\n getRollData({ deterministic=false }={}) {\n const data = { ...this };\n data.prof = new Proficiency(this.attributes?.prof ?? 0, 1);\n data.prof.deterministic = deterministic;\n return data;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Reset combat-related uses.\n * @param {string[]} periods Which recovery periods should be considered.\n * @param {CombatRecoveryResults} results Updates to perform on the actor and containing items.\n */\n async recoverCombatUses(periods, results) {}\n}\n","import Proficiency from \"../../../documents/actor/proficiency.mjs\";\nimport { simplifyBonus } from \"../../../utils.mjs\";\nimport ActorDataModel from \"../../abstract/actor-data-model.mjs\";\nimport AdvantageModeField from \"../../fields/advantage-mode-field.mjs\";\nimport FormulaField from \"../../fields/formula-field.mjs\";\nimport MappingField from \"../../fields/mapping-field.mjs\";\nimport CurrencyTemplate from \"../../shared/currency.mjs\";\nimport RollConfigField from \"../../shared/roll-config-field.mjs\";\n\nconst { NumberField, SchemaField } = foundry.data.fields;\n\n/**\n * @import { ActorRollData } from \"../../../documents/_types.mjs\";\n * @import { CurrencyTemplateData } from \"../../shared/_types.mjs\";\n * @import { CommonTemplateData } from \"./_types.mjs\";\n */\n\n/**\n * A template for all actors that share the common template.\n * @extends {ActorDataModel}\n * @mixes CurrencyTemplate\n * @mixes CommonTemplateData\n */\nexport default class CommonTemplate extends ActorDataModel.mixin(CurrencyTemplate) {\n\n /* -------------------------------------------- */\n /* Model Configuration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static defineSchema() {\n return this.mergeSchema(super.defineSchema(), {\n abilities: new MappingField(new SchemaField({\n value: new NumberField({\n required: true, nullable: false, integer: true, min: 0, initial: 10, label: \"DND5E.AbilityScore\"\n }),\n proficient: new NumberField({\n required: true, integer: true, min: 0, max: 1, initial: 0, label: \"DND5E.ProficiencyLevel\"\n }),\n max: new NumberField({\n required: true, integer: true, nullable: true, min: 0, initial: null, label: \"DND5E.AbilityScoreMax\"\n }),\n bonuses: new SchemaField({\n check: new FormulaField({ required: true, label: \"DND5E.AbilityCheckBonus\" }),\n save: new FormulaField({ required: true, label: \"DND5E.SaveBonus\" })\n }, { label: \"DND5E.AbilityBonuses\" }),\n check: new RollConfigField({ ability: false }),\n save: new RollConfigField({ ability: false })\n }), {\n initialKeys: CONFIG.DND5E.abilities, initialValue: this._initialAbilityValue.bind(this),\n initialKeysOnly: true, label: \"DND5E.Abilities\"\n })\n });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Populate the proper initial value for abilities.\n * @param {string} key Key for which the initial data will be created.\n * @param {object} initial The initial skill object created by SkillData.\n * @param {object} existing Any existing mapping data.\n * @returns {object} Initial ability object.\n * @private\n */\n static _initialAbilityValue(key, initial, existing) {\n const config = CONFIG.DND5E.abilities[key];\n if ( config ) {\n let defaultValue = config.defaults?.[this._systemType] ?? initial.value;\n if ( typeof defaultValue === \"string\" ) defaultValue = existing?.[defaultValue]?.value ?? initial.value;\n initial.value = defaultValue;\n }\n return initial;\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static _migrateData(source) {\n super._migrateData(source);\n CommonTemplate.#migrateACData(source);\n CommonTemplate.#migrateMovementData(source);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Migrate the actor ac.value to new ac.flat override field.\n * @param {object} source The candidate source data from which the model will be constructed.\n */\n static #migrateACData(source) {\n if ( !source.attributes?.ac ) return;\n const ac = source.attributes.ac;\n\n // If the actor has a numeric ac.value, then their AC has not been migrated to the auto-calculation schema yet.\n if ( Number.isNumeric(ac.value) ) {\n ac.flat = parseInt(ac.value);\n ac.calc = this._systemType === \"npc\" ? \"natural\" : \"flat\";\n return;\n }\n\n // Migrate ac.base in custom formulas to ac.armor\n if ( (typeof ac.formula === \"string\") && ac.formula.includes(\"@attributes.ac.base\") ) {\n ac.formula = ac.formula.replaceAll(\"@attributes.ac.base\", \"@attributes.ac.armor\");\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Migrate the actor speed string to movement object.\n * @param {object} source The candidate source data from which the model will be constructed.\n */\n static #migrateMovementData(source) {\n const original = source.attributes?.speed?.value ?? source.attributes?.speed;\n if ( (typeof original !== \"string\") || (source.attributes.movement?.walk !== undefined) ) return;\n source.attributes.movement ??= {};\n const s = original.split(\" \");\n if ( s.length > 0 ) source.attributes.movement.walk = Number.isNumeric(s[0]) ? parseInt(s[0]) : 0;\n }\n\n /* -------------------------------------------- */\n /* Data Preparation */\n /* -------------------------------------------- */\n\n /**\n * Prepare modifiers and other values for abilities.\n * @param {object} [options={}]\n * @param {ActorRollData} [options.rollData={}] Roll data used to calculate bonuses.\n * @param {object} [options.originalSaves] Original ability data for transformed actors.\n */\n prepareAbilities({ rollData={}, originalSaves }={}) {\n const flags = this.parent.flags.dnd5e ?? {};\n const { prof = 0, ac } = this.attributes ?? {};\n Object.values(this.abilities).forEach(a => a.mod = Math.floor((a.value - 10) / 2));\n const checkBonus = simplifyBonus(this.bonuses?.abilities?.check, rollData);\n const saveBonus = simplifyBonus(this.bonuses?.abilities?.save, rollData);\n const dcBonus = simplifyBonus(this.bonuses?.spell?.dc, rollData);\n for ( const [id, abl] of Object.entries(this.abilities) ) {\n if ( flags.diamondSoul ) abl.proficient = 1; // Diamond Soul is proficient in all saves\n const originalAbility = originalSaves?.[id];\n if ( originalAbility?.proficient ) {\n abl.merged = true;\n abl.proficient = originalAbility?.proficient;\n }\n\n const calculatedProf = this.calculateAbilityCheckProficiency(0, id);\n abl.checkProf = originalAbility?.checkProf?.multiplier > calculatedProf.multiplier\n ? originalAbility.checkProf.clone() : calculatedProf;\n const saveBonusAbl = simplifyBonus(abl.bonuses?.save, rollData);\n\n const cover = id === \"dex\" ? Math.max(ac?.cover ?? 0, this.parent.coverBonus) : 0;\n abl.saveBonus = saveBonusAbl + saveBonus + cover;\n\n abl.saveProf = abl.merged ? originalAbility.saveProf.clone() : new Proficiency(prof, abl.proficient);\n const checkBonusAbl = simplifyBonus(abl.bonuses?.check, rollData);\n abl.checkBonus = checkBonusAbl + checkBonus;\n\n abl.save.value = abl.mod + abl.saveBonus;\n if ( Number.isNumeric(abl.saveProf.term) ) abl.save.value += abl.saveProf.flat;\n abl.attack = abl.mod + prof;\n abl.dc = 8 + abl.mod + prof + dcBonus;\n\n if ( !Number.isFinite(abl.max) ) abl.max = CONFIG.DND5E.maxAbilityScore;\n\n // Adjust rolling mode\n if ( this.parent.hasConditionEffect(\"abilityCheckDisadvantage\") ) {\n AdvantageModeField.setMode(this, `abilities.${id}.check.roll.mode`, -1);\n }\n if ( this.parent.hasConditionEffect(\"abilitySaveDisadvantage\")\n || ((id === \"dex\") && this.parent.hasConditionEffect(\"dexteritySaveDisadvantage\")) ) {\n AdvantageModeField.setMode(this, `abilities.${id}.save.roll.mode`, -1);\n }\n }\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /**\n * Create the proficiency object for an ability, skill, or tool, taking remarkable athlete and Jack of All Trades\n * into account.\n * @param {number} multiplier Multiplier stored on the actor.\n * @param {string} ability Ability associated with this proficiency.\n * @param {object} [options={}]\n * @param {string} [options.skill] Skill associated with this proficiency.\n * @param {string} [options.tool] Tool associated with this proficiency.\n * @returns {Proficiency}\n */\n calculateAbilityCheckProficiency(multiplier, ability, options={}) {\n let roundDown = true;\n if ( (multiplier < 1) && ((game.settings.get(\"dnd5e\", \"rulesVersion\") === \"legacy\") || options.skill) ) {\n if ( this.parent._isRemarkableAthlete(ability) ) {\n multiplier = .5;\n roundDown = false;\n }\n else if ( this.parent.flags.dnd5e?.jackOfAllTrades ) multiplier = .5;\n }\n return new Proficiency(this.attributes.prof, multiplier, roundDown);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Calculate proficiency, applying specific logic for tools.\n * @param {number} multiplier Multiplier stored on the actor.\n * @param {string} ability Ability associated with this proficiency.\n * @param {object} [options={}]\n * @param {string} [options.skill] Skill associated with this proficiency.\n * @param {string} [options.tool] Tool associated with this proficiency.\n * @returns {Proficiency}\n */\n calculateToolProficiency(multiplier, ability, options={}) {\n if ( (multiplier === 1) && this.parent.flags.dnd5e?.toolExpertise ) {\n return new Proficiency(this.attributes.prof, 2, true);\n }\n return this.calculateAbilityCheckProficiency(multiplier, ability, options);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Calculate proficiency for a given actor using either a skill, a tool, or both.\n * @param {Actor5e} actor The actor.\n * @param {string} abilityId The ability used with the check.\n * @param {object} [options]\n * @param {string} [options.skill] The skill.\n * @param {string} [options.tool] The tool.\n * @returns {Proficiency|null}\n */\n static calculateSkillToolProficiency(actor, abilityId, options={}) {\n if ( !actor ) return null;\n const skill = actor.system.skills?.[options.skill];\n const tool = actor.system.tools?.[options.tool];\n const multiplier = Math.max(skill?.effectValue ?? 0, tool?.effectValue ?? 0);\n const calc = options.tool ? actor.system.calculateToolProficiency : actor.system.calculateAbilityCheckProficiency;\n return calc.call(actor.system, multiplier, abilityId, options);\n }\n}\n","import LocalDocumentField from \"../../fields/local-document-field.mjs\";\nconst { HTMLField, SchemaField, StringField } = foundry.data.fields;\n\n/**\n * @import { DetailsCommonData, DetailsCreatureData } from \"./_types.mjs\";\n */\n\n/**\n * Shared contents of the details schema between various actor types.\n */\nexport default class DetailsField {\n /**\n * Fields shared between characters, NPCs, and vehicles.\n * @type {DetailsCommonData}\n */\n static get common() {\n return {\n biography: new SchemaField({\n value: new HTMLField({label: \"DND5E.Biography\"}),\n public: new HTMLField({label: \"DND5E.BiographyPublic\"})\n }, {label: \"DND5E.Biography\"})\n };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Fields shared between characters and NPCs.\n * @type {DetailsCreatureData}\n */\n static get creature() {\n return {\n alignment: new StringField({required: true, label: \"DND5E.Alignment\"}),\n ideal: new StringField({required: true, label: \"DND5E.Ideals\"}),\n bond: new StringField({required: true, label: \"DND5E.Bonds\"}),\n flaw: new StringField({required: true, label: \"DND5E.Flaws\"}),\n race: new LocalDocumentField(foundry.documents.BaseItem, {\n required: true, fallback: true, label: \"DND5E.Species\"\n })\n };\n }\n}\n","const { SchemaField, SetField, StringField } = foundry.data.fields;\n\n/**\n * Field for storing standard trait data.\n */\nexport default class SimpleTraitField extends SchemaField {\n constructor(fields={}, { initialValue=[], ...options }={}) {\n fields = {\n value: new SetField(new StringField(), { label: \"DND5E.TraitsChosen\", initial: initialValue }),\n custom: new StringField({ required: true, label: \"DND5E.Special\" }),\n ...fields\n };\n Object.entries(fields).forEach(([k, v]) => !v ? delete fields[k] : null);\n super(fields, options);\n }\n}\n","import SimpleTraitField from \"./simple-trait-field.mjs\";\nconst { SetField, StringField } = foundry.data.fields;\n\n/**\n * Field for storing damage resistances, immunities, and vulnerabilities data.\n */\nexport default class DamageTraitField extends SimpleTraitField {\n constructor(fields={}, { initialBypasses=[], ...options }={}) {\n super({\n bypasses: new SetField(new StringField(), {\n label: \"DND5E.DAMAGE.PhysicalBypass.Label\", hint: \"DND5E.DAMAGE.PhysicalBypass.Hint\", initial: initialBypasses\n })\n }, options);\n }\n}\n","import { defaultUnits, formatLength, splitSemicolons } from \"../../../utils.mjs\";\nimport FormulaField from \"../../fields/formula-field.mjs\";\nimport MappingField from \"../../fields/mapping-field.mjs\";\nimport DamageTraitField from \"../fields/damage-trait-field.mjs\";\nimport SimpleTraitField from \"../fields/simple-trait-field.mjs\";\n\nconst { NumberField, SchemaField, SetField, StringField } = foundry.data.fields;\n\n/**\n * @import { TraitsCommonData, TraitsCreatureData } from \"./_types.mjs\";\n */\n\n/**\n * Shared contents of the traits schema between various actor types.\n */\nexport default class TraitsField {\n /**\n * Fields shared between characters, NPCs, and vehicles.\n * @type {TraitsCommonData}\n */\n static get common() {\n return {\n size: new StringField({ required: true, initial: \"med\", label: \"DND5E.Size\" }),\n di: new DamageTraitField({}, { label: \"DND5E.DamImm\" }),\n dr: new DamageTraitField({}, { label: \"DND5E.DamRes\" }),\n dv: new DamageTraitField({}, { label: \"DND5E.DamVuln\" }),\n dm: new SchemaField({\n amount: new MappingField(new FormulaField({ deterministic: true }), { label: \"DND5E.DamMod\" }),\n bypasses: new SetField(new StringField(), {\n label: \"DND5E.DAMAGE.PhysicalBypass.Label\", hint: \"DND5E.DAMAGE.PhysicalBypass.Hint\"\n })\n }),\n ci: new SimpleTraitField({}, { label: \"DND5E.ConImm\" })\n };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Fields shared between characters and NPCs.\n * @type {TraitsCreatureData}\n */\n static get creature() {\n return {\n languages: new SimpleTraitField({\n communication: new MappingField(new SchemaField({\n units: new StringField({ initial: () => defaultUnits(\"length\") }),\n value: new NumberField({ required: true, min: 0 })\n }))\n }, { label: \"DND5E.Languages\" })\n };\n }\n\n /* -------------------------------------------- */\n /* Data Preparation */\n /* -------------------------------------------- */\n\n /**\n * Prepare the language labels.\n * @this {CharacterData|NPCData}\n */\n static prepareLanguages() {\n const languages = this.traits.languages;\n const labels = languages.labels = { languages: [], ranged: [] };\n\n if ( languages.value.has(\"ALL\") ) labels.languages.push(game.i18n.localize(\"DND5E.Language.All\"));\n else {\n const processCategory = (key, data, group) => {\n // If key is within languages, don't bother with children\n if ( languages.value.has(key) ) (group?.children ?? labels.languages).push(data.label ?? data);\n\n // Display children as part of this group (e.g. \"Primordial (Ignan)\")\n else if ( data.children && (data.selectable !== false) ) {\n const topLevel = group === undefined;\n group ??= { label: data.label, children: [] };\n Object.entries(data.children).forEach(([k, d]) => processCategory(k, d, group));\n if ( topLevel && group.children.length ) labels.languages.push(\n `${data.label} (${game.i18n.getListFormatter({ type: \"unit\" }).format(group.children)})`\n );\n }\n\n // Display children alone if category isn't selectable\n else if ( data.children ) Object.entries(data.children).forEach(([k, d]) => processCategory(k, d));\n };\n\n for ( const [key, data] of Object.entries(CONFIG.DND5E.languages) ) {\n if ( data.children ) Object.entries(data.children).forEach(([k, d]) => processCategory(k, d));\n else processCategory(key, data);\n }\n }\n\n labels.languages.push(...splitSemicolons(languages.custom));\n\n for ( const [key, { label }] of Object.entries(CONFIG.DND5E.communicationTypes) ) {\n const data = languages.communication?.[key];\n if ( !data?.value ) continue;\n labels.ranged.push(`${label} ${formatLength(data.value, data.units)}`);\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare condition immunities & petrified condition and handle \"All Damage\" value.\n * @this {CharacterData|NPCData|VehicleData}\n */\n static prepareResistImmune() {\n // Apply condition immunities\n for ( const condition of this.traits.ci.value ) this.parent.statuses.delete(condition);\n\n // Apply petrified condition\n if ( this.parent.hasConditionEffect(\"petrification\") ) {\n this.traits.dr.value.add(\"ALL\");\n this.traits.dr.bypasses.clear();\n this.traits.di.value.add(\"poison\");\n this.traits.ci.value.add(\"poisoned\");\n this.traits.ci.value.add(\"diseased\");\n }\n\n // Clear other damage resistances/immunities/vulnerabilities if All is set\n for ( const key of [\"dr\", \"di\", \"dv\"] ) {\n const entry = this.traits[key];\n if ( entry.value.has(\"ALL\") ) {\n entry.value.clear();\n entry.value.add(\"ALL\");\n if ( key === \"di\" ) this.traits.dr.value.clear();\n }\n else if ( key === \"di\" ) entry.value.forEach(k => this.traits.dr.value.delete(k));\n }\n }\n\n /* -------------------------------------------- */\n /* Socket Event Handlers */\n /* -------------------------------------------- */\n\n /**\n * Update the prototype token size for newly created actors.\n * @this {CharacterData|NPCData|VehicleData}\n * @param {object} data The initial data object provided to the document creation request.\n * @param {object} options Additional options which modify the creation request.\n */\n static async preCreateSize(data, options) {\n if ( this.parent._stats?.compendiumSource?.startsWith(\"Compendium.\") ) return;\n const prototypeToken = {};\n if ( \"size\" in this.traits ) {\n const size = CONFIG.DND5E.actorSizes[this.traits.size || \"med\"].token ?? 1;\n if ( !foundry.utils.hasProperty(data, \"prototypeToken.width\") ) prototypeToken.width = size;\n if ( !foundry.utils.hasProperty(data, \"prototypeToken.height\") ) prototypeToken.height = size;\n }\n this.parent.updateSource({ prototypeToken });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Update the prototype token size when the actor size is changed.\n * @this {CharacterData|NPCData|VehicleData}\n * @param {object} changes The candidate changes to the Document.\n * @param {object} options Additional options which modify the update request.\n */\n static async preUpdateSize(changes, options) {\n const newSize = foundry.utils.getProperty(changes, \"system.traits.size\");\n if ( !newSize || (newSize === this.traits.size)\n || foundry.utils.hasProperty(changes, \"prototypeToken.width\") ) return;\n const size = CONFIG.DND5E.actorSizes[newSize].token ?? 1;\n changes.prototypeToken ??= {};\n changes.prototypeToken.height = size;\n changes.prototypeToken.width = size;\n }\n}\n","import { convertWeight, defaultUnits, parseDelta } from \"../../utils.mjs\";\nimport SourceField from \"../shared/source-field.mjs\";\nimport TravelField from \"./fields/travel-field.mjs\";\nimport AttributesFields from \"./templates/attributes.mjs\";\nimport CommonTemplate from \"./templates/common.mjs\";\nimport DetailsFields from \"./templates/details.mjs\";\nimport TraitsFields from \"./templates/traits.mjs\";\n\nconst { ArrayField, BooleanField, DocumentUUIDField, NumberField, SchemaField, StringField } = foundry.data.fields;\n\n/**\n * @import { PassengerData, VehicleActorSystemData } from \"./_types.mjs\";\n */\n\n/**\n * System data definition for Vehicles.\n * @extends {CreatureTemplate}\n * @mixes VehicleActorSystemData\n */\nexport default class VehicleData extends CommonTemplate {\n\n /* -------------------------------------------- */\n /* Model Configuration */\n /* -------------------------------------------- */\n\n /** @override */\n static LOCALIZATION_PREFIXES = [\"DND5E.SOURCE\"];\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static _systemType = \"vehicle\";\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static defineSchema() {\n return this.mergeSchema(super.defineSchema(), {\n attributes: new SchemaField({\n ...AttributesFields.common,\n ac: new SchemaField({\n ...AttributesFields.armorClass,\n calc: new StringField({ initial: \"flat\", label: \"DND5E.ArmorClassCalculation\" })\n }, { label: \"DND5E.ArmorClass\" }),\n hp: new SchemaField({\n ...AttributesFields.hitPoints,\n mt: new NumberField({\n required: true, integer: true, min: 0, label: \"DND5E.VEHICLE.Mishap.Threshold.label\"\n })\n }, { label: \"DND5E.HitPoints\" }),\n actions: new SchemaField({\n max: new NumberField({\n required: true, nullable: false, integer: true, initial: 3, min: 0, max: 3,\n label: \"DND5E.VEHICLE.FIELDS.attributes.actions.max.label\"\n }),\n spent: new NumberField({\n required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3,\n label: \"DND5E.VEHICLE.FIELDS.attributes.actions.spent.label\"\n }),\n stations: new BooleanField({\n required: true, initial: true, label: \"DND5E.VEHICLE.FIELDS.attributes.actions.stations.label\"\n }),\n thresholds: new SchemaField({\n 2: new NumberField({\n required: true, integer: true, min: 0,\n label: \"DND5E.VEHICLE.FIELDS.attributes.actions.thresholds.full.label\"\n }),\n 1: new NumberField({\n required: true, integer: true, min: 0,\n label: \"DND5E.VEHICLE.FIELDS.attributes.actions.thresholds.mid.label\"\n }),\n 0: new NumberField({\n required: true, integer: true, min: 0,\n label: \"DND5E.VEHICLE.FIELDS.attributes.actions.thresholds.min.label\"\n })\n }, { label: \"DND5E.VEHICLE.FIELDS.attributes.actions.thresholds.label\" })\n }, { label: \"DND5E.VEHICLE.FIELDS.attributes.actions.label\" }),\n capacity: new SchemaField({\n cargo: new SchemaField({\n value: new NumberField({ min: 0, label: \"DND5E.VEHICLE.FIELDS.attributes.capacity.cargo.value.label\" }),\n units: new StringField({\n required: true, blank: false, label: \"DND5E.UNITS.WEIGHT.Label\", initial: () => defaultUnits(\"weight\")\n })\n }, { label: \"DND5E.VEHICLE.FIELDS.attributes.capacity.cargo.value.label\" }),\n creature: new StringField({ required: true, label: \"DND5E.VehicleCreatureCapacity\" }) // FIXME: Leave in the model until we decide how to migrate it.\n }, { label: \"DND5E.VEHICLE.FIELDS.attributes.capacity.label\" }),\n price: new SchemaField({\n value: new NumberField({ initial: null, min: 0, label: \"DND5E.Price\" }),\n denomination: new StringField({\n required: true, blank: false, initial: () => CONFIG.DND5E.defaultCurrency, label: \"DND5E.Currency\"\n })\n }, { label: \"DND5E.Price\" }),\n quality: new SchemaField({\n value: new NumberField({ required: true, nullable: false, integer: true, min: -10, max: 10, initial: 4 })\n }),\n travel: new TravelField({ pace: false }, {\n initialTime: () => CONFIG.DND5E.travelTimes.vehicle, initialUnits: () => defaultUnits(\"travel\")\n })\n }, { label: \"DND5E.Attributes\" }),\n crew: new SchemaField({\n max: new NumberField({ min: 0, integer: true }),\n value: new ArrayField(new DocumentUUIDField({ type: \"Actor\" }))\n }),\n details: new SchemaField({\n ...DetailsFields.common,\n type: new StringField({ required: true, blank: false, initial: \"water\", label: \"DND5E.VEHICLE.Type.label\" })\n }, { label: \"DND5E.Details\" }),\n draft: new SchemaField({\n value: new ArrayField(new DocumentUUIDField({ type: \"Actor\" }))\n }),\n passengers: new SchemaField({\n max: new NumberField({ min: 0, integer: true }),\n value: new ArrayField(new DocumentUUIDField({ type: \"Actor\" }))\n }),\n source: new SourceField(),\n traits: new SchemaField({\n ...TraitsFields.common,\n size: new StringField({ required: true, blank: false, initial: \"lg\", label: \"DND5E.Size\" }),\n weight: new SchemaField({\n value: new NumberField({ min: 0, label: \"DND5E.Weight\" }),\n units: new StringField({\n required: true, blank: false, label: \"DND5E.UNITS.WEIGHT.Label\", initial: () => defaultUnits(\"weight\")\n })\n }, { label: \"DND5E.Weight\" }),\n keel: new SchemaField({\n value: new NumberField({ min: 0, label: \"DND5E.VEHICLE.FIELDS.traits.keel.value.label\" }),\n units: new StringField({\n required: true, blank: false, label: \"DND5E.UNITS.DISTANCE.Label\", initial: () => defaultUnits(\"length\")\n })\n }),\n beam: new SchemaField({\n value: new NumberField({ min: 0, label: \"DND5E.VEHICLE.FIELDS.traits.beam.value.label\" }),\n units: new StringField({\n required: true, blank: false, label: \"DND5E.UNITS.DISTANCE.Label\", initial: () => defaultUnits(\"length\")\n })\n }),\n dimensions: new StringField({ required: true, label: \"DND5E.Dimensions\" }) // FIXME: Leave in the model until we decide how to migrate it.\n }, { label: \"DND5E.Traits\" }),\n cargo: new SchemaField({ // FIXME: Leave in the model until we decide how to migrate it.\n crew: new ArrayField(makePassengerData()),\n passengers: new ArrayField(makePassengerData())\n })\n });\n }\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /**\n * Whether this Actor type represents a vehicle.\n * @returns {boolean}\n */\n get isVehicle() {\n return true;\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static _migrateData(source) {\n super._migrateData(source);\n AttributesFields._migrateInitiative(source.attributes);\n VehicleData.#migrateSource(source);\n VehicleData.#migrateMovement(source);\n VehicleData.#migrateType(source);\n VehicleData.#migrateCargoCapacity(source);\n VehicleData.#migrateActions(source);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Migrate actions from value to max.\n * @param {object} source The candidate source data from which the model will be constructed.\n */\n static #migrateActions(source) {\n const actions = source.attributes?.actions;\n if ( !actions || (actions.max !== undefined) || (actions.spent !== undefined) || (actions.value === undefined) ) {\n return;\n }\n actions.max = actions.value;\n delete actions.value;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Migrate cargo capacity from a number to an object with units.\n * @param {object} source The candidate source data from which the model will be constructed.\n */\n static #migrateCargoCapacity(source) {\n const cargo = source.attributes?.capacity?.cargo;\n if ( typeof cargo !== \"number\" ) return;\n source.attributes.capacity.cargo = { value: cargo, units: \"tn\" };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Migrate movement speeds to travel pace by taking the previous max speed and assigning it to a movement type\n * based on the vehicle's type.\n * @param {object} source The candidate source data from which the model will be constructed.\n */\n static #migrateMovement(source) {\n const movement = source.attributes?.movement;\n const { vehicleType } = source;\n let newUnits;\n if ( movement?.units === \"mi\" ) newUnits = \"mph\";\n else if ( movement?.units === \"km\" ) newUnits = \"kph\";\n if ( !vehicleType || !movement || !newUnits || !(\"walk\" in movement) || source.attributes?.travel ) return;\n let max = 0;\n for ( const p in CONFIG.DND5E.movementTypes ) {\n if ( movement[p] > max ) max = movement[p];\n delete movement[p];\n }\n source.attributes ??= {};\n source.attributes.travel = {\n [vehicleType === \"space\" ? \"air\" : vehicleType]: max,\n units: newUnits\n };\n movement.units = null;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Convert source string into custom object & move to top-level.\n * @param {object} source The candidate source data from which the model will be constructed.\n */\n static #migrateSource(source) {\n let custom;\n if ( (\"details\" in source) && (\"source\" in source.details) ) {\n if ( foundry.utils.getType(source.details?.source) === \"string\" ) custom = source.details.source;\n else source.source = source.details.source;\n }\n if ( custom ) source.source = { custom };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Convert vehicle type.\n * @param {object} source The candidate source data from which the model will be constructed.\n */\n static #migrateType(source) {\n if ( !source.vehicleType ) return;\n source.details ??= {};\n if ( source.details.type ) return;\n source.details.type = source.vehicleType;\n delete source.vehicleType;\n }\n\n /* -------------------------------------------- */\n /* Data Preparation */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareBaseData() {\n this.attributes.prof = 0;\n AttributesFields.prepareBaseArmorClass.call(this);\n AttributesFields.prepareBaseEncumbrance.call(this);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareDerivedData() {\n const rollData = this.parent.getRollData({ deterministic: true });\n const { originalSaves } = this.parent.getOriginalStats();\n\n this.prepareAbilities({ rollData, originalSaves });\n AttributesFields.prepareArmorClass.call(this, rollData);\n if ( this.attributes.ac.value ) {\n this.attributes.ac.motionless = this.attributes.ac.value - Math.max(0, this.abilities.dex?.mod ?? 0);\n }\n AttributesFields.prepareEncumbrance.call(this, rollData, { validateItem: item => !item.isMountable });\n AttributesFields.prepareHitPoints.call(this, this.attributes.hp);\n AttributesFields.prepareInitiative.call(this, rollData);\n AttributesFields.prepareMovement.call(this, rollData);\n SourceField.prepareData.call(this.source, this.parent._stats?.compendiumSource ?? this.parent.uuid);\n TraitsFields.prepareResistImmune.call(this);\n TravelField.prepareData.call(this, rollData);\n\n this._prepareActions();\n }\n\n /* -------------------------------------------- */\n\n /**\n * Perform preparation steps for action stations.\n */\n _prepareActions() {\n const { actions } = this.attributes;\n const crew = this.crew.value.length;\n\n if ( !actions.stations && actions.max ) {\n for ( let i = actions.max; i--; actions.max-- ) {\n const threshold = actions.thresholds[i];\n if ( Number.isFinite(threshold) && (crew >= threshold) ) break;\n }\n }\n\n actions.value = Math.clamp(actions.max - actions.spent, 0, actions.max);\n }\n\n /* -------------------------------------------- */\n /* Methods */\n /* -------------------------------------------- */\n\n /**\n * Adjust the crew quantity to some target value.\n * @param {string} area The crew area.\n * @param {string} uuid The crew member's UUID.\n * @param {string|number} target The target value, which may be a delta.\n * @returns {Promise} The actor with updates applied.\n */\n async adjustCrew(area, uuid, target) {\n const updates = this.getCrewUpdates(area, uuid, target);\n if ( foundry.utils.isEmpty(updates) ) return this.parent;\n return this.parent.update(updates);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Compute the update required in order to adjust the crew to some target quantity.\n * @param {string} area The crew area.\n * @param {string} uuid The crew member's UUID.\n * @param {string|number} target The target value, which may be a delta.\n * @returns {object}\n */\n getCrewUpdates(area, uuid, target) {\n const roster = this[area].value;\n const quantity = roster.reduce((acc, u) => acc + (u === uuid), 0);\n target = Math.max(0, typeof target === \"number\" ? target : parseDelta(target, quantity));\n const diff = target - quantity;\n const updates = {};\n if ( diff > 0 ) updates[`system.${area}.value`] = roster.concat(Array.fromRange(diff).map(() => uuid));\n else if ( diff < 0 ) {\n let count = quantity;\n const newRoster = [];\n for ( let i = roster.length; i--; ) {\n const u = roster[i];\n if ( (count > target) && (u === uuid) ) count--;\n else newRoster.push(u);\n }\n updates[`system.${area}.value`] = newRoster;\n }\n return updates;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Get vehicle encumbrance including draft animals.\n * @returns {Promise<{ pct: number, max: number, value: number }>}\n */\n async getEncumbrance() {\n const encumbrance = foundry.utils.deepClone(this.attributes.encumbrance);\n if ( Number.isFinite(encumbrance.max) || !this.draft?.value.length ) return encumbrance; // Encumbrance already calculated.\n const { baseUnits, draftMultiplier } = CONFIG.DND5E.encumbrance;\n const unitSystem = game.settings.get(\"dnd5e\", \"metricWeightUnits\") ? \"metric\" : \"imperial\";\n const units = baseUnits.default[unitSystem];\n encumbrance.max = (await Promise.all(this.draft.value.map(fromUuid))).reduce((n, actor) => {\n const capacity = actor.system.attributes?.encumbrance?.max || 0;\n return n + (capacity * draftMultiplier);\n }, 0);\n const { weight } = this.traits;\n if ( weight.value ) {\n encumbrance.max = Math.max(0, encumbrance.max - convertWeight(weight.value, weight.units, units));\n }\n if ( encumbrance.max ) encumbrance.pct = Math.clamp((encumbrance.value * 100) / encumbrance.max, 0, 100);\n return encumbrance;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n async recoverCombatUses(periods, results) {\n const { actions } = this.attributes;\n if ( !actions.stations && actions.max && (periods.includes(\"encounter\") || periods.includes(\"turnEnd\")) ) {\n results.actor[\"system.attributes.actions.spent\"] = 0;\n }\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /**\n * Whether the given activity should prompt for auto-consumption of a crew action.\n * @param {Activity} activity The activity.\n * @returns {boolean|void}\n */\n static canConsumeCrewAction(activity) {\n const { actor } = activity;\n return actor?.system.attributes?.actions?.stations === false;\n }\n\n /* -------------------------------------------- */\n /* Socket Event Handlers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _preCreate(data, options, user) {\n if ( (await super._preCreate(data, options, user)) === false ) return false;\n await TraitsFields.preCreateSize.call(this, data, options, user);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _preUpdate(changes, options, user) {\n if ( (await super._preUpdate(changes, options, user)) === false ) return false;\n await AttributesFields.preUpdateHP.call(this, changes, options, user);\n await TraitsFields.preUpdateSize.call(this, changes, options, user);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _onUpdate(changed, options, userId) {\n super._onUpdate(changed, options, userId);\n AttributesFields.onUpdateHP.call(this, changed, options, userId);\n }\n}\n\n/* -------------------------------------------- */\n\n/**\n * Produce the schema field for a simple trait.\n * @param {object} schemaOptions Options passed to the outer schema.\n * @returns {PassengerData}\n */\nfunction makePassengerData(schemaOptions={}) {\n return new SchemaField({\n name: new StringField({required: true}),\n quantity: new NumberField({\n required: true, nullable: false, integer: true, initial: 0, min: 0\n })\n }, schemaOptions);\n}\n","import CalendarData5e from \"./calendar-data.mjs\";\n\n/**\n * Extension of the core calendar with support for extra formatters.\n */\nexport class CalendarGreyhawk extends CalendarData5e {\n\n /* -------------------------------------------- */\n /* Formatter Functions */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static formatMonthDay(calendar, components, options) {\n return CalendarGreyhawk.formatLocalized(\n \"DND5E.CALENDAR.Greyhawk.Formatters.MonthDay\", calendar, components, options\n );\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static formatMonthDayYear(calendar, components, options) {\n return CalendarGreyhawk.formatLocalized(\n \"DND5E.CALENDAR.Greyhawk.Formatters.MonthDayYear\", calendar, components, options\n );\n }\n}\n\n/* -------------------------------------------- */\n\nexport const CALENDAR_OF_GREYHAWK = {\n name: \"Calendar of Greyhawk\",\n years: {\n yearZero: 576,\n firstWeekday: 0\n },\n months: {\n values: [\n {\n name: \"DND5E.CALENDAR.Greyhawk.Festival.Needfest\",\n ordinal: 1, days: 6 // Days 0–5\n },\n {\n name: \"DND5E.CALENDAR.Greyhawk.Month.Fireseek\",\n ordinal: 1, days: 28 // Days 5–33\n },\n {\n name: \"DND5E.CALENDAR.Greyhawk.Month.Readying\",\n ordinal: 2, days: 28 // Days 33–61\n },\n {\n name: \"DND5E.CALENDAR.Greyhawk.Month.Coldeven\",\n ordinal: 3, days: 28 // Days 61–89\n },\n {\n name: \"DND5E.CALENDAR.Greyhawk.Festival.Growfest\",\n ordinal: 4, days: 6 // Days 89–95\n },\n {\n name: \"DND5E.CALENDAR.Greyhawk.Month.Planting\",\n ordinal: 4, days: 28 // Days 95–123\n },\n {\n name: \"DND5E.CALENDAR.Greyhawk.Month.Flocktime\",\n ordinal: 5, days: 28 // Days 123–151\n },\n {\n name: \"DND5E.CALENDAR.Greyhawk.Month.Wealsun\",\n ordinal: 6, days: 28 // Days 151–179\n },\n {\n name: \"DND5E.CALENDAR.Greyhawk.Festival.Richfest\",\n ordinal: 7, days: 6 // Days 179–185\n },\n {\n name: \"DND5E.CALENDAR.Greyhawk.Month.Reaping\",\n ordinal: 7, days: 28 // Days 185–213\n },\n {\n name: \"DND5E.CALENDAR.Greyhawk.Month.Godmonth\",\n ordinal: 8, days: 28 // Days 213–241\n },\n {\n name: \"DND5E.CALENDAR.Greyhawk.Month.Harvester\",\n ordinal: 9, days: 28 // Days 241–269\n },\n {\n name: \"DND5E.CALENDAR.Greyhawk.Festival.Brewfest\",\n ordinal: 10, days: 6 // Days 269–275\n },\n {\n name: \"DND5E.CALENDAR.Greyhawk.Month.Patchwall\",\n ordinal: 10, days: 28 // Days 275-303\n },\n {\n name: \"DND5E.CALENDAR.Greyhawk.Month.Readyreat\",\n ordinal: 11, days: 28 // Days 303–331\n },\n {\n name: \"DND5E.CALENDAR.Greyhawk.Month.Sunsebb\",\n ordinal: 12, days: 28 // Days 331–359\n }\n ]\n },\n days: {\n values: [\n { name: \"DND5E.CALENDAR.Greyhawk.Day.Starday\", ordinal: 1 },\n { name: \"DND5E.CALENDAR.Greyhawk.Day.Sunday\", ordinal: 2 },\n { name: \"DND5E.CALENDAR.Greyhawk.Day.Moonday\", ordinal: 3 },\n { name: \"DND5E.CALENDAR.Greyhawk.Day.Godsday\", ordinal: 4 },\n { name: \"DND5E.CALENDAR.Greyhawk.Day.Waterday\", ordinal: 5 },\n { name: \"DND5E.CALENDAR.Greyhawk.Day.Earthday\", ordinal: 6 },\n { name: \"DND5E.CALENDAR.Greyhawk.Day.Freeday\", ordinal: 7 }\n ],\n daysPerYear: 360,\n hoursPerDay: 24,\n minutesPerHour: 60,\n secondsPerMinute: 60\n },\n seasons: {\n values: [\n { name: \"DND5E.CALENDAR.Greyhawk.Season.Spring\", dayStart: 48, dayEnd: 137 }, // Readying 15–Flocktime 14\n { name: \"DND5E.CALENDAR.Greyhawk.Season.Summer\", dayStart: 138, dayEnd: 227 }, // Flocktime 15–Godmonth 14\n { name: \"DND5E.CALENDAR.Greyhawk.Season.Fall\", dayStart: 228, dayEnd: 317 }, // Godmonth 15–Readyrest 14\n { name: \"DND5E.CALENDAR.Greyhawk.Season.Winter\", dayStart: 318, dayEnd: 47 } // Readrest 15–Readying 14\n ]\n }\n};\n","import CalendarData5e from \"./calendar-data.mjs\";\n\nconst { ArrayField, NumberField, SchemaField, StringField } = foundry.data.fields;\n\n/**\n * @import { CalendarConfigHarptosFestival } from \"./_types.mjs\";\n */\n\n/**\n * Extension of the core calendar with support for festivals days and extra formatters.\n */\nexport class CalendarHarptos extends CalendarData5e {\n /** @inheritDoc */\n static defineSchema() {\n const schema = super.defineSchema();\n return {\n ...schema,\n festivals: new ArrayField(new SchemaField({\n name: new StringField({ required: true }),\n month: new NumberField({ required: true, nullable: false, min: 1, integer: true }),\n day: new NumberField({ required: true, nullable: false, min: 1, integer: true })\n }))\n };\n }\n\n /* -------------------------------------------- */\n /* Calendar Helper Methods */\n /* -------------------------------------------- */\n\n /**\n * Find festival day for current day.\n * @param {number|Components} [time] Time to use when finding festival day, by default the current world time.\n * @returns {CalendarConfigHarptosFestival|null}\n */\n findFestivalDay(time=game.time.worldTime) {\n const components = typeof time === \"number\" ? this.timeToComponents(time) : time;\n return this.festivals\n .find(f => f.month === (components.month + 1) && f.day === (components.dayOfMonth + 1)) ?? null;\n }\n\n /* -------------------------------------------- */\n /* Formatter Functions */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static formatMonthDay(calendar, components, options) {\n const festivalDay = calendar.findFestivalDay(components);\n return festivalDay ? game.i18n.localize(festivalDay.name) : CalendarHarptos.formatLocalized(\n \"DND5E.CALENDAR.Harptos.Formatters.DayMonth\", calendar, components, options\n );\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static formatMonthDayYear(calendar, components, options) {\n const festivalDay = calendar.findFestivalDay(components);\n if ( festivalDay ) {\n const context = CalendarData5e.dateFormattingParts(calendar, components);\n context.day = game.i18n.localize(festivalDay.name);\n return game.i18n.format(\"DND5E.CALENDAR.Harptos.Formatters.FestivalDayYear\", context);\n }\n return CalendarHarptos.formatLocalized(\n \"DND5E.CALENDAR.Harptos.Formatters.DayMonthYear\", calendar, components, options\n );\n }\n}\n\n/* -------------------------------------------- */\n\nexport const CALENDAR_OF_HARPTOS = {\n name: \"Calendar of Harptos\",\n years: {\n yearZero: 1501,\n firstWeekday: 0,\n leapYear: {\n leapStart: 0,\n leapInterval: 4\n }\n },\n months: {\n values: [\n {\n name: \"DND5E.CALENDAR.Harptos.Month.Hammer\", abbreviation: \"DND5E.CALENDAR.Harptos.Month.HammerCommon\",\n ordinal: 1, days: 31 // Days: 0–30\n },\n {\n name: \"DND5E.CALENDAR.Harptos.Month.Alturiak\", abbreviation: \"DND5E.CALENDAR.Harptos.Month.AlturiakCommon\",\n ordinal: 2, days: 30 // Days: 30–60\n },\n {\n name: \"DND5E.CALENDAR.Harptos.Month.Ches\", abbreviation: \"DND5E.CALENDAR.Harptos.Month.ChesCommon\",\n ordinal: 3, days: 30 // Days: 60–90\n },\n {\n name: \"DND5E.CALENDAR.Harptos.Month.Tarsakh\", abbreviation: \"DND5E.CALENDAR.Harptos.Month.TarsakhCommon\",\n ordinal: 4, days: 31 // Days: 91–122\n },\n {\n name: \"DND5E.CALENDAR.Harptos.Month.Mirtul\", abbreviation: \"DND5E.CALENDAR.Harptos.Month.MirtulCommon\",\n ordinal: 5, days: 30 // Days: 122–152\n },\n {\n name: \"DND5E.CALENDAR.Harptos.Month.Kythorn\", abbreviation: \"DND5E.CALENDAR.Harptos.Month.KythornCommon\",\n ordinal: 6, days: 30 // Days: 152–182\n },\n {\n name: \"DND5E.CALENDAR.Harptos.Month.Flamerule\", abbreviation: \"DND5E.CALENDAR.Harptos.Month.FlameruleCommon\",\n ordinal: 7, days: 31, leapDays: 32 // Days: 182–213\n },\n {\n name: \"DND5E.CALENDAR.Harptos.Month.Eleasis\", abbreviation: \"DND5E.CALENDAR.Harptos.Month.EleasisCommon\",\n ordinal: 8, days: 30 // Days: 213–243\n },\n {\n name: \"DND5E.CALENDAR.Harptos.Month.Eleint\", abbreviation: \"DND5E.CALENDAR.Harptos.Month.EleintCommon\",\n ordinal: 9, days: 31 // Days: 243–273\n },\n {\n name: \"DND5E.CALENDAR.Harptos.Month.Marpenoth\", abbreviation: \"DND5E.CALENDAR.Harptos.Month.MarpenothCommon\",\n ordinal: 10, days: 30 // Days: 273–303\n },\n {\n name: \"DND5E.CALENDAR.Harptos.Month.Uktar\", abbreviation: \"DND5E.CALENDAR.Harptos.Month.UktarCommon\",\n ordinal: 11, days: 31 // Days: 303–334\n },\n {\n name: \"DND5E.CALENDAR.Harptos.Month.Nightal\", abbreviation: \"DND5E.CALENDAR.Harptos.Month.NightalCommon\",\n ordinal: 12, days: 30 // Days: 334–364\n }\n ]\n },\n days: {\n values: [\n { name: \"DND5E.CALENDAR.Harptos.Day.One\", ordinal: 1 },\n { name: \"DND5E.CALENDAR.Harptos.Day.Two\", ordinal: 2 },\n { name: \"DND5E.CALENDAR.Harptos.Day.Three\", ordinal: 3 },\n { name: \"DND5E.CALENDAR.Harptos.Day.Four\", ordinal: 4 },\n { name: \"DND5E.CALENDAR.Harptos.Day.Five\", ordinal: 5 },\n { name: \"DND5E.CALENDAR.Harptos.Day.Six\", ordinal: 6 },\n { name: \"DND5E.CALENDAR.Harptos.Day.Seven\", ordinal: 7 },\n { name: \"DND5E.CALENDAR.Harptos.Day.Eight\", ordinal: 8 },\n { name: \"DND5E.CALENDAR.Harptos.Day.Nine\", ordinal: 9 },\n { name: \"DND5E.CALENDAR.Harptos.Day.Ten\", ordinal: 10 }\n ],\n daysPerYear: 365,\n hoursPerDay: 24,\n minutesPerHour: 60,\n secondsPerMinute: 60\n },\n festivals: [\n { name: \"DND5E.CALENDAR.Harptos.Festival.Midwinter\", month: 1, day: 31 },\n { name: \"DND5E.CALENDAR.Harptos.Festival.Greengrass\", month: 4, day: 31 },\n { name: \"DND5E.CALENDAR.Harptos.Festival.Midsummer\", month: 7, day: 31 },\n { name: \"DND5E.CALENDAR.Harptos.Festival.Shieldmeet\", month: 7, day: 32 },\n { name: \"DND5E.CALENDAR.Harptos.Festival.Highharvestide\", month: 9, day: 31 },\n { name: \"DND5E.CALENDAR.Harptos.Festival.FeastOfTheMoon\", month: 11, day: 31 }\n ],\n seasons: {\n values: [\n { name: \"DND5E.CALENDAR.Harptos.Season.Spring\", dayStart: 79, dayEnd: 171 }, // 19 Ches–19 Kythorn\n { name: \"DND5E.CALENDAR.Harptos.Season.Summer\", dayStart: 172, dayEnd: 263 }, // 20 Kythorn–20 Eleint\n { name: \"DND5E.CALENDAR.Harptos.Season.Fall\", dayStart: 264, dayEnd: 353 }, // 21 Eleint–19 Uktar\n { name: \"DND5E.CALENDAR.Harptos.Season.Winter\", dayStart: 354, dayEnd: 78 } // 20 Uktar-18 Ches\n ]\n }\n};\n","import CalendarData5e from \"./calendar-data.mjs\";\n\n/**\n * Extension of the core calendar with support for extra formatters.\n */\nexport class CalendarKhorvaire extends CalendarData5e {\n\n /* -------------------------------------------- */\n /* Formatter Functions */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static formatMonthDay(calendar, components, options) {\n return CalendarKhorvaire.formatLocalized(\n \"DND5E.CALENDAR.Khorvaire.Formatters.DayMonth\", calendar, components, options\n );\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static formatMonthDayYear(calendar, components, options) {\n return CalendarKhorvaire.formatLocalized(\n \"DND5E.CALENDAR.Khorvaire.Formatters.DayMonthYear\", calendar, components, options\n );\n }\n}\n\n/* -------------------------------------------- */\n\nexport const CALENDAR_OF_KHORVAIRE = {\n name: \"Common Calendar of Khorvaire\",\n years: {\n yearZero: 998,\n firstWeekday: 0\n },\n months: {\n values: [\n {\n name: \"DND5E.CALENDAR.Khorvaire.Month.Zarantyr\",\n ordinal: 1, days: 28\n },\n {\n name: \"DND5E.CALENDAR.Khorvaire.Month.Olarune\",\n ordinal: 2, days: 28\n },\n {\n name: \"DND5E.CALENDAR.Khorvaire.Month.Therendor\",\n ordinal: 3, days: 28\n },\n {\n name: \"DND5E.CALENDAR.Khorvaire.Month.Eyre\",\n ordinal: 4, days: 28\n },\n {\n name: \"DND5E.CALENDAR.Khorvaire.Month.Dravago\",\n ordinal: 5, days: 28\n },\n {\n name: \"DND5E.CALENDAR.Khorvaire.Month.Nymm\",\n ordinal: 6, days: 28\n },\n {\n name: \"DND5E.CALENDAR.Khorvaire.Month.Lharvion\",\n ordinal: 7, days: 28\n },\n {\n name: \"DND5E.CALENDAR.Khorvaire.Month.Barrakas\",\n ordinal: 8, days: 28\n },\n {\n name: \"DND5E.CALENDAR.Khorvaire.Month.Rhaan\",\n ordinal: 9, days: 28\n },\n {\n name: \"DND5E.CALENDAR.Khorvaire.Month.Sypheros\",\n ordinal: 10, days: 28\n },\n {\n name: \"DND5E.CALENDAR.Khorvaire.Month.Aryth\",\n ordinal: 11, days: 28\n },\n {\n name: \"DND5E.CALENDAR.Khorvaire.Month.Vult\",\n ordinal: 12, days: 28\n }\n ]\n },\n days: {\n values: [\n { name: \"DND5E.CALENDAR.Khorvaire.Day.Sul\", ordinal: 1 },\n { name: \"DND5E.CALENDAR.Khorvaire.Day.Mol\", ordinal: 2 },\n { name: \"DND5E.CALENDAR.Khorvaire.Day.Zol\", ordinal: 3 },\n { name: \"DND5E.CALENDAR.Khorvaire.Day.Wir\", ordinal: 4 },\n { name: \"DND5E.CALENDAR.Khorvaire.Day.Zor\", ordinal: 5 },\n { name: \"DND5E.CALENDAR.Khorvaire.Day.Far\", ordinal: 6 },\n { name: \"DND5E.CALENDAR.Khorvaire.Day.Sar\", ordinal: 7 }\n ],\n daysPerYear: 336,\n hoursPerDay: 24,\n minutesPerHour: 60,\n secondsPerMinute: 60\n },\n seasons: {\n values: [\n { name: \"DND5E.CALENDAR.Khorvaire.Season.Spring\", monthStart: 3, monthEnd: 5 }, // Therendor–Dravago\n { name: \"DND5E.CALENDAR.Khorvaire.Season.Summer\", monthStart: 6, monthEnd: 8 }, // Nymm–Barrakas\n { name: \"DND5E.CALENDAR.Khorvaire.Season.Autumn\", monthStart: 9, monthEnd: 11 }, // Rhaan–Aryth\n { name: \"DND5E.CALENDAR.Khorvaire.Season.Winter\", monthStart: 12, monthEnd: 2 } // Vult–Olarune\n ]\n }\n};\n","import ActivitySheet from \"./activity-sheet.mjs\";\n\n/**\n * Sheet for the attack activity.\n */\nexport default class AttackSheet extends ActivitySheet {\n\n /** @inheritDoc */\n static DEFAULT_OPTIONS = {\n classes: [\"attack-activity\"]\n };\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static PARTS = {\n ...super.PARTS,\n identity: {\n template: \"systems/dnd5e/templates/activity/attack-identity.hbs\",\n templates: [\n ...super.PARTS.identity.templates,\n \"systems/dnd5e/templates/activity/parts/attack-identity.hbs\"\n ]\n },\n effect: {\n template: \"systems/dnd5e/templates/activity/attack-effect.hbs\",\n templates: [\n ...super.PARTS.effect.templates,\n \"systems/dnd5e/templates/activity/parts/attack-damage.hbs\",\n \"systems/dnd5e/templates/activity/parts/attack-details.hbs\",\n \"systems/dnd5e/templates/activity/parts/damage-part.hbs\",\n \"systems/dnd5e/templates/activity/parts/damage-parts.hbs\"\n ]\n }\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareEffectContext(context, options) {\n context = await super._prepareEffectContext(context, options);\n\n const availableAbilities = this.activity.availableAbilities;\n context.abilityOptions = [\n {\n value: \"\", label: game.i18n.format(\"DND5E.DefaultSpecific\", {\n default: this.activity.attack.type.classification === \"spell\"\n ? game.i18n.localize(\"DND5E.Spellcasting\").toLowerCase()\n : availableAbilities.size\n ? game.i18n.getListFormatter({ style: \"short\", type: \"disjunction\" }).format(\n Array.from(availableAbilities).map(a => CONFIG.DND5E.abilities[a].label.toLowerCase())\n )\n : game.i18n.localize(\"DND5E.None\").toLowerCase()\n })\n },\n { rule: true },\n { value: \"none\", label: game.i18n.localize(\"DND5E.None\") },\n { value: \"spellcasting\", label: game.i18n.localize(\"DND5E.Spellcasting\") },\n ...Object.entries(CONFIG.DND5E.abilities).map(([value, config]) => ({\n value, label: config.label, group: game.i18n.localize(\"DND5E.Abilities\")\n }))\n ];\n\n context.hasBaseDamage = this.item.system.offersBaseDamage;\n\n return context;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareIdentityContext(context, options) {\n context = await super._prepareIdentityContext(context, options);\n\n context.attackTypeOptions = Object.entries(CONFIG.DND5E.attackTypes)\n .map(([value, config]) => ({ value, label: config.label }));\n if ( this.item.system.validAttackTypes?.size ) context.attackTypeOptions.unshift({\n value: \"\",\n label: game.i18n.format(\"DND5E.DefaultSpecific\", {\n default: game.i18n.getListFormatter({ type: \"disjunction\" }).format(\n Array.from(this.item.system.validAttackTypes).map(t => CONFIG.DND5E.attackTypes[t].label.toLowerCase())\n )\n })\n });\n\n context.attackClassificationOptions = Object.entries(CONFIG.DND5E.attackClassifications)\n .map(([value, config]) => ({ value, label: config.label }));\n if ( this.item.system.attackClassification ) context.attackClassificationOptions.unshift({\n value: \"\",\n label: game.i18n.format(\"DND5E.DefaultSpecific\", {\n default: CONFIG.DND5E.attackClassifications[this.item.system.attackClassification].label.toLowerCase()\n })\n });\n\n return context;\n }\n}\n","import Scaling from \"../../documents/scaling.mjs\";\nimport EmbeddedDataField5e from \"../fields/embedded-data-field.mjs\";\nimport FormulaField from \"../fields/formula-field.mjs\";\n\nconst { BooleanField, NumberField, SchemaField, SetField, StringField } = foundry.data.fields;\n\n/**\n * Field for storing damage data.\n */\nexport default class DamageField extends EmbeddedDataField5e {\n constructor(options) {\n super(DamageData, options);\n }\n}\n\n/* -------------------------------------------- */\n\n/**\n * Data model that stores information on a single damage part.\n */\nexport class DamageData extends foundry.abstract.DataModel {\n\n /* -------------------------------------------- */\n /* Model Configuration */\n /* -------------------------------------------- */\n\n /** @override */\n static defineSchema() {\n return {\n number: new NumberField({ min: 0, integer: true }),\n denomination: new NumberField({ min: 0, integer: true }),\n bonus: new FormulaField(),\n types: new SetField(new StringField()),\n custom: new SchemaField({\n enabled: new BooleanField(),\n formula: new FormulaField()\n }),\n scaling: new SchemaField({\n mode: new StringField(),\n number: new NumberField({ initial: 1, min: 0, integer: true }),\n formula: new FormulaField()\n })\n };\n }\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /**\n * The default damage formula.\n * @type {string}\n */\n get formula() {\n if ( this.custom.enabled ) return this.custom.formula ?? \"\";\n return this._automaticFormula();\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /**\n * Produce the auto-generated formula from the `number`, `denomination`, and `bonus`.\n * @param {number} [increase=0] Amount to increase the die count.\n * @returns {string}\n * @protected\n */\n _automaticFormula(increase=0) {\n let formula;\n const number = (this.number ?? 0) + increase;\n if ( number && this.denomination ) formula = `${number}d${this.denomination}`;\n if ( this.bonus ) formula = formula ? `${formula} + ${this.bonus}` : this.bonus;\n return formula ?? \"\";\n }\n\n /* -------------------------------------------- */\n\n /**\n * Scale the damage by a number of steps using its configured scaling configuration.\n * @param {number|Scaling} increase Number of steps above base damage to scaling.\n * @returns {string}\n */\n scaledFormula(increase) {\n if ( increase instanceof Scaling ) increase = increase.increase;\n\n switch ( this.scaling.mode ) {\n case \"whole\": break;\n case \"half\": increase = Math.floor(increase * .5); break;\n default: increase = 0; break;\n }\n if ( !increase ) return this.formula;\n let formula;\n\n // If dice count scaling, increase the count on the first die rolled\n const dieIncrease = (this.scaling.number ?? 0) * increase;\n if ( this.custom.enabled ) {\n formula = this.custom.formula;\n formula = formula.replace(/^(\\d)+d/, (match, number) => `${Number(number) + dieIncrease}d`);\n } else {\n formula = this._automaticFormula(dieIncrease);\n }\n\n // If custom scaling included, modify to match increase and append for formula\n if ( this.scaling.formula ) {\n let roll = new Roll(this.scaling.formula);\n roll = roll.alter(increase, 0, { multiplyNumeric: true });\n formula = formula ? `${formula} + ${roll.formula}` : roll.formula;\n }\n\n return formula;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Step the die denomination up or down by a number of steps, sticking to proper die sizes. Will return `null` if\n * stepping reduced the denomination below minimum die size.\n * @param {number} [steps=1] Number of steps to increase or decrease the denomination.\n * @returns {number|null}\n */\n steppedDenomination(steps=1) {\n return CONFIG.DND5E.dieSteps[Math.min(\n CONFIG.DND5E.dieSteps.indexOf(this.denomination) + steps,\n CONFIG.DND5E.dieSteps.length - 1\n )] ?? null;\n }\n}\n","import simplifyRollFormula from \"../../dice/simplify-roll-formula.mjs\";\nimport { convertLength, formatLength, formatNumber, simplifyBonus } from \"../../utils.mjs\";\nimport FormulaField from \"../fields/formula-field.mjs\";\nimport DamageField from \"../shared/damage-field.mjs\";\nimport BaseActivityData from \"./base-activity.mjs\";\n\nconst { ArrayField, BooleanField, NumberField, SchemaField, StringField } = foundry.data.fields;\n\n/**\n * @import { AttackDamageRollProcessConfiguration } from \"../../dice/_types.mjs\";\n * @import { AttackActivityData } from \"./_types.mjs\";\n */\n\n/**\n * Data model for an attack activity.\n * @extends {BaseActivityData}\n * @mixes AttackActivityData\n */\nexport default class BaseAttackActivityData extends BaseActivityData {\n /** @inheritDoc */\n static defineSchema() {\n return {\n ...super.defineSchema(),\n attack: new SchemaField({\n ability: new StringField(),\n bonus: new FormulaField(),\n critical: new SchemaField({\n threshold: new NumberField({ integer: true, positive: true })\n }),\n flat: new BooleanField(),\n type: new SchemaField({\n value: new StringField(),\n classification: new StringField()\n })\n }),\n damage: new SchemaField({\n critical: new SchemaField({\n bonus: new FormulaField()\n }),\n includeBase: new BooleanField({ initial: true }),\n parts: new ArrayField(new DamageField())\n })\n };\n }\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /** @override */\n get ability() {\n if ( this.attack.ability === \"none\" ) return null;\n if ( this.attack.ability === \"spellcasting\" ) return this.spellcastingAbility;\n if ( this.attack.ability in CONFIG.DND5E.abilities ) return this.attack.ability;\n\n const availableAbilities = this.availableAbilities;\n if ( !availableAbilities?.size ) return null;\n if ( availableAbilities?.size === 1 ) return availableAbilities.first();\n const abilities = this.actor?.system.abilities ?? {};\n return availableAbilities.reduce((largest, ability) =>\n (abilities[ability]?.mod ?? -Infinity) > (abilities[largest]?.mod ?? -Infinity) ? ability : largest\n , availableAbilities.first());\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n get actionType() {\n const type = this.attack.type;\n return `${type.value === \"ranged\" ? \"r\" : \"m\"}${type.classification === \"spell\" ? \"sak\" : \"wak\"}`;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n get activationLabels() {\n const labels = super.activationLabels;\n if ( labels && (this.item.type === \"weapon\") && !this.range.override ) {\n labels.range = this.item.labels?.range ? this.item.labels.range : null;\n if ( this.item.labels?.reach ) labels.reach = this.item.labels.reach;\n }\n return labels;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Abilities that could potentially be used with this attack. Unless a specific ability is specified then\n * whichever ability has the highest modifier will be selected when making an attack.\n * @type {Set}\n */\n get availableAbilities() {\n // Defer to item if available and matching attack classification\n if ( this.item.system.availableAbilities && (this.item.type === this.attack.type.classification) ) {\n return this.item.system.availableAbilities;\n }\n\n // Natural weapons also defer to the item if using any classification other than spell.\n if ( this.item.system.availableAbilities && (this.item.system.type?.value === \"natural\")\n && (this.attack.type.classification !== \"spell\") ) {\n return this.item.system.availableAbilities;\n }\n\n // Spell attack not associated with a single class, use highest spellcasting ability on actor\n if ( this.attack.type.classification === \"spell\" ) return new Set(\n this.actor?.system.attributes?.spellcasting\n ? [this.actor.system.attributes.spellcasting]\n : Object.values(this.actor?.spellcastingClasses ?? {}).map(c => c.spellcasting.ability)\n );\n\n // Weapon & unarmed attacks uses melee or ranged ability depending on type, or both if actor is an NPC\n const melee = CONFIG.DND5E.defaultAbilities.meleeAttack;\n const ranged = CONFIG.DND5E.defaultAbilities.rangedAttack;\n return new Set([this.attack.type.value === \"melee\" ? melee : ranged]);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Critical threshold for attacks with this activity.\n * @type {number}\n */\n get criticalThreshold() {\n let ammoThreshold;\n // TODO: Fetch threshold from ammo\n const threshold = Math.min(\n this.attack.critical.threshold ?? Infinity,\n this.item.system.criticalThreshold ?? Infinity,\n ammoThreshold ?? Infinity\n );\n return threshold < Infinity ? threshold : 20;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Potential attack types when attacking with this activity.\n * @type {Set}\n */\n get validAttackTypes() {\n const sourceType = this._source.attack.type.value;\n if ( sourceType ) return new Set([sourceType]);\n return this.item.system.validAttackTypes ?? new Set();\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /** @override */\n static transformTypeData(source, activityData, options) {\n // For weapons and ammunition, separate the first part from the rest to be used as the base damage and keep the rest\n let damageParts = source.system.damage?.parts ?? [];\n const hasBase = (source.type === \"weapon\")\n || ((source.type === \"consumable\") && (source.system?.type?.value === \"ammo\"));\n if ( hasBase && damageParts.length && !source.system.damage?.base ) {\n const [base, ...rest] = damageParts;\n source.system.damage.parts = [base];\n damageParts = rest;\n }\n\n return foundry.utils.mergeObject(activityData, {\n attack: {\n ability: source.system.ability ?? \"\",\n bonus: source.system.attack?.bonus ?? \"\",\n critical: {\n threshold: source.system.critical?.threshold\n },\n flat: source.system.attack?.flat ?? false,\n type: {\n value: source.system.actionType.startsWith(\"m\") ? \"melee\" : \"ranged\",\n classification: source.system.actionType.endsWith(\"wak\") ? \"weapon\" : \"spell\"\n }\n },\n damage: {\n critical: {\n bonus: source.system.critical?.damage\n },\n includeBase: true,\n parts: damageParts.map(part => this.transformDamagePartData(source, part)) ?? []\n }\n });\n }\n\n /* -------------------------------------------- */\n /* Data Preparation */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareData() {\n super.prepareData();\n this.attack.type.value ||= this.item.system.attackType ?? \"melee\";\n this.attack.type.classification ||= this.item.system.attackClassification ?? \"weapon\";\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareFinalData(rollData) {\n if ( this.damage.includeBase && this.item.system.offersBaseDamage && this.item.system.damage.base.formula ) {\n const basePart = this.item.system.damage.base.clone(this.item.system.damage.base.toObject(false));\n basePart.base = true;\n basePart.locked = true;\n this.damage.parts.unshift(basePart);\n }\n\n rollData ??= this.getRollData({ deterministic: true });\n super.prepareFinalData(rollData);\n this.prepareDamageLabel(rollData);\n\n const { data, parts } = this.getAttackData();\n const roll = new Roll(parts.join(\"+\"), data);\n this.labels.modifier = simplifyRollFormula(roll.formula, { deterministic: true }).replaceAll(\" \", \"\") || \"0\";\n const formula = simplifyRollFormula(roll.formula).trim() || \"0\";\n this.labels.toHit = !/^[+-]/.test(formula) ? `+${formula}` : formula;\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /**\n * The game term label for this attack.\n * @param {string} [attackMode] The mode the attack was made with.\n * @returns {string}\n */\n getActionLabel(attackMode) {\n let attackModeLabel;\n if ( attackMode ) {\n const key = attackMode.split(\"-\").map(s => s.capitalize()).join(\"\");\n attackModeLabel = game.i18n.localize(`DND5E.ATTACK.Mode.${key}`);\n }\n const actionType = this.getActionType(attackMode);\n let actionTypeLabel = game.i18n.localize(`DND5E.Action${actionType.toUpperCase()}`);\n const isLegacy = dnd5e.settings.rulesVersion === \"legacy\";\n const isUnarmed = this.attack.type.classification === \"unarmed\";\n if ( isUnarmed ) attackModeLabel = game.i18n.localize(\"DND5E.ATTACK.Classification.Unarmed\");\n const isSpell = (actionType === \"rsak\") || (actionType === \"msak\");\n if ( isLegacy || isSpell ) return [actionTypeLabel, attackModeLabel].filterJoin(\" • \");\n actionTypeLabel = game.i18n.localize(`DND5E.ATTACK.Attack.${actionType}`);\n if ( isUnarmed ) return [actionTypeLabel, attackModeLabel].filterJoin(\" • \");\n const weaponType = CONFIG.DND5E.weaponTypeMap[this.item.system.type?.value];\n const weaponTypeLabel = weaponType\n ? game.i18n.localize(`DND5E.ATTACK.Weapon.${weaponType.capitalize()}`)\n : CONFIG.DND5E.weaponTypes[this.item.system.type?.value];\n return [actionTypeLabel, weaponTypeLabel, attackModeLabel].filterJoin(\" • \");\n }\n\n /* -------------------------------------------- */\n\n /**\n * Get the roll parts used to create the attack roll.\n * @param {object} [config={}]\n * @param {string} [config.ammunition]\n * @param {string} [config.attackMode]\n * @param {string} [config.situational]\n * @returns {{ data: object, parts: string[] }}\n */\n getAttackData({ ammunition, attackMode, situational }={}) {\n const rollData = this.getRollData();\n if ( this.attack.flat ) return CONFIG.Dice.BasicRoll.constructParts({ toHit: this.attack.bonus }, rollData);\n\n const weapon = this.item.system;\n const ammo = this.actor?.items.get(ammunition)?.system;\n const { parts, data } = CONFIG.Dice.BasicRoll.constructParts({\n mod: this.attack.ability !== \"none\" ? rollData.mod : null,\n prof: weapon.prof?.term,\n bonus: this.attack.bonus,\n weaponMagic: weapon.magicAvailable ? weapon.magicalBonus : null,\n ammoMagic: ammo?.magicAvailable ? ammo.magicalBonus : null,\n actorBonus: this.actor?.system.bonuses?.[this.getActionType(attackMode)]?.attack,\n situational\n }, rollData);\n\n // Add exhaustion reduction\n this.actor?.addRollExhaustion(parts, data);\n\n return { data, parts };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Get the roll parts used to create the damage rolls.\n * @param {Partial} [config={}]\n * @returns {AttackDamageRollProcessConfiguration}\n */\n getDamageConfig(config={}) {\n const rollConfig = super.getDamageConfig(config);\n\n // Handle ammunition\n const ammo = config.ammunition?.system;\n if ( ammo ) {\n const properties = Array.from(ammo.properties).filter(p => CONFIG.DND5E.itemProperties[p]?.isPhysical);\n if ( this.item.system.properties?.has(\"mgc\") && !properties.includes(\"mgc\") ) properties.push(\"mgc\");\n\n // Add any new physical properties from the ammunition to the damage properties\n for ( const roll of rollConfig.rolls ) {\n for ( const property of properties ) {\n if ( !roll.options.properties.includes(property) ) roll.options.properties.push(property);\n }\n }\n\n // Add the ammunition's damage\n if ( ammo.damage.base.formula ) {\n const basePartIndex = rollConfig.rolls.findIndex(i => i.base);\n const damage = ammo.damage.base.clone(ammo.damage.base);\n const rollData = this.getRollData();\n\n // If mode is \"replace\" and base part is present, replace the base part\n if ( ammo.damage.replace & (basePartIndex !== -1) ) {\n damage.base = true;\n rollConfig.rolls.splice(basePartIndex, 1, this._processDamagePart(damage, config, rollData, basePartIndex));\n }\n\n // Otherwise stick the ammo damage after base part (or as first part)\n else {\n damage.ammo = true;\n rollConfig.rolls.splice(\n basePartIndex + 1, 0, this._processDamagePart(damage, rollConfig, rollData, basePartIndex + 1)\n );\n }\n }\n }\n\n if ( this.damage.critical.bonus && rollConfig.rolls[0] && !rollConfig.rolls[0].options?.critical?.bonusDamage ) {\n foundry.utils.setProperty(rollConfig.rolls[0], \"options.critical.bonusDamage\", this.damage.critical.bonus);\n }\n\n return rollConfig;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Create a label based on this activity's settings and, if contained in a weapon, additional details from the weapon.\n * @returns {string}\n */\n getRangeLabel() {\n if ( this.item.type !== \"weapon\" ) return this.labels?.range ?? \"\";\n\n const parts = [];\n\n // Add reach for melee weapons, unless the activity is explicitly specified as a ranged attack\n if ( this.validAttackTypes.has(\"melee\") ) {\n let { reach, units } = this.item.system.range;\n if ( !reach ) reach = convertLength(5, \"ft\", units);\n parts.push(game.i18n.format(\"DND5E.RANGE.Formatted.Reach\", {\n reach: formatLength(reach, units, { strict: false })\n }));\n }\n\n // Add range for ranged or thrown weapons, unless the activity is explicitly specified as melee\n if ( this.validAttackTypes.has(\"ranged\") ) {\n let range;\n if ( this.range.override ) range = `${this.range.value} ${this.range.units ?? \"\"}`;\n else {\n const { value, long, units } = this.item.system.range;\n range = !long || (long === value) ? formatLength(value, units)\n : `${formatNumber(value)}/${formatLength(long, units)}`;\n }\n if ( range ) parts.push(game.i18n.format(\"DND5E.RANGE.Formatted.Range\", { range }));\n }\n\n return game.i18n.getListFormatter({ type: \"disjunction\" }).format(parts.filter(_ => _));\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _processDamagePart(damage, rollConfig, rollData, index=0) {\n if ( !damage.base ) return super._processDamagePart(damage, rollConfig, rollData, index);\n\n // Swap base damage for versatile if two-handed attack is made on versatile weapon\n if ( this.item.system.isVersatile && (rollConfig.attackMode === \"twoHanded\") ) {\n const versatile = this.item.system.damage.versatile.clone(this.item.system.damage.versatile);\n versatile.base = true;\n versatile.denomination ||= damage.steppedDenomination();\n versatile.number ||= damage.number;\n versatile.types = damage.types;\n damage = versatile;\n }\n\n const roll = super._processDamagePart(damage, rollConfig, rollData, index);\n roll.base = true;\n\n if ( this.item.type === \"weapon\" ) {\n // Ensure `@mod` is present in damage unless it is positive and an off-hand attack or damage is a flat value\n const isDeterministic = new Roll(roll.parts[0]).isDeterministic;\n const includeMod = (!rollConfig.attackMode?.endsWith(\"offhand\") || (roll.data.mod < 0)) && !isDeterministic\n && !((this.attack.type.classification === \"spell\") && (this.item.system.type.value === \"natural\"));\n if ( includeMod && !roll.parts.some(p => p.includes(\"@mod\")) ) roll.parts.push(\"@mod\");\n\n // Add magical bonus\n const magicalBonus = simplifyBonus(this.item.system.magicalBonus, rollData);\n if ( magicalBonus && this.item.system.magicAvailable ) {\n roll.parts.push(\"@magicalBonus\");\n roll.data.magicalBonus = magicalBonus;\n }\n\n // Add ammunition bonus\n const ammo = rollConfig.ammunition?.system;\n const ammoMagicalBonus = simplifyBonus(ammo?.magicalBonus, rollData);\n if ( ammo?.magicAvailable && ammoMagicalBonus ) {\n roll.parts.push(\"@ammoBonus\");\n roll.data.ammoBonus = ammoMagicalBonus;\n }\n }\n\n const criticalBonusDice = this.actor?.getFlag(\"dnd5e\", \"meleeCriticalDamageDice\") ?? 0;\n if ( (this.getActionType(rollConfig.attackMode) === \"mwak\") && (parseInt(criticalBonusDice) !== 0) ) {\n foundry.utils.setProperty(roll, \"options.critical.bonusDice\", criticalBonusDice);\n }\n\n return roll;\n }\n}\n","import AttackSheet from \"../../applications/activity/attack-sheet.mjs\";\nimport AttackRollConfigurationDialog from \"../../applications/dice/attack-configuration-dialog.mjs\";\nimport BaseAttackActivityData from \"../../data/activity/attack-data.mjs\";\nimport { getTargetDescriptors } from \"../../utils.mjs\";\nimport ActivityMixin from \"./mixin.mjs\";\n\n/**\n * @import {\n * AttackRollDialogConfiguration, AttackRollProcessConfiguration, BasicRollMessageConfiguration, D20RollConfiguration\n * } from \"../../dice/_types.mjs\";\n * @import { AmmunitionUpdate } from \"./_types.mjs\";\n */\n\n/**\n * Activity for making attacks and rolling damage.\n */\nexport default class AttackActivity extends ActivityMixin(BaseAttackActivityData) {\n /* -------------------------------------------- */\n /* Model Configuration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static LOCALIZATION_PREFIXES = [...super.LOCALIZATION_PREFIXES, \"DND5E.ATTACK\"];\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static metadata = Object.freeze(\n foundry.utils.mergeObject(super.metadata, {\n type: \"attack\",\n img: \"systems/dnd5e/icons/svg/activity/attack.svg\",\n title: \"DND5E.ATTACK.Title.one\",\n hint: \"DND5E.ATTACK.Hint\",\n sheetClass: AttackSheet,\n usage: {\n actions: {\n rollAttack: AttackActivity.#rollAttack,\n rollDamage: AttackActivity.#rollDamage\n }\n }\n }, { inplace: false })\n );\n\n /* -------------------------------------------- */\n /* Activation */\n /* -------------------------------------------- */\n\n /** @override */\n _usageChatButtons(message) {\n const buttons = [{\n label: game.i18n.localize(\"DND5E.Attack\"),\n icon: ' ',\n dataset: {\n action: \"rollAttack\"\n }\n }];\n if ( this.damage.parts.length || this.item.system.properties?.has(\"amm\") ) buttons.push({\n label: game.i18n.localize(\"DND5E.Damage\"),\n icon: ' ',\n dataset: {\n action: \"rollDamage\"\n }\n });\n return buttons.concat(super._usageChatButtons(message));\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n async _triggerSubsequentActions(config, results) {\n this.rollAttack({ event: config.event }, {}, { data: { \"flags.dnd5e.originatingMessage\": results.message?.id } });\n }\n\n /* -------------------------------------------- */\n /* Rolling */\n /* -------------------------------------------- */\n\n /**\n * Perform an attack roll.\n * @param {AttackRollProcessConfiguration} config Configuration information for the roll.\n * @param {AttackRollDialogConfiguration} dialog Configuration for the roll dialog.\n * @param {BasicRollMessageConfiguration} message Configuration for the roll message.\n * @returns {Promise}\n */\n async rollAttack(config={}, dialog={}, message={}) {\n const targets = getTargetDescriptors();\n\n if ( (this.item.type === \"weapon\") && (this.item.system.quantity === 0) ) {\n ui.notifications.warn(\"DND5E.ATTACK.Warning.NoQuantity\", { localize: true });\n }\n\n const buildConfig = this._buildAttackConfig.bind(this);\n\n const rollConfig = foundry.utils.mergeObject({\n ammunition: this.item.getFlag(\"dnd5e\", `last.${this.id}.ammunition`),\n attackMode: this.item.getFlag(\"dnd5e\", `last.${this.id}.attackMode`),\n elvenAccuracy: this.actor?.getFlag(\"dnd5e\", \"elvenAccuracy\")\n && CONFIG.DND5E.characterFlags.elvenAccuracy.abilities.includes(this.ability),\n halflingLucky: this.actor?.getFlag(\"dnd5e\", \"halflingLucky\"),\n mastery: this.item.getFlag(\"dnd5e\", `last.${this.id}.mastery`),\n target: targets.length === 1 ? targets[0].ac : undefined\n }, config);\n\n const ammunitionOptions = this.item.system.ammunitionOptions ?? [];\n if ( ammunitionOptions.length ) ammunitionOptions.unshift({ value: \"\", label: \"\" });\n if ( rollConfig.ammunition === undefined ) rollConfig.ammunition = ammunitionOptions?.[1]?.value;\n else if ( !ammunitionOptions?.find(m => m.value === rollConfig.ammunition) ) {\n rollConfig.ammunition = ammunitionOptions?.[0]?.value;\n }\n const attackModeOptions = this.item.system.attackModes;\n if ( !attackModeOptions?.find(m => m.value === rollConfig.attackMode) ) {\n rollConfig.attackMode = attackModeOptions?.[0]?.value;\n }\n const masteryOptions = this.item.system.masteryOptions;\n if ( !masteryOptions?.find(m => m.value === rollConfig.mastery) ) {\n rollConfig.mastery = masteryOptions?.[0]?.value;\n }\n\n rollConfig.hookNames = [...(config.hookNames ?? []), \"attack\", \"d20Test\"];\n rollConfig.rolls = [CONFIG.Dice.D20Roll.mergeConfigs({\n options: {\n ammunition: rollConfig.ammunition,\n attackMode: rollConfig.attackMode,\n criticalSuccess: this.criticalThreshold,\n mastery: rollConfig.mastery\n }\n }, config.rolls?.shift())].concat(config.rolls ?? []);\n rollConfig.subject = this;\n\n const dialogConfig = foundry.utils.mergeObject({\n applicationClass: AttackRollConfigurationDialog,\n options: {\n ammunitionOptions: rollConfig.ammunition !== false ? ammunitionOptions : [],\n attackModeOptions,\n buildConfig,\n masteryOptions: (masteryOptions?.length > 1) && !config.mastery ? masteryOptions : [],\n position: {\n top: config.event ? config.event.clientY - 80 : null,\n left: window.innerWidth - 710\n },\n window: {\n title: game.i18n.localize(\"DND5E.AttackRoll\"),\n subtitle: this.item.name,\n icon: this.item.img\n }\n }\n }, dialog);\n\n const messageConfig = foundry.utils.mergeObject({\n create: true,\n data: {\n flavor: `${this.item.name} - ${game.i18n.localize(\"DND5E.AttackRoll\")}`,\n flags: {\n dnd5e: {\n ...this.messageFlags,\n messageType: \"roll\",\n roll: { type: \"attack\" }\n }\n },\n speaker: ChatMessage.getSpeaker({ actor: this.actor })\n }\n }, message);\n\n const rolls = await CONFIG.Dice.D20Roll.buildConfigure(rollConfig, dialogConfig, messageConfig);\n await CONFIG.Dice.D20Roll.buildEvaluate(rolls, rollConfig, messageConfig);\n if ( !rolls.length ) return null;\n for ( const key of [\"ammunition\", \"attackMode\", \"mastery\"] ) {\n if ( !rolls[0].options[key] ) continue;\n foundry.utils.setProperty(messageConfig.data, `flags.dnd5e.roll.${key}`, rolls[0].options[key]);\n }\n await CONFIG.Dice.D20Roll.buildPost(rolls, rollConfig, messageConfig);\n\n const flags = {};\n let ammoUpdate = null;\n\n const canUpdate = this.item.isOwner && !this.item.inCompendium;\n if ( rolls[0].options.ammunition ) {\n const ammo = this.actor?.items.get(rolls[0].options.ammunition);\n if ( ammo ) {\n if ( !ammo.system.properties?.has(\"ret\") ) {\n ammoUpdate = { id: ammo.id, quantity: Math.max(0, ammo.system.quantity - 1) };\n ammoUpdate.destroy = ammo.system.uses.autoDestroy && (ammoUpdate.quantity === 0);\n }\n flags.ammunition = rolls[0].options.ammunition;\n }\n } else if ( rolls[0].options.attackMode?.startsWith(\"thrown\") && !this.item.system.properties?.has(\"ret\") ) {\n ammoUpdate = { id: this.item.id, quantity: Math.max(0, this.item.system.quantity - 1) };\n } else if ( !rolls[0].options.ammunition && dialogConfig.options?.ammunitionOptions?.length ) {\n flags.ammunition = \"\";\n }\n if ( rolls[0].options.attackMode ) flags.attackMode = rolls[0].options.attackMode;\n else if ( rollConfig.attackMode ) rolls[0].options.attackMode = rollConfig.attackMode;\n if ( rolls[0].options.mastery ) flags.mastery = rolls[0].options.mastery;\n if ( canUpdate && !foundry.utils.isEmpty(flags) && (this.actor && this.actor.items.has(this.item.id)) ) {\n await this.item.setFlag(\"dnd5e\", `last.${this.id}`, flags);\n }\n\n /**\n * A hook event that fires after an attack has been rolled but before any ammunition is consumed.\n * @function dnd5e.rollAttack\n * @memberof hookEvents\n * @param {D20Roll[]} rolls The resulting rolls.\n * @param {object} data\n * @param {AttackActivity|null} data.subject The Activity that performed the attack.\n * @param {AmmunitionUpdate|null} data.ammoUpdate Any updates related to ammo consumption for this attack.\n */\n Hooks.callAll(\"dnd5e.rollAttack\", rolls, { subject: this, ammoUpdate });\n Hooks.callAll(\"dnd5e.rollAttackV2\", rolls, { subject: this, ammoUpdate });\n\n // Commit ammunition consumption on attack rolls resource consumption if the attack roll was made\n if ( canUpdate && ammoUpdate?.destroy ) {\n // If ammunition was deleted, store a copy of it in the roll message\n const data = this.actor.items.get(ammoUpdate.id).toObject();\n const messageId = messageConfig.data?.flags?.dnd5e?.originatingMessage\n ?? rollConfig.event?.target.closest(\"[data-message-id]\")?.dataset.messageId;\n const attackMessage = dnd5e.registry.messages.get(messageId, \"attack\")?.pop();\n await attackMessage?.setFlag(\"dnd5e\", \"roll.ammunitionData\", data);\n await this.actor.deleteEmbeddedDocuments(\"Item\", [ammoUpdate.id]);\n }\n else if ( canUpdate && ammoUpdate ) await this.actor?.updateEmbeddedDocuments(\"Item\", [\n { _id: ammoUpdate.id, \"system.quantity\": ammoUpdate.quantity }\n ]);\n\n /**\n * A hook event that fires after an attack has been rolled and ammunition has been consumed.\n * @function dnd5e.postRollAttack\n * @memberof hookEvents\n * @param {D20Roll[]} rolls The resulting rolls.\n * @param {object} data\n * @param {AttackActivity|null} data.subject The activity that performed the attack.\n */\n Hooks.callAll(\"dnd5e.postRollAttack\", rolls, { subject: this });\n\n return rolls;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Configure a roll config for each roll performed as part of the attack process. Will be called once per roll\n * in the process each time an option is changed in the roll configuration interface.\n * @param {AttackRollProcessConfiguration} process Configuration for the entire rolling process.\n * @param {D20RollConfiguration} config Configuration for a specific roll.\n * @param {FormDataExtended} [formData] Any data entered into the rolling prompt.\n * @param {number} index Index of the roll within all rolls being prepared.\n */\n _buildAttackConfig(process, config, formData, index) {\n const ammunition = formData?.get(\"ammunition\") ?? process.ammunition;\n const attackMode = formData?.get(\"attackMode\") ?? process.attackMode;\n const mastery = formData?.get(\"mastery\") ?? process.mastery;\n\n let { parts, data } = this.getAttackData({ ammunition, attackMode });\n const options = config.options ?? {};\n if ( ammunition !== undefined ) options.ammunition = ammunition;\n if ( attackMode !== undefined ) options.attackMode = attackMode;\n if ( mastery !== undefined ) options.mastery = mastery;\n\n config.parts = [...(config.parts ?? []), ...parts];\n config.data = { ...data, ...(config.data ?? {}) };\n config.options = options;\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /**\n * Handle performing an attack roll.\n * @this {AttackActivity}\n * @param {PointerEvent} event Triggering click event.\n * @param {HTMLElement} target The capturing HTML element which defined a [data-action].\n * @param {ChatMessage5e} message Message associated with the activation.\n */\n static #rollAttack(event, target, message) {\n this.rollAttack({ event });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle performing a damage roll.\n * @this {AttackActivity}\n * @param {PointerEvent} event Triggering click event.\n * @param {HTMLElement} target The capturing HTML element which defined a [data-action].\n * @param {ChatMessage5e} message Message associated with the activation.\n */\n static #rollDamage(event, target, message) {\n const lastAttack = message.getAssociatedRolls(\"attack\").pop();\n const attackMode = lastAttack?.getFlag(\"dnd5e\", \"roll.attackMode\");\n\n // Fetch the ammunition used with the last attack roll\n let ammunition;\n const actor = lastAttack?.getAssociatedActor();\n if ( actor ) {\n const storedData = lastAttack.getFlag(\"dnd5e\", \"roll.ammunitionData\");\n ammunition = storedData\n ? new Item.implementation(storedData, { parent: actor })\n : actor.items.get(lastAttack.getFlag(\"dnd5e\", \"roll.ammunition\"));\n }\n\n const isCritical = lastAttack?.rolls[0]?.isCritical;\n const dialogConfig = {};\n if ( isCritical ) dialogConfig.options = { defaultButton: \"critical\" };\n\n this.rollDamage({ event, ammunition, attackMode, isCritical }, dialogConfig);\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async getFavoriteData() {\n return foundry.utils.mergeObject(await super.getFavoriteData(), { modifier: this.labels.modifier });\n }\n}\n","import * as Trait from \"../../documents/actor/trait.mjs\";\nimport ActivitySheet from \"./activity-sheet.mjs\";\n\n/**\n * Sheet for the check activity.\n */\nexport default class CheckSheet extends ActivitySheet {\n\n /** @inheritDoc */\n static DEFAULT_OPTIONS = {\n classes: [\"check-activity\"]\n };\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static PARTS = {\n ...super.PARTS,\n effect: {\n template: \"systems/dnd5e/templates/activity/check-effect.hbs\",\n templates: [\n ...super.PARTS.effect.templates,\n \"systems/dnd5e/templates/activity/parts/check-details.hbs\"\n ]\n }\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareEffectContext(context, options) {\n context = await super._prepareEffectContext(context, options);\n\n const group = game.i18n.localize(\"DND5E.Abilities\");\n context.abilityOptions = [\n { value: \"\", label: \"\" },\n { rule: true },\n { value: \"spellcasting\", label: game.i18n.localize(\"DND5E.SpellAbility\") },\n ...Object.entries(CONFIG.DND5E.abilities).map(([value, config]) => ({ value, label: config.label, group }))\n ];\n let ability;\n const associated = this.activity.check.associated;\n if ( (this.item.type === \"tool\") && !associated.size ) {\n ability = CONFIG.DND5E.abilities[this.item.system.ability]?.label?.toLowerCase();\n } else if ( (associated.size === 1) && (associated.first() in CONFIG.DND5E.skills) ) {\n ability = CONFIG.DND5E.abilities[CONFIG.DND5E.skills[associated.first()].ability]?.label?.toLowerCase();\n }\n if ( ability ) context.abilityOptions[0].label = game.i18n.format(\"DND5E.DefaultSpecific\", { default: ability });\n\n context.associatedOptions = [\n ...Object.entries(CONFIG.DND5E.skills).map(([value, { label }]) => ({\n value, label, group: game.i18n.localize(\"DND5E.Skills\")\n })),\n ...Object.keys(CONFIG.DND5E.tools).map(value => ({\n value, label: Trait.keyLabel(value, { trait: \"tool\" }), group: game.i18n.localize(\"TYPES.Item.toolPl\")\n })).sort((lhs, rhs) => lhs.label.localeCompare(rhs.label, game.i18n.lang))\n ];\n\n context.calculationOptions = [\n { value: \"\", label: game.i18n.localize(\"DND5E.SAVE.FIELDS.save.dc.CustomFormula\") },\n { rule: true },\n { value: \"spellcasting\", label: game.i18n.localize(\"DND5E.SpellAbility\") },\n ...Object.entries(CONFIG.DND5E.abilities).map(([value, config]) => ({ value, label: config.label, group }))\n ];\n\n return context;\n }\n}\n","import { simplifyBonus } from \"../../utils.mjs\";\nimport FormulaField from \"../fields/formula-field.mjs\";\nimport BaseActivityData from \"./base-activity.mjs\";\n\nconst { SchemaField, SetField, StringField } = foundry.data.fields;\n\n/**\n * @import { CheckActivityData } from \"./_types.mjs\";\n */\n\n/**\n * Data model for a check activity.\n * @extends {BaseActivityData}\n * @mixes CheckActivityData\n */\nexport default class BaseCheckActivityData extends BaseActivityData {\n /** @inheritDoc */\n static defineSchema() {\n return {\n ...super.defineSchema(),\n check: new SchemaField({\n ability: new StringField(),\n associated: new SetField(new StringField()),\n dc: new SchemaField({\n calculation: new StringField(),\n formula: new FormulaField({ deterministic: true })\n })\n })\n };\n }\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /** @override */\n get ability() {\n if ( this.check.dc.calculation in CONFIG.DND5E.abilities ) return this.check.dc.calculation;\n if ( this.check.dc.calculation === \"spellcasting\" ) return this.spellcastingAbility;\n return this.check.ability;\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /** @override */\n static transformTypeData(source, activityData, options) {\n return foundry.utils.mergeObject(activityData, {\n check: {\n ability: source.system.ability ?? Object.keys(CONFIG.DND5E.abilities)[0]\n }\n });\n }\n\n /* -------------------------------------------- */\n /* Data Preparation */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareFinalData(rollData) {\n rollData ??= this.getRollData({ deterministic: true });\n super.prepareFinalData(rollData);\n\n if ( this.check.ability === \"spellcasting\" ) this.check.ability = this.spellcastingAbility;\n\n let ability;\n if ( this.check.dc.calculation ) ability = this.ability;\n else this.check.dc.value = simplifyBonus(this.check.dc.formula, rollData);\n if ( ability ) this.check.dc.value = this.actor?.system.abilities?.[ability]?.dc\n ?? 8 + (this.actor?.system.attributes?.prof ?? 0);\n\n if ( !this.check.dc.value ) this.check.dc.value = null;\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /**\n * Get the ability to use with an associated value.\n * @param {string} associated Skill or tool ID.\n * @returns {string|null} Ability to use.\n */\n getAbility(associated) {\n if ( this.check.ability ) return this.check.ability;\n if ( associated in CONFIG.DND5E.skills ) return CONFIG.DND5E.skills[associated]?.ability ?? null;\n else if ( associated in CONFIG.DND5E.tools ) {\n if ( (this.item.type === \"tool\") && this.item.system.ability ) return this.item.system.ability;\n return CONFIG.DND5E.tools[associated]?.ability ?? null;\n }\n return null;\n }\n}\n","import CheckSheet from \"../../applications/activity/check-sheet.mjs\";\nimport BaseCheckActivityData from \"../../data/activity/check-data.mjs\";\nimport * as Trait from \"../../documents/actor/trait.mjs\";\nimport { getSceneTargets } from \"../../utils.mjs\";\nimport ActivityMixin from \"./mixin.mjs\";\n\n/**\n * Activity for making ability checks.\n */\nexport default class CheckActivity extends ActivityMixin(BaseCheckActivityData) {\n /* -------------------------------------------- */\n /* Model Configuration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static LOCALIZATION_PREFIXES = [...super.LOCALIZATION_PREFIXES, \"DND5E.CHECK\"];\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static metadata = Object.freeze(\n foundry.utils.mergeObject(super.metadata, {\n type: \"check\",\n img: \"systems/dnd5e/icons/svg/activity/check.svg\",\n title: \"DND5E.CHECK.Title\",\n hint: \"DND5E.CHECK.Hint\",\n sheetClass: CheckSheet,\n usage: {\n actions: {\n rollCheck: CheckActivity.#rollCheck\n }\n }\n }, { inplace: false })\n );\n\n /* -------------------------------------------- */\n /* Activation */\n /* -------------------------------------------- */\n\n /** @override */\n _usageChatButtons(message) {\n const buttons = [];\n const dc = this.check.dc.value;\n\n const createButton = (abilityKey, associated) => {\n const ability = CONFIG.DND5E.abilities[abilityKey]?.label;\n const checkType = (associated in CONFIG.DND5E.skills) ? \"skill\"\n : (associated in CONFIG.DND5E.tools) ? \"tool\": \"ability\";\n const dataset = { ability: abilityKey, action: \"rollCheck\", visibility: \"all\" };\n if ( dc ) dataset.dc = dc;\n if ( checkType !== \"ability\" ) dataset[checkType] = associated;\n\n let label = ability;\n let type;\n if ( checkType === \"skill\" ) type = CONFIG.DND5E.skills[associated]?.label;\n else if ( checkType === \"tool\" ) type = Trait.keyLabel(associated, { trait: \"tool\" });\n if ( type ) label = game.i18n.format(\"EDITOR.DND5E.Inline.SpecificCheck\", { ability, type });\n else label = ability;\n\n buttons.push({\n label: dc ? `\n ${game.i18n.format(\"EDITOR.DND5E.Inline.DC\", { dc, check: wrap(label) })} \n ${wrap(label)} \n ` : wrap(label),\n icon: checkType === \"tool\" ? ' '\n : ' ',\n dataset\n });\n };\n const wrap = check => game.i18n.format(\"EDITOR.DND5E.Inline.CheckShort\", { check });\n\n const associated = Array.from(this.check.associated);\n if ( !associated.length && (this.item.type === \"tool\") ) associated.push(this.item.system.type.baseItem);\n if ( associated.length ) associated.forEach(a => {\n const ability = this.getAbility(a);\n if ( ability ) createButton(ability, a);\n });\n else if ( this.check.ability ) createButton(this.check.ability);\n\n return buttons.concat(super._usageChatButtons(message));\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /**\n * Handle performing an ability check.\n * @this {CheckActivity}\n * @param {PointerEvent} event Triggering click event.\n * @param {HTMLElement} target The capturing HTML element which defined a [data-action].\n * @param {ChatMessage5e} message Message associated with the activation.\n */\n static async #rollCheck(event, target, message) {\n const targets = getSceneTargets();\n if ( !targets.length && game.user.character ) targets.push(game.user.character);\n if ( !targets.length ) ui.notifications.warn(\"DND5E.ActionWarningNoToken\", { localize: true });\n let { ability, dc, skill, tool } = target.dataset;\n dc = parseInt(dc);\n const rollData = { event, target: Number.isFinite(dc) ? dc : this.check.dc.value };\n if ( ability in CONFIG.DND5E.abilities ) rollData.ability = ability;\n\n for ( const token of targets ) {\n const actor = token instanceof Actor ? token : token.actor;\n const speaker = ChatMessage.getSpeaker({ actor, scene: canvas.scene, token: token.document });\n const messageData = { data: { speaker } };\n if ( skill ) await actor.rollSkill({ ...rollData, skill }, {}, messageData);\n else if ( tool ) {\n rollData.tool = tool;\n if ( (this.item.type === \"tool\")\n && (!this.item.system.type.baseItem || (tool === this.item.system.type.baseItem)) ) {\n rollData.bonus = this.item.system.bonus;\n rollData.prof = this.item.system.prof;\n rollData.item = this.item;\n }\n await actor.rollToolCheck(rollData, {}, messageData);\n }\n else await actor.rollAbilityCheck(rollData, {}, messageData);\n }\n }\n}\n","import ActivitySheet from \"./activity-sheet.mjs\";\n\n/**\n * Sheet for the damage activity.\n */\nexport default class DamageSheet extends ActivitySheet {\n\n /** @inheritDoc */\n static DEFAULT_OPTIONS = {\n classes: [\"damage-activity\"]\n };\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static PARTS = {\n ...super.PARTS,\n effect: {\n template: \"systems/dnd5e/templates/activity/damage-effect.hbs\",\n templates: [\n ...super.PARTS.effect.templates,\n \"systems/dnd5e/templates/activity/parts/damage-damage.hbs\",\n \"systems/dnd5e/templates/activity/parts/damage-part.hbs\",\n \"systems/dnd5e/templates/activity/parts/damage-parts.hbs\"\n ]\n }\n };\n}\n","import FormulaField from \"../fields/formula-field.mjs\";\nimport DamageField from \"../shared/damage-field.mjs\";\nimport BaseActivityData from \"./base-activity.mjs\";\n\nconst { ArrayField, BooleanField, SchemaField } = foundry.data.fields;\n\n/**\n * @import { DamageActivityData } from \"./_types.mjs\";\n */\n\n/**\n * Data model for an damage activity.\n * @extends {BaseActivityData}\n * @mixes DamageActivityData\n */\nexport default class BaseDamageActivityData extends BaseActivityData {\n /** @inheritDoc */\n static defineSchema() {\n return {\n ...super.defineSchema(),\n damage: new SchemaField({\n critical: new SchemaField({\n allow: new BooleanField(),\n bonus: new FormulaField()\n }),\n parts: new ArrayField(new DamageField())\n })\n };\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /** @override */\n static transformTypeData(source, activityData, options) {\n return foundry.utils.mergeObject(activityData, {\n damage: {\n critical: {\n allow: false,\n bonus: source.system.critical?.damage ?? \"\"\n },\n parts: options.versatile\n ? [this.transformDamagePartData(source, [source.system.damage?.versatile, \"\"])]\n : (source.system.damage?.parts?.map(part => this.transformDamagePartData(source, part)) ?? [])\n }\n });\n }\n\n /* -------------------------------------------- */\n /* Data Preparation */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareFinalData(rollData) {\n rollData ??= this.getRollData({ deterministic: true });\n super.prepareFinalData(rollData);\n this.prepareDamageLabel(rollData);\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n getDamageConfig(config={}) {\n const rollConfig = super.getDamageConfig(config);\n\n rollConfig.critical ??= {};\n rollConfig.critical.allow ??= this.damage.critical.allow;\n rollConfig.critical.bonusDamage ??= this.damage.critical.bonus;\n\n return rollConfig;\n }\n}\n","import DamageSheet from \"../../applications/activity/damage-sheet.mjs\";\nimport BaseDamageActivityData from \"../../data/activity/damage-data.mjs\";\nimport ActivityMixin from \"./mixin.mjs\";\n\n/**\n * Activity for rolling damage.\n */\nexport default class DamageActivity extends ActivityMixin(BaseDamageActivityData) {\n /* -------------------------------------------- */\n /* Model Configuration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static LOCALIZATION_PREFIXES = [...super.LOCALIZATION_PREFIXES, \"DND5E.DAMAGE\"];\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static metadata = Object.freeze(\n foundry.utils.mergeObject(super.metadata, {\n type: \"damage\",\n img: \"systems/dnd5e/icons/svg/activity/damage.svg\",\n title: \"DND5E.DAMAGE.Title\",\n hint: \"DND5E.DAMAGE.Hint\",\n sheetClass: DamageSheet,\n usage: {\n actions: {\n rollDamage: DamageActivity.#rollDamage\n }\n }\n }, { inplace: false })\n );\n\n /* -------------------------------------------- */\n /* Activation */\n /* -------------------------------------------- */\n\n /** @override */\n _usageChatButtons(message) {\n if ( !this.damage.parts.length ) return super._usageChatButtons(message);\n return [{\n label: game.i18n.localize(\"DND5E.Damage\"),\n icon: ' ',\n dataset: {\n action: \"rollDamage\"\n }\n }].concat(super._usageChatButtons(message));\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n async _triggerSubsequentActions(config, results) {\n this.rollDamage({ event: config.event }, {}, { data: { \"flags.dnd5e.originatingMessage\": results.message?.id } });\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /**\n * Handle performing a damage roll.\n * @this {DamageActivity}\n * @param {PointerEvent} event Triggering click event.\n * @param {HTMLElement} target The capturing HTML element which defined a [data-action].\n * @param {ChatMessage5e} message Message associated with the activation.\n */\n static #rollDamage(event, target, message) {\n this.rollDamage({ event });\n }\n}\n","import ActivitySheet from \"./activity-sheet.mjs\";\n\n/**\n * Sheet for the enchant activity.\n */\nexport default class EnchantSheet extends ActivitySheet {\n\n /** @inheritDoc */\n static DEFAULT_OPTIONS = {\n classes: [\"enchant-activity\"]\n };\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static PARTS = {\n ...super.PARTS,\n effect: {\n template: \"systems/dnd5e/templates/activity/enchant-effect.hbs\",\n templates: [\n \"systems/dnd5e/templates/activity/parts/enchant-enchantments.hbs\",\n \"systems/dnd5e/templates/activity/parts/enchant-restrictions.hbs\"\n ]\n }\n };\n\n /* -------------------------------------------- */\n\n /** @override */\n tabGroups = {\n sheet: \"identity\",\n activation: \"time\",\n effect: \"enchantments\"\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @override */\n _prepareAppliedEffectContext(context, effect) {\n effect.activityOptions = this.item.system.activities\n .filter(a => a.id !== this.activity.id)\n .map(a => ({ value: a.id, label: a.name, selected: effect.data.riders.activity.has(a.id) }));\n effect.effectOptions = context.allEffects.map(e => ({\n ...e, selected: effect.data.riders.effect.has(e.value)\n }));\n return effect;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareEffectContext(context, options) {\n context = await super._prepareEffectContext(context, options);\n\n const appliedEnchantments = new Set(context.activity.effects?.map(e => e._id) ?? []);\n context.allEnchantments = this.item.effects\n .filter(e => e.type === \"enchantment\")\n .map(effect => ({\n value: effect.id, label: effect.name, selected: appliedEnchantments.has(effect.id)\n }));\n\n const enchantableTypes = this.activity.enchantableTypes;\n context.typeOptions = [\n { value: \"\", label: game.i18n.localize(\"DND5E.ENCHANT.FIELDS.restrictions.type.Any\"), rule: true },\n ...Object.keys(CONFIG.Item.dataModels)\n .filter(t => enchantableTypes.has(t))\n .map(value => ({ value, label: game.i18n.localize(CONFIG.Item.typeLabels[value]) }))\n ];\n context.isTypePhysical = !context.source.restrictions.type\n || !!CONFIG.Item.dataModels[context.source.restrictions.type]?.schema.has(\"quantity\");\n\n const type = context.source.restrictions.type;\n const typeDataModel = CONFIG.Item.dataModels[type];\n if ( typeDataModel ) context.categoryOptions = Object.entries(typeDataModel.itemCategories ?? {})\n .map(([value, config]) => ({ value, label: foundry.utils.getType(config) === \"string\" ? config : config.label }));\n\n context.propertyOptions = (CONFIG.DND5E.validProperties[type] ?? [])\n .map(value => ({ value, label: CONFIG.DND5E.itemProperties[value]?.label ?? value }));\n\n return context;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareIdentityContext(context, options) {\n context = await super._prepareIdentityContext(context, options);\n context.behaviorFields.unshift({\n field: context.fields.enchant.fields.self,\n value: context.source.enchant.self,\n input: context.inputs.createCheckboxInput\n });\n return context;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _getTabs() {\n const tabs = super._getTabs();\n tabs.effect.label = \"DND5E.ENCHANT.SECTIONS.Enchanting\";\n tabs.effect.icon = \"fa-solid fa-wand-sparkles\";\n tabs.effect.tabs = this._markTabs({\n enchantments: {\n id: \"enchantments\", group: \"effect\", icon: \"fa-solid fa-star\",\n label: \"DND5E.ENCHANT.SECTIONS.Enchantments\"\n },\n restrictions: {\n id: \"restrictions\", group: \"effect\", icon: \"fa-solid fa-ban\",\n label: \"DND5E.ENCHANT.SECTIONS.Restrictions\"\n }\n });\n return tabs;\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /** @override */\n _addEffectData() {\n return {\n type: \"enchantment\",\n name: this.item.name,\n img: this.item.img,\n disabled: true\n };\n }\n}\n","import ActivityUsageDialog from \"./activity-usage-dialog.mjs\";\n\nconst { StringField } = foundry.data.fields;\n\n/**\n * Dialog for configuring the usage of an activity.\n */\nexport default class EnchantUsageDialog extends ActivityUsageDialog {\n\n /** @inheritDoc */\n static PARTS = {\n ...super.PARTS,\n creation: {\n template: \"systems/dnd5e/templates/activity/enchant-usage-creation.hbs\"\n }\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareCreationContext(context, options) {\n context = await super._prepareCreationContext(context, options);\n\n const enchantments = this.activity.availableEnchantments;\n if ( (enchantments.length > 1) && this._shouldDisplay(\"create.enchantment\") ) {\n const existingProfile = this.activity.existingEnchantment?.flags.dnd5e?.enchantmentProfile;\n context.hasCreation = true;\n context.enchantment = {\n field: new StringField({ required: true, blank: false, label: game.i18n.localize(\"DND5E.ENCHANTMENT.Label\") }),\n name: \"enchantmentProfile\",\n value: this.config.enchantmentProfile,\n options: enchantments.map(e => ({\n value: e._id,\n label: e._id === existingProfile\n ? game.i18n.format(\"DND5E.ENCHANT.Enchantment.Active\", { name: e.effect.name })\n : e.effect.name\n }))\n };\n } else if ( enchantments.length ) {\n context.enchantment = enchantments[0]?._id ?? false;\n }\n\n return context;\n }\n}\n","import BaseActivityData from \"./base-activity.mjs\";\nimport AppliedEffectField from \"./fields/applied-effect-field.mjs\";\n\nconst {\n ArrayField, BooleanField, DocumentIdField, DocumentUUIDField, SchemaField, SetField, StringField\n} = foundry.data.fields;\n\n/**\n * @import { EnchantActivityData, EnchantEffectApplicationData } from \"./_types.mjs\";\n */\n\n/**\n * Data model for a enchant activity.\n * @extends {BaseActivityData}\n * @mixes EnchantActivityData\n */\nexport default class BaseEnchantActivityData extends BaseActivityData {\n /** @inheritDoc */\n static defineSchema() {\n return {\n ...super.defineSchema(),\n effects: new ArrayField(new AppliedEffectField({\n riders: new SchemaField({\n activity: new SetField(new DocumentIdField()),\n effect: new SetField(new DocumentIdField()),\n item: new SetField(new DocumentUUIDField({ type: \"Item\" }))\n })\n })),\n enchant: new SchemaField({\n self: new BooleanField()\n }),\n restrictions: new SchemaField({\n allowMagical: new BooleanField(),\n categories: new SetField(new StringField()),\n properties: new SetField(new StringField()),\n type: new StringField()\n })\n };\n }\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /** @override */\n get actionType() {\n return \"ench\";\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n get applicableEffects() {\n return null;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Enchantments that have been applied by this activity.\n * @type {ActiveEffect5e[]}\n */\n get appliedEnchantments() {\n return dnd5e.registry.enchantments.applied(this.uuid);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Enchantments that can be applied based on spell/character/class level.\n * @type {EnchantEffectApplicationData[]}\n */\n get availableEnchantments() {\n const level = this.relevantLevel;\n return this.effects\n .filter(e => e.effect && ((e.level.min ?? -Infinity) <= level) && (level <= (e.level.max ?? Infinity)));\n }\n\n /* -------------------------------------------- */\n\n /**\n * List of item types that are enchantable.\n * @type {Set}\n */\n static get enchantableTypes() {\n return Object.entries(CONFIG.Item.dataModels).reduce((set, [k, v]) => {\n if ( v.metadata?.enchantable ) set.add(k);\n return set;\n }, new Set());\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static migrateData(source) {\n super.migrateData(source);\n if ( source.enchant?.identifier ) {\n foundry.utils.setProperty(source, \"visibility.identifier\", source.enchant.identifier);\n delete source.enchant.identifier;\n }\n return source;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n static transformEffectsData(source, options) {\n const effects = [];\n for ( const effect of source.effects ) {\n if ( (effect.type !== \"enchantment\") && (effect.flags?.dnd5e?.type !== \"enchantment\") ) continue;\n effects.push({ _id: effect._id, ...(effect.flags?.dnd5e?.enchantment ?? {}) });\n delete effect.flags?.dnd5e?.enchantment;\n }\n return effects;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n static transformTypeData(source, activityData) {\n return foundry.utils.mergeObject(activityData, {\n restrictions: source.system.enchantment?.restrictions ?? [],\n visibility: {\n identifier: source.system.enchantment?.classIdentifier ?? \"\"\n }\n });\n }\n}\n","import EnchantSheet from \"../../applications/activity/enchant-sheet.mjs\";\nimport EnchantUsageDialog from \"../../applications/activity/enchant-usage-dialog.mjs\";\nimport BaseEnchantActivityData from \"../../data/activity/enchant-data.mjs\";\nimport Item5e from \"../../documents/item.mjs\";\nimport { getSceneTargets } from \"../../utils.mjs\";\nimport ActivityMixin from \"./mixin.mjs\";\n\n/**\n * Activity for enchanting items.\n */\nexport default class EnchantActivity extends ActivityMixin(BaseEnchantActivityData) {\n /* -------------------------------------------- */\n /* Model Configuration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static LOCALIZATION_PREFIXES = [...super.LOCALIZATION_PREFIXES, \"DND5E.ENCHANT\"];\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static metadata = Object.freeze(\n foundry.utils.mergeObject(super.metadata, {\n type: \"enchant\",\n img: \"systems/dnd5e/icons/svg/activity/enchant.svg\",\n title: \"DND5E.ENCHANT.Title\",\n hint: \"DND5E.ENCHANT.Hint\",\n sheetClass: EnchantSheet,\n usage: {\n dialog: EnchantUsageDialog\n }\n }, { inplace: false })\n );\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /**\n * List of item types that are enchantable.\n * @type {Set}\n */\n get enchantableTypes() {\n return Object.entries(CONFIG.Item.dataModels).reduce((set, [k, v]) => {\n if ( v.metadata?.enchantable ) set.add(k);\n return set;\n }, new Set());\n }\n\n /* -------------------------------------------- */\n\n /**\n * Existing enchantment applied by this activity on this activity's item.\n * @type {ActiveEffect5e}\n */\n get existingEnchantment() {\n return this.enchant.self\n ? this.item.effects.find(e => e.isAppliedEnchantment && (e.origin === this.uuid)) : undefined;\n }\n\n /* -------------------------------------------- */\n /* Activation */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _prepareUsageConfig(config) {\n config = super._prepareUsageConfig(config);\n const existingProfile = this.existingEnchantment?.flags.dnd5e?.enchantmentProfile;\n config.enchantmentProfile ??= this.item.effects.has(existingProfile) ? existingProfile\n : this.availableEnchantments[0]?._id;\n return config;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _requiresConfigurationDialog(config) {\n return super._requiresConfigurationDialog(config) || (this.availableEnchantments.length > 1);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _finalizeMessageConfig(usageConfig, messageConfig, results) {\n super._finalizeMessageConfig(usageConfig, messageConfig, results);\n\n // Store selected enchantment profile in message flag\n if ( usageConfig.enchantmentProfile ) foundry.utils.setProperty(\n messageConfig, \"data.flags.dnd5e.use.enchantmentProfile\", usageConfig.enchantmentProfile\n );\n\n // Don't display message if just auto-disabling existing enchantment\n if ( this.existingEnchantment?.flags.dnd5e?.enchantmentProfile === usageConfig.enchantmentProfile ) {\n messageConfig.create = false;\n }\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n onRenderChatCard(message, element) {\n const enchantmentProfile = message.getFlag(\"dnd5e\", \"use.enchantmentProfile\");\n if ( !enchantmentProfile || !message.isContentVisible ) return;\n\n // Ensure concentration is still being maintained\n const concentrationId = message.system.concentration;\n if ( concentrationId && !message.getAssociatedActor()?.effects.get(concentrationId) ) return;\n\n // Create the enchantment tray\n const enchantmentApplication = document.createElement(\"enchantment-application\");\n enchantmentApplication.classList.add(\"dnd5e2\");\n const afterElement = element.querySelector(\".card-footer\");\n if ( afterElement ) afterElement.insertAdjacentElement(\"beforebegin\", enchantmentApplication);\n else element.querySelector(\".chat-card\")?.append(enchantmentApplication);\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n async _triggerSubsequentActions(config, results) {\n if ( !this.enchant.self ) return;\n\n // If enchantment from this activity already exists, remove it\n const existingEnchantment = this.existingEnchantment;\n if ( existingEnchantment ) await existingEnchantment?.delete({ chatMessageOrigin: results.message?.id });\n\n // If no existing enchantment, or existing enchantment profile doesn't match provided one, create new enchantment\n if ( !existingEnchantment || (existingEnchantment.flags.dnd5e?.enchantmentProfile !== config.enchantmentProfile) ) {\n const concentration = results.effects.find(e => e.statuses.has(CONFIG.specialStatusEffects.CONCENTRATING));\n this.applyEnchantment(config.enchantmentProfile, this.item, {\n chatMessage: results.message, concentration, strict: false\n });\n }\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /**\n * Apply an enchantment to the provided item.\n * @param {string} profile ID of the enchantment profile to apply.\n * @param {Item5e} item Item to which to apply the enchantment.\n * @param {object} [options={}]\n * @param {ChatMessage5e} [options.chatMessage] Chat message used to make the enchantment, if applicable.\n * @param {ActiveEffect5e} [options.concentration] Concentration active effect to associate with this enchantment.\n * @param {boolean} [options.strict] Display UI errors and prevent creation if enchantment isn't allowed.\n * @returns {Promise} Created enchantment effect if the process was successful.\n */\n async applyEnchantment(profile, item, { chatMessage, concentration, strict=true }={}) {\n const effect = this.item.effects.get(profile);\n if ( !effect ) return null;\n\n // Validate against the enchantment's restraints on the origin item\n if ( strict ) {\n const errors = this.canEnchant(item);\n if ( errors?.length ) {\n errors.forEach(err => ui.notifications.error(err.message, { console: false }));\n return null;\n }\n }\n\n // If concentration is required, ensure it is still being maintained & GM is present\n if ( !game.user.isGM && concentration && !concentration.isOwner ) {\n if ( strict ) {\n ui.notifications.error(\"DND5E.EffectApplyWarningConcentration\", { console: false, localize: true });\n return null;\n } else {\n concentration = null;\n }\n }\n\n const flags = { enchantmentProfile: profile };\n if ( concentration ) flags.dependentOn = concentration.uuid;\n const enchantmentData = effect.clone({ origin: this.uuid, \"flags.dnd5e\": flags }).toObject();\n\n /**\n * Hook that fires before an enchantment is applied to an item.\n * @function dnd5e.preApplyEnchantment\n * @memberof hookEvents\n * @param {Item5e} item Item to which the enchantment will be applied.\n * @param {object} enchantmentData Data for the enchantment effect that will be created.\n * @param {object} options\n * @param {Activity} options.activity Enchant activity applied the enchantment.\n * @returns {boolean} Explicitly return `false` to prevent enchantment from being applied.\n */\n if ( Hooks.call(\"dnd5e.preApplyEnchantment\", item, enchantmentData, { activity: this }) === false ) return null;\n\n // For compendium items, create on actor\n if ( item.inCompendium ) {\n const actor = this.actor.isOwner ? this.actor : (getSceneTargets()[0]?.actor ?? game.user.character);\n if ( !actor ) {\n ui.notifications.warn(\"DND5E.ENCHANT.Warning.NoTargetActor\", { localize: true });\n return null;\n }\n enchantmentData._id = foundry.utils.randomID();\n const toCreate = await Item5e.createWithContents([item], {\n transformAll: item => item.clone({ \"flags.dnd5e.dependentOn\": `.ActiveEffect.${enchantmentData._id}` })\n });\n [item] = await Item5e.createDocuments(toCreate, { keepId: true, parent: actor });\n }\n\n const enchantment = await ActiveEffect.create(enchantmentData, {\n parent: item, keepId: true, keepOrigin: true, chatMessageOrigin: chatMessage?.id\n });\n\n /**\n * Hook that fires after an enchantment has been applied to an item.\n * @function dnd5e.applyEnchantment\n * @memberof hookEvents\n * @param {Item5e} item Item to which the enchantment was be applied.\n * @param {ActiveEffect5e} enchantment The enchantment effect that was be created.\n * @param {object} options\n * @param {Activity} options.activity Enchant activity applied the enchantment.\n */\n Hooks.callAll(\"dnd5e.applyEnchantment\", item, enchantment, { activity: this });\n\n return enchantment;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Determine whether the provided item can be enchanted based on this enchantment's restrictions.\n * @param {Item5e} item Item that might be enchanted.\n * @returns {true|EnchantmentError[]}\n */\n canEnchant(item) {\n const errors = [];\n\n if ( !this.restrictions.allowMagical && item.system.properties?.has(\"mgc\")\n && (\"quantity\" in item.system) ) {\n errors.push(new EnchantmentError(game.i18n.localize(\"DND5E.ENCHANT.Warning.NoMagicalItems\")));\n }\n\n if ( this.restrictions.type && (item.type !== this.restrictions.type) ) {\n errors.push(new EnchantmentError(game.i18n.format(\"DND5E.ENCHANT.Warning.WrongType\", {\n incorrectType: game.i18n.localize(CONFIG.Item.typeLabels[item.type]),\n allowedType: game.i18n.localize(CONFIG.Item.typeLabels[this.restrictions.type])\n })));\n }\n\n if ( this.restrictions.categories.size && !this.restrictions.categories.has(item.system.type?.value) ) {\n const getLabel = key => {\n const config = CONFIG.Item.dataModels[this.restrictions.type]?.itemCategories[key];\n if ( !config ) return key;\n if ( foundry.utils.getType(config) === \"string\" ) return config;\n return config.label;\n };\n errors.push(new EnchantmentError(game.i18n.format(\n `DND5E.ENCHANT.Warning.${item.system.type?.value ? \"WrongType\" : \"NoSubtype\"}`,\n {\n allowedType: game.i18n.getListFormatter({ type: \"disjunction\" }).format(\n Array.from(this.restrictions.categories).map(c => getLabel(c).toLowerCase())\n ),\n incorrectType: getLabel(item.system.type?.value)\n }\n )));\n }\n\n if ( this.restrictions.properties.size\n && !this.restrictions.properties.intersection(item.system.properties ?? new Set()).size ) {\n errors.push(new EnchantmentError(game.i18n.format(\"DND5E.Enchantment.Warning.MissingProperty\", {\n validProperties: game.i18n.getListFormatter({ type: \"disjunction\" }).format(\n Array.from(this.restrictions.properties).map(p => CONFIG.DND5E.itemProperties[p]?.label ?? p)\n )\n })));\n }\n\n /**\n * A hook event that fires while validating whether an enchantment can be applied to a specific item.\n * @function dnd5e.canEnchant\n * @memberof hookEvents\n * @param {EnchantActivity} activity The activity performing the enchanting.\n * @param {Item5e} item Item to which the enchantment will be applied.\n * @param {EnchantmentError[]} errors List of errors containing failed restrictions. The item will be enchanted\n * so long as no errors are listed, otherwise the provided errors will be\n * displayed to the user.\n */\n Hooks.callAll(\"dnd5e.canEnchant\", this, item, errors);\n\n return errors.length ? errors : true;\n }\n}\n\n/**\n * Error to throw when an item cannot be enchanted.\n */\nexport class EnchantmentError extends Error {\n constructor(...args) {\n super(...args);\n this.name = \"EnchantmentError\";\n }\n}\n","import ActivitySheet from \"./activity-sheet.mjs\";\n\n/**\n * Sheet for the forward activity.\n */\nexport default class ForwardSheet extends ActivitySheet {\n\n /** @inheritDoc */\n static DEFAULT_OPTIONS = {\n classes: [\"forward-activity\"]\n };\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static PARTS = {\n ...super.PARTS,\n activation: {\n template: \"systems/dnd5e/templates/activity/forward-activation.hbs\",\n templates: [\n \"systems/dnd5e/templates/activity/parts/activity-consumption.hbs\"\n ]\n },\n effect: {\n template: \"systems/dnd5e/templates/activity/forward-effect.hbs\"\n }\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareActivationContext(context, options) {\n context = await super._prepareActivationContext(context, options);\n context.showConsumeSpellSlot = false;\n context.showScaling = true;\n return context;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareEffectContext(context, options) {\n context = await super._prepareEffectContext(context, options);\n context.activityOptions = [\n { value: \"\", label: \"\" },\n ...this.item.system.activities.contents\n .filter(a => (a.type !== \"forward\") && (CONFIG.DND5E.activityTypes[a.type] !== false))\n .map(activity => ({ value: activity.id, label: activity.name }))\n ];\n return context;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareIdentityContext(context, options) {\n context = await super._prepareIdentityContext(context, options);\n context.behaviorFields = [];\n return context;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare the tab information for the sheet.\n * @returns {Record>}\n * @protected\n */\n _getTabs() {\n return this._markTabs({\n identity: {\n id: \"identity\", group: \"sheet\", icon: \"fa-solid fa-tag\",\n label: \"DND5E.ACTIVITY.SECTIONS.Identity\"\n },\n activation: {\n id: \"activation\", group: \"sheet\", icon: \"fa-solid fa-clapperboard\",\n label: \"DND5E.ACTIVITY.SECTIONS.Activation\"\n },\n effect: {\n id: \"effect\", group: \"sheet\", icon: \"fa-solid fa-sun\",\n label: \"DND5E.ACTIVITY.SECTIONS.Effect\"\n }\n });\n }\n}\n","import BaseActivityData from \"./base-activity.mjs\";\n\nconst { DocumentIdField, SchemaField } = foundry.data.fields;\n\n/**\n * @import { ForwardActivityData } from \"./_types.mjs\";\n */\n\n/**\n * Data model for a Forward activity.\n * @extends {BaseActivityData}\n * @mixes ForwardActivityData\n */\nexport default class BaseForwardActivityData extends BaseActivityData {\n /** @inheritDoc */\n static defineSchema() {\n const schema = super.defineSchema();\n delete schema.duration;\n delete schema.effects;\n delete schema.range;\n delete schema.target;\n return {\n ...schema,\n activity: new SchemaField({\n id: new DocumentIdField()\n })\n };\n }\n\n /* -------------------------------------------- */\n /* Data Preparation */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareFinalData(rollData) {\n const activity = this.item.system.activities.get(this.activity.id);\n if ( activity && activity.activation.override ) this.activation = activity.toObject().activation;\n\n super.prepareFinalData(rollData);\n\n Object.defineProperty(this.activation, \"canOverride\", {\n value: true,\n configurable: true,\n enumerable: false\n });\n }\n}\n","import ForwardSheet from \"../../applications/activity/forward-sheet.mjs\";\nimport BaseForwardActivityData from \"../../data/activity/forward-data.mjs\";\nimport ActivityMixin from \"./mixin.mjs\";\n\n/**\n * Activity for triggering another activity with modified consumption.\n */\nexport default class ForwardActivity extends ActivityMixin(BaseForwardActivityData) {\n /* -------------------------------------------- */\n /* Model Configuration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static LOCALIZATION_PREFIXES = [...super.LOCALIZATION_PREFIXES, \"DND5E.FORWARD\"];\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static metadata = Object.freeze(\n foundry.utils.mergeObject(super.metadata, {\n type: \"forward\",\n img: \"systems/dnd5e/icons/svg/activity/forward.svg\",\n title: \"DND5E.FORWARD.Title\",\n hint: \"DND5E.FORWARD.Hint\",\n sheetClass: ForwardSheet\n }, { inplace: false })\n );\n\n /* -------------------------------------------- */\n /* Activation */\n /* -------------------------------------------- */\n\n /** @override */\n async use(usage={}, dialog={}, message={}) {\n const usageConfig = foundry.utils.mergeObject({\n cause: {\n activity: this.relativeUUID\n },\n consume: {\n resources: false,\n spellSlot: false\n }\n }, usage);\n\n const activity = this.item.system.activities.get(this.activity.id);\n if ( !activity ) ui.notifications.error(\"DND5E.FORWARD.Warning.NoActivity\", { localize: true });\n return activity?.use(usageConfig, dialog, message);\n }\n}\n","import ActivitySheet from \"./activity-sheet.mjs\";\n\n/**\n * Sheet for the healing activity.\n */\nexport default class HealSheet extends ActivitySheet {\n\n /** @inheritDoc */\n static DEFAULT_OPTIONS = {\n classes: [\"heal-activity\"]\n };\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static PARTS = {\n ...super.PARTS,\n effect: {\n template: \"systems/dnd5e/templates/activity/heal-effect.hbs\",\n templates: [\n ...super.PARTS.effect.templates,\n \"systems/dnd5e/templates/activity/parts/damage-part.hbs\",\n \"systems/dnd5e/templates/activity/parts/heal-healing.hbs\"\n ]\n }\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareEffectContext(context, options) {\n context = await super._prepareEffectContext(context, options);\n context.typeOptions = Object.entries(CONFIG.DND5E.healingTypes).map(([value, config]) => ({\n value, label: config.label, selected: context.activity.healing.types.has(value)\n }));\n const scaleKey = (this.item.type === \"spell\" && this.item.system.level === 0) ? \"labelCantrip\" : \"label\";\n context.scalingOptions = [\n { value: \"\", label: game.i18n.localize(\"DND5E.DAMAGE.Scaling.None\") },\n ...Object.entries(CONFIG.DND5E.damageScalingModes).map(([value, { [scaleKey]: label }]) => ({ value, label }))\n ];\n return context;\n }\n}\n","import DamageField from \"../shared/damage-field.mjs\";\nimport BaseActivityData from \"./base-activity.mjs\";\n\n/**\n * @import { HealActivityData } from \"./_types.mjs\";\n */\n\n/**\n * Data model for an heal activity.\n * @extends {BaseActivityData}\n * @mixes HealActivityData\n */\nexport default class BaseHealActivityData extends BaseActivityData {\n /** @inheritDoc */\n static defineSchema() {\n return {\n ...super.defineSchema(),\n healing: new DamageField()\n };\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /** @override */\n static transformTypeData(source, activityData, options) {\n return foundry.utils.mergeObject(activityData, {\n healing: this.transformDamagePartData(source, source.system.damage?.parts?.[0] ?? [\"\", \"\"])\n });\n }\n\n /* -------------------------------------------- */\n /* Data Preparation */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareFinalData(rollData) {\n rollData ??= this.getRollData({ deterministic: true });\n super.prepareFinalData(rollData);\n this.prepareDamageLabel(rollData);\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /** @override */\n getDamageConfig(config={}) {\n if ( !this.healing.formula ) return foundry.utils.mergeObject({ rolls: [] }, config);\n\n const rollConfig = foundry.utils.mergeObject({ critical: { allow: false } }, config);\n const rollData = this.getRollData();\n rollConfig.rolls = [this._processDamagePart(this.healing, rollConfig, rollData)].concat(config.rolls ?? []);\n\n return rollConfig;\n }\n}\n","import HealSheet from \"../../applications/activity/heal-sheet.mjs\";\nimport BaseHealActivityData from \"../../data/activity/heal-data.mjs\";\nimport ActivityMixin from \"./mixin.mjs\";\n\n/**\n * Activity for rolling healing.\n */\nexport default class HealActivity extends ActivityMixin(BaseHealActivityData) {\n /* -------------------------------------------- */\n /* Model Configuration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static LOCALIZATION_PREFIXES = [...super.LOCALIZATION_PREFIXES, \"DND5E.HEAL\"];\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static metadata = Object.freeze(\n foundry.utils.mergeObject(super.metadata, {\n type: \"heal\",\n img: \"systems/dnd5e/icons/svg/activity/heal.svg\",\n title: \"DND5E.HEAL.Title\",\n hint: \"DND5E.HEAL.Hint\",\n sheetClass: HealSheet,\n usage: {\n actions: {\n rollHealing: HealActivity.#rollHealing\n }\n }\n }, { inplace: false })\n );\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /** @override */\n get damageFlavor() {\n return game.i18n.localize(\"DND5E.HEAL.HealingRoll\");\n }\n\n /* -------------------------------------------- */\n /* Activation */\n /* -------------------------------------------- */\n\n /** @override */\n _usageChatButtons(message) {\n if ( !this.healing.formula ) return super._usageChatButtons(message);\n return [{\n label: game.i18n.localize(\"DND5E.HEAL.HealingButton\"),\n icon: ' ',\n dataset: {\n action: \"rollHealing\"\n }\n }].concat(super._usageChatButtons(message));\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n async _triggerSubsequentActions(config, results) {\n this.rollDamage({ event: config.event }, {}, { data: { \"flags.dnd5e.originatingMessage\": results.message?.id } });\n }\n\n /* -------------------------------------------- */\n /* Rolling */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async rollDamage(config={}, dialog={}, message={}) {\n const messageConfig = foundry.utils.mergeObject({\n [\"data.flags.dnd5e.roll.type\"]: \"healing\"\n }, message);\n return super.rollDamage(config, dialog, messageConfig);\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /**\n * Handle performing a healing roll.\n * @this {HealActivity}\n * @param {PointerEvent} event Triggering click event.\n * @param {HTMLElement} target The capturing HTML element which defined a [data-action].\n * @param {ChatMessage5e} message Message associated with the activation.\n */\n static #rollHealing(event, target, message) {\n this.rollDamage({ event });\n }\n}\n","import BaseActivityData from \"./base-activity.mjs\";\n\nconst { DocumentIdField, FilePathField, StringField } = foundry.data.fields;\n\n/**\n * @import { OrderActivityData } from \"./_types.mjs\";\n */\n\n/**\n * Data model for an order activity.\n * @extends {BaseActivityData}\n * @mixes OrderActivityData\n */\nexport default class BaseOrderActivityData extends BaseActivityData {\n /** @override */\n static defineSchema() {\n return {\n _id: new DocumentIdField({ initial: () => foundry.utils.randomID() }),\n type: new StringField({\n blank: false, required: true, readOnly: true, initial: () => this.metadata.type\n }),\n name: new StringField({ initial: undefined }),\n img: new FilePathField({ initial: undefined, categories: [\"IMAGE\"], base64: false }),\n order: new StringField({ required: true, blank: false, nullable: false })\n };\n }\n\n /* -------------------------------------------- */\n /* Data Preparation */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareData() {\n super.prepareData();\n this.img = CONFIG.DND5E.facilities.orders[this.order]?.icon || this.metadata?.img;\n }\n}\n","import ActivityUsageDialog from \"./activity-usage-dialog.mjs\";\n\nconst { BooleanField, DocumentUUIDField, NumberField, StringField } = foundry.data.fields;\n\n/**\n * Dialog for configuring the usage of an order activity.\n */\nexport default class OrderUsageDialog extends ActivityUsageDialog {\n /** @override */\n static DEFAULT_OPTIONS = {\n actions: {\n deleteOccupant: OrderUsageDialog.#onDeleteOccupant,\n removeCraft: OrderUsageDialog.#onRemoveCraft\n }\n };\n\n /** @override */\n static PARTS = {\n order: {\n template: \"systems/dnd5e/templates/activity/order-usage.hbs\"\n },\n footer: {\n template: \"templates/generic/form-footer.hbs\"\n }\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /**\n * Prepare render context for the build section.\n * @param {ApplicationRenderContext} context Render context.\n * @param {HandlebarsRenderOptions} options Render options.\n * @protected\n */\n _prepareBuildContext(context, options) {\n context.build = {\n choices: CONFIG.DND5E.facilities.sizes,\n field: new StringField({ nullable: false, blank: false, label: \"DND5E.FACILITY.FIELDS.size.label\" }),\n name: \"building.size\",\n value: this.config.building?.size ?? \"cramped\"\n };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare render context for the costs section.\n * @param {ApplicationRenderContext} context Render context.\n * @param {HandlebarsRenderOptions} options Render options.\n * @param {number} options.days The cost in days.\n * @param {number} options.gold The cost in gold.\n * @protected\n */\n _prepareCostsContext(context, { days, gold }) {\n const { duration } = game.settings.get(\"dnd5e\", \"bastionConfiguration\");\n context.costs = {\n days: {\n field: new NumberField({ nullable: true, integer: true, min: 0, label: \"DND5E.TimeDay\" }),\n name: \"costs.days\",\n value: this.config.costs?.days ?? days ?? duration\n },\n gold: {\n field: new NumberField({ nullable: true, integer: true, min: 0, label: \"DND5E.CurrencyGP\" }),\n name: \"costs.gold\",\n value: this.config.costs?.gold ?? gold ?? 0\n }\n };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare render context for the craft section.\n * @param {ApplicationRenderContext} context Render context.\n * @param {HandlebarsRenderOptions} options Render options.\n * @protected\n */\n async _prepareCraftContext(context, options) {\n const { craft } = this.item.system;\n context.craft = {\n legend: game.i18n.localize(`DND5E.FACILITY.Orders.${this.activity.order}.present`),\n item: {\n field: new DocumentUUIDField(),\n name: \"craft.item\",\n value: this.config.craft?.item ?? \"\"\n }\n };\n\n if ( this.activity.order === \"harvest\" ) {\n context.craft.isHarvesting = true;\n context.craft.item.value = this.config.craft?.item ?? craft.item ?? \"\";\n context.craft.quantity = {\n field: new NumberField({ nullable: false, integer: true, positive: true }),\n name: \"craft.quantity\",\n value: this.config.craft?.quantity ?? craft.quantity ?? 1\n };\n } else {\n context.craft.baseItem = {\n field: new BooleanField({\n label: \"DND5E.FACILITY.Craft.BaseItem.Label\",\n hint: \"DND5E.FACILITY.Craft.BaseItem.Hint\"\n }),\n name: \"craft.buyBaseItem\",\n value: this.config.craft?.buyBaseItem ?? false\n };\n }\n\n if ( context.craft.item.value ) {\n const item = await fromUuid(context.craft.item.value);\n context.craft.value = {\n img: item.img,\n name: item.name,\n contentLink: item.toAnchor().outerHTML\n };\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare render context for the enlarge order.\n * @param {ApplicationRenderContext} context Render context.\n * @param {HandlebarsRenderOptions} options Render options.\n * @returns {{ days: number, gold: number }} The costs associated with performing this order.\n * @protected\n */\n _prepareEnlargeContext(context, options) {\n const sizes = Object.entries(CONFIG.DND5E.facilities.sizes).sort((a, b) => a.value - b.value);\n const index = sizes.findIndex(([size]) => size === this.item.system.size);\n const [, current] = sizes[index];\n const [, target] = sizes[index + 1];\n context.description = `\n ${current.label} \n ➡ \n ${target.label} \n `;\n const days = this.item.system.type.value === \"basic\" ? target.days - current.days : 0;\n return { days, gold: target.value - current.value };\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n async _prepareFooterContext(context, options) {\n context.buttons = [{\n action: \"use\",\n type: \"button\",\n icon: \"fas fa-hand-point-right\",\n label: \"DND5E.FACILITY.Order.Execute\"\n }];\n return context;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare render context for orders.\n * @param {ApplicationRenderContext} context Render context.\n * @param {HandlebarsRenderOptions} options Render options.\n * @protected\n */\n async _prepareOrderContext(context, options) {\n if ( this.activity.order === \"enlarge\" ) {\n const { days, gold } = this._prepareEnlargeContext(context, options);\n this._prepareCostsContext(context, { ...options, days, gold });\n return;\n }\n\n if ( this.activity.order === \"build\" ) {\n const { days, value: gold } = CONFIG.DND5E.facilities.sizes.cramped;\n this._prepareBuildContext(context, options);\n this._prepareCostsContext(context, { ...options, days, gold });\n return;\n }\n\n let { duration } = game.settings.get(\"dnd5e\", \"bastionConfiguration\");\n if ( (this.activity.order === \"craft\") || (this.activity.order === \"harvest\") ) {\n await this._prepareCraftContext(context, options);\n }\n else if ( this.activity.order === \"trade\" ) await this._prepareTradeContext(context, options);\n else {\n const config = CONFIG.DND5E.facilities.orders[this.activity.order];\n if ( config?.duration ) duration = config.duration;\n }\n\n this._prepareCostsContext(context, { ...options, days: duration });\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _preparePartContext(partId, context, options) {\n context = await super._preparePartContext(partId, context, options);\n switch ( partId ) {\n case \"order\": await this._prepareOrderContext(context, options); break;\n }\n return context;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare render context for the trade order.\n * @param {ApplicationRenderContext} context Render context.\n * @param {HandlebarsRenderOptions} options Render options.\n * @protected\n */\n async _prepareTradeContext(context, options) {\n const { trade } = this.item.system;\n if ( !trade.creatures.max && !trade.stock.max ) {\n context.trade = {\n stocked: {\n field: new BooleanField({\n label: \"DND5E.FACILITY.Trade.Stocked.Label\",\n hint: \"DND5E.FACILITY.Trade.Stocked.Hint\"\n }),\n name: \"trade.stock.stocked\",\n value: this.config.trade?.stock?.stocked ?? false\n }\n };\n } else {\n const isSelling = this.config.trade?.sell ?? false;\n context.trade = {\n sell: {\n field: new BooleanField({ label: \"DND5E.FACILITY.Trade.Sell.Label\" }),\n name: \"trade.sell\",\n value: isSelling\n }\n };\n\n if ( trade.stock.max ) {\n const max = isSelling ? trade.stock.value || 0 : trade.stock.max - (trade.stock.value ?? 0);\n const label = `DND5E.FACILITY.Trade.Stock.${isSelling ? \"Sell\" : \"Buy\"}`;\n context.trade.stock = {\n field: new NumberField({ label, max, min: 0, step: 1, nullable: false }),\n name: \"trade.stock.value\",\n value: this.config.trade?.stock?.value ?? 0\n };\n } else if ( trade.creatures.max ) {\n const sell = await Promise.all(trade.creatures.value.map(async (uuid, i) => {\n const doc = await fromUuid(uuid);\n return {\n contentLink: doc.toAnchor().outerHTML,\n field: new BooleanField(),\n name: \"trade.creatures.sell\",\n value: this.config.trade?.creatures?.sell?.[i] ?? false\n };\n }));\n const buy = await Promise.all(Array.fromRange(trade.creatures.max).map(async i => {\n let removable = true;\n let uuid = trade.creatures.value[i];\n if ( uuid ) removable = false;\n else uuid = this.config.trade?.creatures?.buy?.[i];\n const doc = await fromUuid(uuid);\n if ( doc ) return { removable, uuid, img: doc.img, name: doc.name };\n return { empty: true };\n }));\n context.trade.creatures = {\n buy, sell,\n hint: \"DND5E.FACILITY.Trade.Creatures.Buy\",\n price: {\n field: new NumberField({\n nullable: false, min: 0, integer: true,\n label: \"DND5E.FACILITY.Trade.Price.Label\",\n hint: \"DND5E.FACILITY.Trade.Price.Hint\"\n }),\n name: \"trade.creatures.price\",\n value: this.config.trade?.creatures?.price ?? 0\n }\n };\n }\n }\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _attachFrameListeners() {\n super._attachFrameListeners();\n this.element.addEventListener(\"drop\", this._onDrop.bind(this));\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle drops onto the dialog.\n * @param {DragEvent} event The drag-drop event.\n * @protected\n */\n _onDrop(event) {\n const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);\n if ( (data.type !== \"Actor\") || !data.uuid ) return;\n const { trade } = this.item.system;\n if ( !this.config.trade?.creatures?.buy ) {\n this.config.trade ??= {};\n this.config.trade.creatures ??= {};\n this.config.trade.creatures.buy = [];\n }\n const index = Math.max(trade.creatures.value.length, this.config.trade.creatures.buy.length);\n if ( index + 1 > trade.creatures.max ) return;\n this.config.trade.creatures.buy[index] = data.uuid;\n this.render();\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare submission data for build orders.\n * @param {object} submitData Submission data.\n * @protected\n */\n _prepareBuildData(submitData) {\n if ( (this.config.building?.size ?? \"cramped\") !== submitData.building?.size ) {\n const { days, value: gold } = CONFIG.DND5E.facilities.sizes[submitData.building.size];\n Object.assign(submitData.costs, { days, gold });\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare submission data for craft orders.\n * @param {object} submitData Submission data.\n * @returns {Promise}\n * @protected\n */\n async _prepareCraftData(submitData) {\n let recalculateCosts = submitData.craft.item !== this.config.craft?.item;\n recalculateCosts ||= submitData.craft.buyBaseItem !== this.config.craft?.buyBaseItem;\n if ( (this.activity.order === \"craft\") && recalculateCosts ) {\n const item = await fromUuid(submitData.craft.item);\n const { days, gold } = await item.system.getCraftCost({\n baseItem: submitData.craft.buyBaseItem ? \"buy\" : \"craft\"\n });\n Object.assign(submitData.costs, { days, gold });\n }\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareSubmitData(event, formData) {\n const submitData = await super._prepareSubmitData(event, formData);\n if ( \"building\" in submitData ) this._prepareBuildData(submitData);\n if ( submitData.craft?.item ) await this._prepareCraftData(submitData);\n if ( \"trade\" in submitData ) await this._prepareTradeData(submitData);\n return submitData;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare submission data for trade orders.\n * @param {object} submitData Submission data.\n * @returns {Promise}\n * @protected\n */\n async _prepareTradeData(submitData) {\n // Clear data when toggling trade mode.\n if ( (\"trade\" in this.config) && (submitData.trade.sell !== this.config.trade?.sell) ) {\n delete this.config.trade.stock;\n delete this.config.trade.creatures;\n submitData.costs.gold = 0;\n }\n\n if ( (\"stock\" in submitData.trade) && (\"value\" in submitData.trade.stock) && !submitData.trade.sell ) {\n submitData.costs.gold = submitData.trade.stock.value;\n }\n\n if ( \"creatures\" in submitData.trade && !submitData.trade.sell ) {\n const buy = [];\n const { creatures } = submitData.trade;\n Object.keys(creatures.buy ?? {}).forEach(k => buy[k] = creatures.buy[k]);\n submitData.trade.creatures.buy = buy;\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle removing a configured occupant.\n * @this {OrderUsageDialog}\n * @param {PointerEvent} event The triggering event.\n * @param {HTMLElement} target The event target.\n */\n static #onDeleteOccupant(event, target) {\n const { index } = target.closest(\"[data-index]\")?.dataset ?? {};\n this.config.trade.creatures.buy.splice(index, 1);\n this.render();\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle clearing the currently configured item for crafting.\n * @this {OrderUsageDialog}\n */\n static #onRemoveCraft() {\n delete this.config.craft.item;\n this.render();\n }\n}\n","import { filteredKeys } from \"../utils.mjs\";\nimport Award from \"./award.mjs\";\nimport Application5e from \"./api/application.mjs\";\n\n/**\n * @import { CurrencyUpdateOptions } from \"./_types.mjs\";\n */\n\n/**\n * Application for performing currency conversions & transfers.\n */\nexport default class CurrencyManager extends Application5e {\n\n /** @override */\n static DEFAULT_OPTIONS = {\n actions: {\n setAll: CurrencyManager.#setTransferValue,\n setHalf: CurrencyManager.#setTransferValue\n },\n classes: [\"currency-manager\", \"standard-form\"],\n document: null,\n form: {\n closeOnSubmit: true,\n handler: CurrencyManager.#handleFormSubmission\n },\n position: {\n width: 350\n },\n tag: \"form\",\n window: {\n title: \"DND5E.CurrencyManager.Title\"\n }\n };\n\n /* -------------------------------------------- */\n\n /** @override */\n static PARTS = {\n tabs: {\n template: \"templates/generic/tab-navigation.hbs\"\n },\n convert: {\n template: \"systems/dnd5e/templates/apps/currency-manager-convert.hbs\"\n },\n transfer: {\n template: \"systems/dnd5e/templates/apps/currency-manager-transfer.hbs\"\n }\n };\n\n /* -------------------------------------------- */\n\n /** @override */\n tabGroups = {\n primary: \"transfer\"\n };\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /**\n * Document for which the currency is being managed.\n * @type {Actor5e|Item5e}\n */\n get document() {\n return this.options.document;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Destinations to which currency can be transferred.\n * @type {(Actor5e|Item5e)[]}\n */\n get transferDestinations() {\n const destinations = [];\n const actor = this.document instanceof Actor ? this.document : this.document.parent;\n if ( actor && (actor !== this.document) ) destinations.push(actor);\n destinations.push(...(actor?.system.transferDestinations ?? []));\n destinations.push(...(actor?.itemTypes.container.filter(b => b !== this.document) ?? []));\n if ( game.user.isGM ) {\n const primaryParty = game.actors.party;\n if ( primaryParty && (this.document !== primaryParty) && !destinations.includes(primaryParty) ) {\n destinations.push(primaryParty);\n }\n }\n return destinations;\n }\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareContext(options) {\n const context = await super._prepareContext(options);\n\n context.currency = this.document.system.currency;\n context.destinations = Award.prepareDestinations(this.transferDestinations);\n context.tabs = this._getTabs();\n\n return context;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n async _preparePartContext(partId, context) {\n context = await super._preparePartContext(partId, context);\n context.tab = context.tabs[partId];\n return context;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare the tab information for the sheet.\n * @returns {Record>}\n * @protected\n */\n _getTabs() {\n return {\n convert: {\n id: \"convert\", group: \"primary\", icon: \"fa-solid fa-arrow-up-short-wide\",\n label: \"DND5E.CurrencyManager.Convert.Label\",\n active: this.tabGroups.primary === \"convert\",\n cssClass: this.tabGroups.primary === \"convert\" ? \"active\" : \"\"\n },\n transfer: {\n id: \"transfer\", group: \"primary\", icon: \"fa-solid fa-reply-all fa-flip-horizontal\",\n label: \"DND5E.CurrencyManager.Transfer.Label\",\n active: this.tabGroups.primary === \"transfer\",\n cssClass: this.tabGroups.primary === \"transfer\" ? \"active\" : \"\"\n }\n };\n }\n\n /* -------------------------------------------- */\n /* Event Handling */\n /* -------------------------------------------- */\n\n /**\n * Handle setting the transfer amount based on the buttons.\n * @this {CurrencyManager}\n * @param {Event} event Triggering click event.\n * @param {HTMLElement} target Button that was clicked.\n * @protected\n */\n static #setTransferValue(event, target) {\n for ( let [key, value] of Object.entries(this.document.system.currency) ) {\n if ( target.dataset.action === \"setHalf\" ) value = Math.floor(value / 2);\n const input = this.element.querySelector(`[name=\"amount.${key}\"]`);\n if ( input && value ) input.value = value;\n }\n this._validateForm();\n }\n\n /* -------------------------------------------- */\n /* Form Handling */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _onChangeForm(formConfig, event) {\n super._onChangeForm(formConfig, event);\n this._validateForm();\n }\n\n /* -------------------------------------------- */\n\n /**\n * Ensure the transfer form is in a valid form to be submitted.\n * @protected\n */\n _validateForm() {\n const formData = new foundry.applications.ux.FormDataExtended(this.element);\n const data = foundry.utils.expandObject(formData.object);\n let valid = true;\n if ( !filteredKeys(data.amount ?? {}).length ) valid = false;\n if ( !filteredKeys(data.destination ?? {}).length ) valid = false;\n this.element.querySelector('button[name=\"transfer\"]').disabled = !valid;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle submitting the currency manager form.\n * @this {Award}\n * @param {Event|SubmitEvent} event The form submission event.\n * @param {HTMLFormElement} form The submitted form.\n * @param {FormDataExtended} formData Data from the dialog.\n */\n static async #handleFormSubmission(event, form, formData) {\n const data = foundry.utils.expandObject(formData.object);\n switch ( event.submitter?.name ) {\n case \"convert\":\n await this.constructor.convertCurrency(this.document);\n break;\n case \"transfer\":\n const destinations = this.transferDestinations.filter(d => data.destination[d.id]);\n await this.constructor.transferCurrency(this.document, destinations, data.amount);\n break;\n }\n }\n\n /* -------------------------------------------- */\n /* Currency Operations */\n /* -------------------------------------------- */\n\n /**\n * Convert all carried currency to the highest possible denomination using configured conversion rates.\n * See CONFIG.DND5E.currencies for configuration.\n * @param {Actor5e|Item5e} doc Actor or container item to convert.\n * @returns {Promise}\n */\n static convertCurrency(doc) {\n const currency = foundry.utils.deepClone(doc.system.currency);\n\n const currencies = Object.entries(CONFIG.DND5E.currencies)\n .filter(([, c]) => c.conversion)\n .sort((a, b) => a[1].conversion - b[1].conversion);\n\n // Convert all currently to smallest denomination\n const smallestConversion = currencies.at(-1)[1].conversion;\n let amount = currencies.reduce((amount, [denomination, config]) =>\n amount + (currency[denomination] * (smallestConversion / config.conversion))\n , 0);\n\n // Convert base units into the highest denomination possible\n for ( const [denomination, config] of currencies) {\n const ratio = smallestConversion / config.conversion;\n currency[denomination] = Math.floor(amount / ratio);\n amount -= currency[denomination] * ratio;\n }\n\n // Save the updated currency object\n return doc.update({ \"system.currency\": currency });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Deduct a certain amount of currency from a given Actor.\n * @param {Actor5e} actor The actor.\n * @param {number} amount The amount of currency.\n * @param {string} denomination The currency's denomination.\n * @param {CurrencyUpdateOptions} [options]\n * @throws {Error} If the Actor does not have sufficient currency.\n * @returns {Promise|void}\n */\n static deductActorCurrency(actor, amount, denomination, options={}) {\n if ( amount <= 0 ) return;\n // eslint-disable-next-line no-unused-vars\n const { item, remainder, ...updates } = this.getActorCurrencyUpdates(actor, amount, denomination, options);\n if ( remainder ) throw new Error(game.i18n.format(\"DND5E.CurrencyManager.Error.InsufficientFunds\", {\n denomination,\n amount: new Intl.NumberFormat(game.i18n.lang).format(amount),\n name: actor.name\n }));\n return actor.update(updates);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Determine model updates for deducting a certain amount of currency from a given Actor.\n * @param {Actor5e} actor The actor.\n * @param {number} amount The amount of currency.\n * @param {string} denomination The currency's denomination.\n * @param {CurrencyUpdateOptions} [options]\n * @returns {{ item: object[], remainder: number, [p: string]: any }}\n */\n static getActorCurrencyUpdates(\n actor, amount, denomination, { recursive=false, priority=\"low\", exact=true, makeChange=true }={}\n ) {\n const { currency } = actor.system;\n if ( amount <= 0 ) return { system: { currency: { ...currency } }, remainder: amount, item: [] };\n\n const currencies = Object.entries(CONFIG.DND5E.currencies)\n .filter(([denom]) => !exact || (denom !== denomination))\n .map(([denom, { conversion }]) => [denom, conversion])\n .sort(([, a], [, b]) => priority === \"high\" ? a - b : b - a);\n const baseConversion = CONFIG.DND5E.currencies[denomination].conversion;\n if ( exact ) currencies.unshift([denomination, baseConversion]);\n\n let passes = currencies.length;\n let updates;\n while ( passes ) {\n updates = { system: { currency: { ...currency } }, remainder: amount, item: [] };\n for ( const [denom, conversion] of currencies ) {\n const multiplier = conversion / baseConversion;\n const deduct = Math.min(updates.system.currency[denom], Math.floor(updates.remainder * multiplier));\n // Handle normal deduction first.\n updates.remainder -= deduct / multiplier;\n updates.system.currency[denom] -= deduct;\n // If there's still a remainder, break the denomination into change.\n if ( updates.remainder && makeChange && (conversion < baseConversion) && updates.system.currency[denom] ) {\n const rate = Math.floor(baseConversion / conversion);\n const breaks = Math.min(updates.system.currency[denom], Math.ceil(updates.remainder / rate));\n updates.system.currency[denom] -= breaks;\n updates.system.currency[denomination] += breaks * rate;\n const change = Math.min(updates.system.currency[denomination], updates.remainder);\n updates.remainder -= change;\n updates.system.currency[denomination] -= change;\n }\n if ( !updates.remainder ) return updates;\n }\n currencies.push(currencies.shift());\n passes--;\n }\n\n return updates;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Transfer currency between one document and another.\n * @param {Actor5e|Item5e} origin Document from which to move the currency.\n * @param {Document[]} destinations Documents that should receive the currency.\n * @param {object[]} amounts Amount of each denomination to transfer.\n */\n static async transferCurrency(origin, destinations, amounts) {\n Award.awardCurrency(amounts, destinations, { origin });\n }\n}\n","import ActivityMixin from \"./mixin.mjs\";\nimport BaseOrderActivityData from \"../../data/activity/order-data.mjs\";\nimport OrderUsageDialog from \"../../applications/activity/order-usage-dialog.mjs\";\nimport CurrencyManager from \"../../applications/currency-manager.mjs\";\nimport { formatNumber } from \"../../utils.mjs\";\n\n/**\n * @import { OrderUseConfiguration } from \"./_types.mjs\";\n */\n\n/**\n * An activity for issuing an order to a facility.\n */\nexport default class OrderActivity extends ActivityMixin(BaseOrderActivityData) {\n /* -------------------------------------------- */\n /* Model Configuration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static metadata = Object.freeze(foundry.utils.mergeObject(super.metadata, {\n type: \"order\",\n img: \"systems/dnd5e/icons/svg/activity/order.svg\",\n title: \"DND5E.FACILITY.Order.Issue\",\n usage: {\n actions: {\n pay: OrderActivity.#onPayOrder\n },\n chatCard: \"systems/dnd5e/templates/chat/order-activity-card.hbs\",\n dialog: OrderUsageDialog\n }\n }, { inplace: false }));\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n get canUse() {\n return super.canUse\n // Don't allow usage if facility is already executing the same order or has been disabled by attack\n && !this.inProgress && !this.item.system.disabled\n // Enlarge order cannot be executed if facility is already maximum size\n && ((this.order !== \"enlarge\") || (this.parent.size !== \"vast\"));\n }\n\n /* -------------------------------------------- */\n\n /**\n * Is this order currently in the process of being executed by its facility?\n * @type {boolean}\n */\n get inProgress() {\n if ( this.parent.progress.order !== this.order ) return false;\n // TODO: Ideally this would also check to see if the order has already been paid,\n // but that information is only part of the chat message and there isn't a clean\n // way to retrieve it at the moment\n return this.parent.progress.value > 0;\n }\n\n /* -------------------------------------------- */\n /* Activation */\n /* -------------------------------------------- */\n\n /**\n * Update building configuration.\n * @param {OrderUseConfiguration} usageConfig Order configuration.\n * @param {object} updates Item updates.\n * @protected\n */\n _finalizeBuild(usageConfig, updates) {\n updates[\"system.building.size\"] = usageConfig.building.size;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Update costs.\n * @param {OrderUseConfiguration} usageConfig Order configuration.\n * @param {object} updates Item updates.\n * @protected\n */\n _finalizeCosts(usageConfig, updates) {\n const { costs } = usageConfig;\n if ( costs.days ) updates[\"system.progress\"] = { value: 0, max: costs.days, order: this.order };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Update crafting configuration.\n * @param {OrderUseConfiguration} usageConfig Order configuration.\n * @param {object} updates Item updates.\n * @protected\n */\n _finalizeCraft(usageConfig, updates) {\n const { craft } = usageConfig;\n updates[\"system.craft\"] = { item: craft.item, quantity: 1 };\n if ( this.order === \"harvest\" ) updates[\"system.craft\"].quantity = craft.quantity;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Update facility size.\n * @param {OrderUseConfiguration} usageConfig Order configuration.\n * @param {object} updates Item updates.\n * @protected\n */\n _finalizeEnlarge(usageConfig, updates) {\n // Special facilities enlarge immediately.\n if ( (this.item.system.type.value !== \"special\") || (this.item.system.size === \"vast\") ) return;\n const sizes = Object.entries(CONFIG.DND5E.facilities.sizes).sort((a, b) => a.value - b.value);\n const index = sizes.findIndex(([size]) => size === this.item.system.size);\n updates[\"system.size\"] = sizes[index + 1][0];\n }\n\n /* -------------------------------------------- */\n\n /**\n * Update trading configuration.\n * @param {OrderUseConfiguration} usageConfig Order configuration.\n * @param {object} updates Item updates.\n * @protected\n */\n _finalizeTrade(usageConfig, updates) {\n const { costs, trade } = usageConfig;\n const { system } = this.item;\n updates[\"system.trade.pending.operation\"] = trade.sell ? \"sell\" : \"buy\";\n updates[\"system.trade.pending.creatures\"] = [];\n updates[\"system.trade.pending.value\"] = null;\n if ( trade.stock ) {\n if ( \"stocked\" in trade.stock ) {\n updates[\"system.trade.pending.stocked\"] = trade.stock.stocked;\n updates[\"system.trade.pending.operation\"] = trade.stock.stocked ? \"buy\" : null;\n }\n else updates[\"system.trade.pending.value\"] = trade.stock.value;\n }\n if ( trade.creatures ) {\n let creatures = (trade.creatures.buy ?? []).filter(_ => _);\n if ( trade.sell ) {\n creatures = [];\n for ( let i = 0; i < trade.creatures.sell?.length ?? 0; i++ ) {\n const sold = trade.creatures.sell[i];\n if ( sold ) creatures.push(system.trade.creatures.value[i]);\n }\n }\n updates[\"system.trade.pending.value\"] = trade.sell ? (trade.creatures.price ?? 0) : costs.gold;\n updates[\"system.trade.pending.creatures\"] = creatures;\n\n // Sold livestock are removed immediately. Bought livestock remain pending until the order is complete.\n if ( trade.sell ) {\n updates[\"system.trade.creatures.value\"] = system.trade.creatures.value.filter((_, i) => {\n return !trade.creatures.sell[i];\n });\n }\n }\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n async _finalizeUsage(usageConfig, results) {\n const updates = {};\n switch ( this.order ) {\n case \"build\": this._finalizeBuild(usageConfig, updates); break;\n case \"craft\":\n case \"harvest\":\n this._finalizeCraft(usageConfig, updates);\n break;\n case \"enlarge\": this._finalizeEnlarge(usageConfig, updates); break;\n case \"trade\": this._finalizeTrade(usageConfig, updates); break;\n }\n this._finalizeCosts(usageConfig, updates);\n return this.item.update(updates);\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n _prepareUsageConfig(config) {\n config.consume = false;\n return config;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n _prepareUsageScaling(usageConfig, messageConfig, item) {\n // FIXME: No scaling happening here, but this is the only context we have both usageConfig and messageConfig.\n const { costs, craft, trade } = usageConfig;\n messageConfig.data.flags.dnd5e.order = { costs, craft, trade };\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n _requiresConfigurationDialog(config) {\n return true;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n _usageChatButtons(message) {\n const { costs } = message.data.flags.dnd5e.order;\n if ( !costs.gold || costs.paid ) return [];\n return [{\n label: game.i18n.localize(\"DND5E.FACILITY.Costs.Automatic\"),\n icon: ' ',\n dataset: { action: \"pay\", method: \"automatic\" }\n }, {\n label: game.i18n.localize(\"DND5E.FACILITY.Costs.Manual\"),\n icon: ' ',\n dataset: { action: \"pay\", method: \"manual\" }\n }];\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n async _usageChatContext(message) {\n const { costs, craft, trade } = message.data.flags.dnd5e.order;\n const { type } = this.item.system;\n const supplements = [];\n if ( costs.days ) supplements.push(`\n ${game.i18n.localize(\"DND5E.DurationTime\")} \n ${game.i18n.format(\"DND5E.FACILITY.Costs.Days\", { days: costs.days })}\n `);\n if ( costs.gold ) supplements.push(`\n ${game.i18n.localize(\"DND5E.CurrencyGP\")} \n ${formatNumber(costs.gold)}\n (${game.i18n.localize(`DND5E.FACILITY.Costs.${costs.paid ? \"Paid\" : \"Unpaid\"}`)})\n `);\n if ( craft?.item ) {\n const item = await fromUuid(craft.item);\n supplements.push(`\n ${game.i18n.localize(\"DOCUMENT.Items\")} \n ${craft.quantity > 1 ? `${craft.quantity}×` : \"\"}\n ${item.toAnchor().outerHTML}\n `);\n }\n if ( trade?.stock?.value && trade.sell ) supplements.push(`\n ${game.i18n.localize(\"DND5E.FACILITY.Trade.Sell.Supplement\")} \n ${formatNumber(trade.stock.value)}\n ${CONFIG.DND5E.currencies[CONFIG.DND5E.defaultCurrency]?.abbreviation ?? \"\"}\n `);\n if ( trade?.creatures ) {\n const creatures = [];\n if ( trade.sell ) {\n for ( let i = 0; i < trade.creatures.sell.length; i++ ) {\n const sold = trade.creatures.sell[i];\n if ( sold ) creatures.push(await fromUuid(this.item.system.trade.creatures.value[i]));\n }\n }\n else creatures.push(...await Promise.all(trade.creatures.buy.filter(_ => _).map(uuid => fromUuid(uuid))));\n supplements.push(`\n ${game.i18n.localize(`DND5E.FACILITY.Trade.${trade.sell ? \"Sell\" : \"Buy\"}.Supplement`)} \n ${game.i18n.getListFormatter({ style: \"narrow\" }).format(creatures.map(a => a.toAnchor().outerHTML))}\n `);\n }\n const facilityType = game.i18n.localize(`DND5E.FACILITY.Types.${type.value.titleCase()}.Label.one`);\n const buttons = this._usageChatButtons(message);\n return {\n supplements,\n buttons: buttons.length ? buttons : null,\n description: game.i18n.format(\"DND5E.FACILITY.Use.Description\", {\n order: game.i18n.localize(`DND5E.FACILITY.Orders.${this.order}.inf`),\n link: this.item.toAnchor().outerHTML,\n facilityType: facilityType.toLocaleLowerCase(game.i18n.lang)\n })\n };\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /**\n * Handle deducting currency for the order.\n * @this {OrderActivity}\n * @param {PointerEvent} event The triggering event.\n * @param {HTMLElement} target The button that was clicked.\n * @param {ChatMessage5e} message The message associated with the activation.\n * @returns {Promise}\n */\n static async #onPayOrder(event, target, message) {\n const { method } = target.dataset;\n const order = message.getFlag(\"dnd5e\", \"order\");\n const config = foundry.utils.expandObject({ \"data.flags.dnd5e.order\": order });\n if ( method === \"automatic\" ) {\n try {\n await CurrencyManager.deductActorCurrency(this.actor, order.costs.gold, CONFIG.DND5E.defaultCurrency, {\n recursive: true,\n priority: \"high\"\n });\n } catch(err) {\n ui.notifications.error(err.message);\n return;\n }\n }\n foundry.utils.setProperty(config, \"data.flags.dnd5e.order.costs.paid\", true);\n const context = await this._usageChatContext(config);\n const content = await foundry.applications.handlebars.renderTemplate(this.metadata.usage.chatCard, context);\n await message.update({ content, flags: config.data.flags });\n }\n}\n","import ActivitySheet from \"./activity-sheet.mjs\";\n\n/**\n * Sheet for the save activity.\n */\nexport default class SaveSheet extends ActivitySheet {\n\n /** @inheritDoc */\n static DEFAULT_OPTIONS = {\n classes: [\"save-activity\"]\n };\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static PARTS = {\n ...super.PARTS,\n effect: {\n template: \"systems/dnd5e/templates/activity/save-effect.hbs\",\n templates: [\n ...super.PARTS.effect.templates,\n \"systems/dnd5e/templates/activity/parts/damage-part.hbs\",\n \"systems/dnd5e/templates/activity/parts/damage-parts.hbs\",\n \"systems/dnd5e/templates/activity/parts/save-damage.hbs\",\n \"systems/dnd5e/templates/activity/parts/save-details.hbs\",\n \"systems/dnd5e/templates/activity/parts/save-effect-settings.hbs\"\n ]\n }\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @override */\n _prepareAppliedEffectContext(context, effect) {\n effect.additionalSettings = \"systems/dnd5e/templates/activity/parts/save-effect-settings.hbs\";\n return effect;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareEffectContext(context, options) {\n context = await super._prepareEffectContext(context, options);\n\n context.abilityOptions = Object.entries(CONFIG.DND5E.abilities).map(([value, config]) => ({\n value, label: config.label\n }));\n context.calculationOptions = [\n { value: \"\", label: game.i18n.localize(\"DND5E.SAVE.FIELDS.save.dc.CustomFormula\") },\n { rule: true },\n { value: \"spellcasting\", label: game.i18n.localize(\"DND5E.SpellAbility\") },\n ...Object.entries(CONFIG.DND5E.abilities).map(([value, config]) => ({\n value, label: config.label, group: game.i18n.localize(\"DND5E.Abilities\")\n }))\n ];\n context.onSaveOptions = [\n { value: \"none\", label: game.i18n.localize(\"DND5E.SAVE.FIELDS.damage.onSave.None\") },\n { value: \"half\", label: game.i18n.localize(\"DND5E.SAVE.FIELDS.damage.onSave.Half\") },\n { value: \"full\", label: game.i18n.localize(\"DND5E.SAVE.FIELDS.damage.onSave.Full\") }\n ];\n\n return context;\n }\n}\n","import { simplifyBonus } from \"../../utils.mjs\";\nimport FormulaField from \"../fields/formula-field.mjs\";\nimport DamageField from \"../shared/damage-field.mjs\";\nimport BaseActivityData from \"./base-activity.mjs\";\nimport AppliedEffectField from \"./fields/applied-effect-field.mjs\";\n\nconst { ArrayField, BooleanField, SchemaField, SetField, StringField } = foundry.data.fields;\n\n/**\n * @import { SaveActivityData } from \"./_types.mjs\";\n */\n\n/**\n * Data model for an save activity.\n * @extends {BaseActivityData}\n * @mixes SaveActivityData\n */\nexport default class BaseSaveActivityData extends BaseActivityData {\n /** @inheritDoc */\n static defineSchema() {\n return {\n ...super.defineSchema(),\n damage: new SchemaField({\n onSave: new StringField({ required: true, blank: false, initial: \"half\" }),\n parts: new ArrayField(new DamageField())\n }),\n effects: new ArrayField(new AppliedEffectField({\n onSave: new BooleanField()\n })),\n save: new SchemaField({\n ability: new SetField(new StringField()),\n dc: new SchemaField({\n calculation: new StringField({ initial: \"initial\" }),\n formula: new FormulaField({ deterministic: true })\n })\n })\n };\n }\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /** @override */\n get ability() {\n if ( this.save.dc.calculation in CONFIG.DND5E.abilities ) return this.save.dc.calculation;\n if ( this.save.dc.calculation === \"spellcasting\" ) return this.spellcastingAbility;\n return this.save.ability.first() ?? null;\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /** @override */\n static migrateData(source) {\n if ( foundry.utils.getType(source.save?.ability) === \"string\" ) {\n if ( source.save.ability ) source.save.ability = [source.save.ability];\n else source.save.ability = [];\n }\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n static transformTypeData(source, activityData, options) {\n let calculation = source.system.save?.scaling;\n if ( calculation === \"flat\" ) calculation = \"\";\n else if ( calculation === \"spell\" ) calculation = \"spellcasting\";\n\n return foundry.utils.mergeObject(activityData, {\n damage: {\n onSave: (source.type === \"spell\") && (source.system.level === 0) ? \"none\" : \"half\",\n parts: source.system.damage?.parts?.map(part => this.transformDamagePartData(source, part)) ?? []\n },\n save: {\n ability: [source.system.save?.ability || Object.keys(CONFIG.DND5E.abilities)[0]],\n dc: {\n calculation,\n formula: String(source.system.save?.dc ?? \"\")\n }\n }\n });\n }\n\n /* -------------------------------------------- */\n /* Data Preparation */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareData() {\n super.prepareData();\n if ( this.save.dc.calculation === \"initial\" ) this.save.dc.calculation = this.isSpell ? \"spellcasting\" : \"\";\n this.save.dc.bonus = \"\";\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareFinalData(rollData) {\n rollData ??= this.getRollData({ deterministic: true });\n super.prepareFinalData(rollData);\n this.prepareDamageLabel(rollData);\n\n const bonus = this.save.dc.bonus ? simplifyBonus(this.save.dc.bonus, rollData) : 0;\n\n let ability;\n if ( this.save.dc.calculation ) ability = this.ability;\n else this.save.dc.value = simplifyBonus(this.save.dc.formula, rollData);\n this.save.dc.value ??= this.actor?.system.abilities?.[ability]?.dc\n ?? 8 + (this.actor?.system.attributes?.prof ?? 0);\n this.save.dc.value += bonus;\n\n if ( this.save.dc.value ) this.labels.save = game.i18n.format(\"DND5E.SaveDC\", {\n dc: this.save.dc.value,\n ability: CONFIG.DND5E.abilities[ability]?.label ?? \"\"\n });\n }\n\n /* -------------------------------------------- */\n /* Socket Event Handlers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _preCreate(data) {\n super._preCreate(data);\n if ( !(\"onSave\" in (data.damage ?? {})) && this.isSpell && (this.item.system.level === 0) ) {\n this.updateSource({ \"damage.onSave\": \"none\" });\n }\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n getDamageConfig(config={}) {\n const rollConfig = super.getDamageConfig(config);\n\n rollConfig.critical ??= {};\n rollConfig.critical.allow ??= false;\n\n return rollConfig;\n }\n}\n","import SaveSheet from \"../../applications/activity/save-sheet.mjs\";\nimport BaseSaveActivityData from \"../../data/activity/save-data.mjs\";\nimport { getSceneTargets } from \"../../utils.mjs\";\nimport ActivityMixin from \"./mixin.mjs\";\n\n/**\n * Activity for making saving throws and rolling damage.\n */\nexport default class SaveActivity extends ActivityMixin(BaseSaveActivityData) {\n /* -------------------------------------------- */\n /* Model Configuration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static LOCALIZATION_PREFIXES = [...super.LOCALIZATION_PREFIXES, \"DND5E.SAVE\"];\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static metadata = Object.freeze(\n foundry.utils.mergeObject(super.metadata, {\n type: \"save\",\n img: \"systems/dnd5e/icons/svg/activity/save.svg\",\n title: \"DND5E.SAVE.Title.one\",\n hint: \"DND5E.SAVE.Hint\",\n sheetClass: SaveSheet,\n usage: {\n actions: {\n rollDamage: SaveActivity.#rollDamage,\n rollSave: SaveActivity.#rollSave\n }\n }\n }, { inplace: false })\n );\n\n /* -------------------------------------------- */\n /* Activation */\n /* -------------------------------------------- */\n\n /** @override */\n _usageChatButtons(message) {\n const buttons = [];\n const dc = this.save.dc.value;\n\n for ( const abilityId of this.save.ability ) {\n const ability = CONFIG.DND5E.abilities[abilityId]?.label ?? \"\";\n buttons.push({\n label: `\n ${game.i18n.format(\"DND5E.SavingThrowDC\", { dc, ability })} \n ${game.i18n.format(\"DND5E.SavePromptTitle\", { ability })} \n `,\n icon: ' ',\n dataset: {\n dc,\n ability: abilityId,\n action: \"rollSave\",\n visibility: \"all\"\n }\n });\n }\n\n if ( this.damage.parts.length ) buttons.push({\n label: game.i18n.localize(\"DND5E.Damage\"),\n icon: ' ',\n dataset: {\n action: \"rollDamage\"\n }\n });\n return buttons.concat(super._usageChatButtons(message));\n }\n\n /* -------------------------------------------- */\n /* Rolling */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async rollDamage(config={}, dialog={}, message={}) {\n message = foundry.utils.mergeObject({\n \"data.flags.dnd5e.roll\": {\n damageOnSave: this.damage.onSave\n }\n }, message);\n return super.rollDamage(config, dialog, message);\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /**\n * Handle performing a damage roll.\n * @this {SaveActivity}\n * @param {PointerEvent} event Triggering click event.\n * @param {HTMLElement} target The capturing HTML element which defined a [data-action].\n * @param {ChatMessage5e} message Message associated with the activation.\n */\n static #rollDamage(event, target, message) {\n this.rollDamage({ event });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle performing a saving throw.\n * @this {SaveActivity}\n * @param {PointerEvent} event Triggering click event.\n * @param {HTMLElement} target The capturing HTML element which defined a [data-action].\n * @param {ChatMessage5e} message Message associated with the activation.\n */\n static async #rollSave(event, target, message) {\n const targets = getSceneTargets();\n if ( !targets.length && game.user.character ) targets.push(game.user.character);\n if ( !targets.length ) ui.notifications.warn(\"DND5E.ActionWarningNoToken\", { localize: true });\n const dc = parseInt(target.dataset.dc);\n for ( const token of targets ) {\n const actor = token instanceof Actor ? token : token.actor;\n const speaker = ChatMessage.getSpeaker({ actor, scene: canvas.scene, token: token.document });\n await actor.rollSavingThrow({\n event,\n ability: target.dataset.ability ?? this.save.ability.first(),\n target: Number.isFinite(dc) ? dc : this.save.dc.value\n }, {}, { data: { speaker } });\n }\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async getFavoriteData() {\n return foundry.utils.mergeObject(await super.getFavoriteData(), { save: this.save });\n }\n}\n","import ActivitySheet from \"./activity-sheet.mjs\";\n\n/**\n * Sheet for the summon activity.\n */\nexport default class SummonSheet extends ActivitySheet {\n\n /** @inheritDoc */\n static DEFAULT_OPTIONS = {\n classes: [\"summon-activity\"],\n actions: {\n addProfile: SummonSheet.#addProfile,\n deleteProfile: SummonSheet.#deleteProfile\n }\n };\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static PARTS = {\n ...super.PARTS,\n effect: {\n template: \"systems/dnd5e/templates/activity/summon-effect.hbs\",\n templates: [\n ...super.PARTS.effect.templates,\n \"systems/dnd5e/templates/activity/parts/summon-changes.hbs\",\n \"systems/dnd5e/templates/activity/parts/summon-profiles.hbs\"\n ]\n }\n };\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static CLEAN_ARRAYS = [...super.CLEAN_ARRAYS, \"profiles\"];\n\n /* -------------------------------------------- */\n\n /** @override */\n tabGroups = {\n sheet: \"identity\",\n activation: \"time\",\n effect: \"profiles\"\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareEffectContext(context, options) {\n context = await super._prepareEffectContext(context, options);\n\n context.abilityOptions = [\n {\n value: \"\", rule: true,\n label: game.i18n.format(\"DND5E.DefaultSpecific\", {\n default: this.activity.isSpell ? game.i18n.localize(\"DND5E.Spellcasting\").toLowerCase()\n : CONFIG.DND5E.abilities[this.activity.ability]?.label.toLowerCase()\n ?? game.i18n.localize(\"DND5E.None\").toLowerCase()\n })\n },\n ...Object.entries(CONFIG.DND5E.abilities).map(([value, { label }]) => ({ value, label }))\n ];\n context.creatureSizeOptions = Object.entries(CONFIG.DND5E.actorSizes).map(([value, config]) => ({\n value, label: config.label, selected: this.activity.creatureSizes.has(value)\n }));\n context.creatureTypeOptions = Object.entries(CONFIG.DND5E.creatureTypes).map(([value, config]) => ({\n value, label: config.label, selected: this.activity.creatureTypes.has(value)\n }));\n\n context.profileModes = [\n { value: \"\", label: game.i18n.localize(\"DND5E.SUMMON.FIELDS.summon.mode.Direct\") },\n { value: \"cr\", label: game.i18n.localize(\"DND5E.SUMMON.FIELDS.summon.mode.CR\") }\n ];\n context.profiles = this.activity.profiles.map((data, index) => ({\n data, index,\n collapsed: this.expandedSections.get(`profiles.${data._id}`) ? \"\" : \"collapsed\",\n fields: this.activity.schema.fields.profiles.element.fields,\n prefix: `profiles.${index}.`,\n source: context.source.profiles[index] ?? data,\n document: data.uuid ? fromUuidSync(data.uuid) : null,\n mode: this.activity.summon.mode,\n typeOptions: this.activity.summon.mode === \"cr\" ? context.creatureTypeOptions.map(t => ({\n ...t, selected: data.types.has(t.value)\n })) : null\n })).sort((lhs, rhs) =>\n (lhs.name || lhs.document?.name || \"\").localeCompare(rhs.name || rhs.document?.name || \"\", game.i18n.lang)\n );\n\n return context;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareIdentityContext(context, options) {\n context = await super._prepareIdentityContext(context, options);\n context.behaviorFields.push({\n field: context.fields.summon.fields.prompt,\n value: context.source.summon.prompt,\n input: context.inputs.createCheckboxInput\n });\n return context;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _getTabs() {\n const tabs = super._getTabs();\n tabs.effect.label = \"DND5E.SUMMON.SECTIONS.Summoning\";\n tabs.effect.icon = \"fa-solid fa-spaghetti-monster-flying\";\n tabs.effect.tabs = this._markTabs({\n profiles: {\n id: \"profiles\", group: \"effect\", icon: \"fa-solid fa-address-card\",\n label: \"DND5E.SUMMON.SECTIONS.Profiles\"\n },\n changes: {\n id: \"changes\", group: \"effect\", icon: \"fa-solid fa-sliders\",\n label: \"DND5E.SUMMON.SECTIONS.Changes\"\n }\n });\n return tabs;\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _onRender(context, options) {\n await super._onRender(context, options);\n this.element.querySelector(\".activity-profiles\").addEventListener(\"drop\", this.#onDrop.bind(this));\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle adding a new entry to the summoning profiles list.\n * @this {SummonSheet}\n * @param {Event} event Triggering click event.\n * @param {HTMLElement} target Button that was clicked.\n */\n static #addProfile(event, target) {\n this.activity.update({ profiles: [...this.activity.toObject().profiles, {}] });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle removing an entry from the summoning profiles list.\n * @this {SummonSheet}\n * @param {Event} event Triggering click event.\n * @param {HTMLElement} target Button that was clicked.\n */\n static #deleteProfile(event, target) {\n const profiles = this.activity.toObject().profiles;\n profiles.splice(target.closest(\"[data-index]\").dataset.index, 1);\n this.activity.update({ profiles });\n }\n\n /* -------------------------------------------- */\n /* Drag & Drop */\n /* -------------------------------------------- */\n\n /**\n * Handle dropping actors onto the sheet.\n * @param {Event} event Triggering drop event.\n */\n async #onDrop(event) {\n // Try to extract the data\n const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);\n\n // Handle dropping linked items\n if ( data?.type !== \"Actor\" ) return;\n const actor = await Actor.implementation.fromDropData(data);\n\n // If dropped onto existing profile, add or replace link\n const profileId = event.target.closest(\"[data-profile-id]\")?.dataset.profileId;\n if ( profileId ) {\n const profiles = this.activity.toObject().profiles;\n const profile = profiles.find(p => p._id === profileId);\n profile.uuid = actor.uuid;\n this.activity.update({ profiles });\n }\n\n // Otherwise create a new profile\n else this.activity.update({ profiles: [...this.activity.toObject().profiles, { uuid: actor.uuid }] });\n }\n}\n","import simplifyRollFormula from \"../../dice/simplify-roll-formula.mjs\";\nimport { formatCR, simplifyBonus } from \"../../utils.mjs\";\nimport ActivityUsageDialog from \"./activity-usage-dialog.mjs\";\n\nconst { BooleanField, StringField } = foundry.data.fields;\n\n/**\n * @import { ActivityRollData } from \"../../documents/_types.mjs\";\n */\n\n/**\n * Dialog for configuring the usage of the summon activity.\n */\nexport default class SummonUsageDialog extends ActivityUsageDialog {\n\n /** @inheritDoc */\n static PARTS = {\n ...super.PARTS,\n creation: {\n template: \"systems/dnd5e/templates/activity/summon-usage-creation.hbs\"\n }\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareCreationContext(context, options) {\n context = await super._prepareCreationContext(context, options);\n\n const profiles = this.activity.availableProfiles;\n if ( this._shouldDisplay(\"create.summons\") && (profiles.length || (this.activity.creatureSizes.size > 1)\n || (this.activity.creatureTypes.size > 1)) ) {\n context.hasCreation = true;\n context.summonsFields = [];\n\n if ( !foundry.utils.hasProperty(this.options.display, \"create.summons\") ) context.summonsFields.push({\n field: new BooleanField({ label: game.i18n.localize(\"DND5E.SUMMON.Action.Place\") }),\n name: \"create.summons\",\n value: this.config.create?.summons,\n input: context.inputs.createCheckboxInput\n });\n\n if ( this.config.create?.summons ) {\n const rollData = this.activity.getRollData();\n if ( profiles.length > 1 ) {\n let options = profiles.map(profile => ({\n value: profile._id, label: this.getProfileLabel(profile, rollData)\n }));\n if ( options.every(o => o.label.startsWith(\"1 × \")) ) {\n options = options.map(({ value, label }) => ({ value, label: label.replace(\"1 × \", \"\") }));\n }\n context.summonsFields.push({\n field: new StringField({\n required: true, blank: false, label: game.i18n.localize(\"DND5E.SUMMON.Profile.Label\")\n }),\n name: \"summons.profile\",\n value: this.config.summons?.profile,\n options\n });\n } else context.summonsProfile = profiles[0]._id;\n\n if ( this.activity.creatureSizes.size > 1 ) context.summonsFields.push({\n field: new StringField({ label: game.i18n.localize(\"DND5E.Size\") }),\n name: \"summons.creatureSize\",\n value: this.config.summons?.creatureSize,\n options: Array.from(this.activity.creatureSizes)\n .map(value => ({ value, label: CONFIG.DND5E.actorSizes[value]?.label }))\n .filter(k => k)\n });\n\n if ( this.activity.creatureTypes.size > 1 ) context.summonsFields.push({\n field: new StringField({ label: game.i18n.localize(\"DND5E.CreatureType\") }),\n name: \"summons.creatureType\",\n value: this.config.summons?.creatureType,\n options: Array.from(this.activity.creatureTypes)\n .map(value => ({ value, label: CONFIG.DND5E.creatureTypes[value]?.label }))\n .filter(k => k)\n });\n }\n }\n\n return context;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Determine the label for a profile in the ability use dialog.\n * @param {SummonsProfile} profile Profile for which to generate the label.\n * @param {ActivityRollData} rollData Roll data used to prepare the count.\n * @returns {string}\n */\n getProfileLabel(profile, rollData) {\n let label;\n if ( profile.name ) label = profile.name;\n else {\n switch ( this.activity.summon.mode ) {\n case \"cr\":\n const cr = simplifyBonus(profile.cr, rollData);\n label = game.i18n.format(\"DND5E.SUMMON.Profile.ChallengeRatingLabel\", { cr: formatCR(cr) });\n break;\n default:\n const doc = fromUuidSync(profile.uuid);\n if ( doc ) label = doc.name;\n break;\n }\n }\n label ??= \"—\";\n\n let count = simplifyRollFormula(Roll.replaceFormulaData(profile.count ?? \"1\", rollData));\n if ( Number.isNumeric(count) ) count = parseInt(count);\n if ( count ) label = `${count} × ${label}`;\n\n return label;\n }\n}\n","/**\n * @import { FilterDescription } from \"./_types.mjs\";\n */\n\n/**\n * Check some data against a filter to determine if it matches.\n * @param {object} data Data to check.\n * @param {FilterDescription|FilterDescription[]} filter Filter to compare against.\n * @returns {boolean}\n * @throws\n */\nexport function performCheck(data, filter=[]) {\n if ( Array.isArray(filter) ) return AND(data, filter);\n return _check(data, filter.k, filter.v, filter.o);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Determine the unique keys referenced by a set of filters.\n * @param {FilterDescription[]} filter Filter to examine.\n * @returns {Set}\n */\nexport function uniqueKeys(filter=[]) {\n const keys = new Set();\n const _uniqueKeys = filters => {\n for ( const f of filters ) {\n const operator = f.o in OPERATOR_FUNCTIONS;\n if ( operator && Array.isArray(f.v) ) _uniqueKeys(f.v);\n else if ( f.o === \"NOT\" ) _uniqueKeys([f.v]);\n else if ( !operator ) keys.add(f.k);\n }\n };\n _uniqueKeys(filter);\n return keys;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Internal check implementation.\n * @param {object} data Data to check.\n * @param {string} [keyPath] Path to individual piece within data to check.\n * @param {*} value Value to compare against or additional filters.\n * @param {string} [operation=\"_\"] Checking function to use.\n * @returns {boolean}\n * @internal\n * @throws\n */\nfunction _check(data, keyPath, value, operation=\"_\") {\n const operator = OPERATOR_FUNCTIONS[operation];\n if ( operator ) return operator(data, value);\n\n const comparison = COMPARISON_FUNCTIONS[operation];\n if ( !comparison ) throw new Error(`Comparison function \"${operation}\" could not be found.`);\n return comparison(foundry.utils.getProperty(data, keyPath), value);\n}\n\n/* -------------------------------------------- */\n/* Operator Functions */\n/* -------------------------------------------- */\n\n/**\n * Operator functions.\n * @enum {Function}\n */\nexport const OPERATOR_FUNCTIONS = {\n AND, NAND, OR, NOR, XOR, NOT\n};\n\n/* -------------------------------------------- */\n\n/**\n * Perform an AND check against all filters.\n * @param {object} data Data to check.\n * @param {FilterDescription[]} filter Filter to compare against.\n * @returns {boolean}\n */\nexport function AND(data, filter) {\n return filter.every(({k, v, o}) => _check(data, k, v, o));\n}\n\n/* -------------------------------------------- */\n\n/**\n * Perform an NAND check against all filters.\n * @param {object} data Data to check.\n * @param {FilterDescription[]} filter Filter to compare against.\n * @returns {boolean}\n */\nexport function NAND(data, filter) {\n return !filter.every(({k, v, o}) => _check(data, k, v, o));\n}\n\n/* -------------------------------------------- */\n\n/**\n * Perform an OR check against all filters.\n * @param {object} data Data to check.\n * @param {FilterDescription[]} filter Filter to compare against.\n * @returns {boolean}\n */\nexport function OR(data, filter) {\n return filter.some(({k, v, o}) => _check(data, k, v, o));\n}\n\n/* -------------------------------------------- */\n\n/**\n * Perform an NOR check against all filters.\n * @param {object} data Data to check.\n * @param {FilterDescription[]} filter Filter to compare against.\n * @returns {boolean}\n */\nexport function NOR(data, filter) {\n return !filter.some(({k, v, o}) => _check(data, k, v, o));\n}\n\n/* -------------------------------------------- */\n\n/**\n * Perform an XOR check against all filters.\n * @param {object} data Data to check.\n * @param {FilterDescription[]} filter Filter to compare against.\n * @returns {boolean}\n */\nexport function XOR(data, filter) {\n if ( !filter.length ) return false;\n let currentResult = _check(data, filter[0].k, filter[0].v, filter[0].o);\n for ( let i = 1; i < filter.length; i++ ) {\n const { k, v, o } = filter[i];\n currentResult ^= _check(data, k, v, o);\n }\n return Boolean(currentResult);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Invert the result of a nested check,\n * @param {object} data Data to check.\n * @param {FilterDescription} filter Filter to compare against.\n * @returns {boolean}\n */\nexport function NOT(data, filter) {\n const { k, v, o } = filter;\n return !_check(data, k, v, o);\n}\n\n/* -------------------------------------------- */\n/* Comparison Functions */\n/* -------------------------------------------- */\n\n/**\n * Currently supported comparison functions.\n * @enum {Function}\n */\nexport const COMPARISON_FUNCTIONS = {\n _: exact, exact, contains, icontains, startswith, istartswith, endswith,\n has, hasany, hasall, in: in_, gt, gte, lt, lte\n};\n\n/* -------------------------------------------- */\n\n/**\n * Check for an exact match. The default comparison mode if none is provided.\n * @param {*} data\n * @param {*} value\n * @returns {boolean}\n */\nexport function exact(data, value) {\n return data === value;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Check that data contains value.\n * @param {*} data\n * @param {*} value\n * @returns {boolean}\n */\nexport function contains(data, value) {\n return String(data).includes(String(value));\n}\n\n/* -------------------------------------------- */\n\n/**\n * Case-insensitive check that data contains value.\n * @param {*} data\n * @param {*} value\n * @returns {boolean}\n */\nexport function icontains(data, value) {\n return contains(String(data).toLocaleLowerCase(game.i18n.lang), String(value).toLocaleLowerCase(game.i18n.lang));\n}\n\n/* -------------------------------------------- */\n\n/**\n * Check that data starts with value.\n * @param {*} data\n * @param {*} value\n * @returns {boolean}\n */\nexport function startswith(data, value) {\n return String(data).startsWith(String(value));\n}\n\n/* -------------------------------------------- */\n\n/**\n * Case-insensitive check that data starts with value.\n * @param {*} data\n * @param {*} value\n * @returns {boolean}\n */\nexport function istartswith(data, value) {\n return startswith(String(data).toLocaleLowerCase(game.i18n.lang), String(value).toLocaleLowerCase(game.i18n.lang));\n}\n\n/* -------------------------------------------- */\n\n/**\n * Check that data ends with value.\n * @param {*} data\n * @param {*} value\n * @returns {boolean}\n */\nexport function endswith(data, value) {\n return String(data).endsWith(String(value));\n}\n\n/* -------------------------------------------- */\n\n/**\n * Check that the data collection has the provided value.\n * @param {*} data\n * @param {*} value\n * @returns {boolean}\n */\nexport function has(data, value) {\n // If the value is another filter description, apply that check against each member of the collection\n if ( foundry.utils.getType(value) === \"Object\" ) {\n switch ( foundry.utils.getType(data) ) {\n case \"Array\":\n case \"Set\": return !!data.find(d => performCheck(d, value));\n default: return false;\n }\n } else return in_(value, data);\n}\n\n/* -------------------------------------------- */\n\n/**\n * Check that the data collection has any of the provided values.\n * @param {*} data\n * @param {*} value\n * @returns {boolean}\n */\nexport function hasany(data, value) {\n return Array.from(value).some(v => has(data, v));\n}\n\n/* -------------------------------------------- */\n\n/**\n * Check that the data collection has all of the provided values.\n * @param {*} data\n * @param {*} value\n * @returns {boolean}\n */\nexport function hasall(data, value) {\n return Array.from(value).every(v => has(data, v));\n}\n\n/* -------------------------------------------- */\n\n/**\n * Check that data matches one of the provided values.\n * @param {*} data\n * @param {*} value\n * @returns {boolean}\n */\nexport function in_(data, value) {\n switch ( foundry.utils.getType(value) ) {\n case \"Array\": return value.includes(data);\n case \"Set\": return value.has(data);\n default: return false;\n }\n}\n\n/* -------------------------------------------- */\n\n/**\n * Check that value is greater than data.\n * @param {*} data\n * @param {*} value\n * @returns {boolean}\n */\nexport function gt(data, value) {\n return data > value;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Check that value is greater than or equal to data.\n * @param {*} data\n * @param {*} value\n * @returns {boolean}\n */\nexport function gte(data, value) {\n return data >= value;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Check that value is less than data.\n * @param {*} data\n * @param {*} value\n * @returns {boolean}\n */\nexport function lt(data, value) {\n return data < value;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Check that value is less than or equal to data.\n * @param {*} data\n * @param {*} value\n * @returns {boolean}\n */\nexport function lte(data, value) {\n return data <= value;\n}\n","import Application5e from \"../api/application.mjs\";\n\n/**\n * @import { CompendiumSourcePackageConfig5e, CompendiumSourcePackGroup5e } from \"../../data/settings/_types.mjs\";\n * @import { CompendiumBrowserSourceConfiguration } from \"./_types.mjs\";\n */\n\n/**\n * An application for configuring which compendium packs contribute their content to the compendium browser.\n * @extends Application5e\n */\nexport default class CompendiumBrowserSettingsConfig extends Application5e {\n constructor(options) {\n super(options);\n this.#selected = this.options.selected;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n static DEFAULT_OPTIONS = {\n id: \"compendium-browser-source-config\",\n classes: [\"dialog-lg\"],\n tag: \"form\",\n window: {\n title: \"DND5E.CompendiumBrowser.Sources.Label\",\n icon: \"fas fa-book-open-reader\",\n resizable: true\n },\n position: {\n width: 800,\n height: 650\n },\n actions: {\n clearFilter: CompendiumBrowserSettingsConfig.#onClearPackageFilter,\n selectPackage: CompendiumBrowserSettingsConfig.#onSelectPackage\n },\n selected: \"system\"\n };\n\n /* -------------------------------------------- */\n\n /** @override */\n static PARTS = {\n sidebar: {\n id: \"sidebar\",\n template: \"systems/dnd5e/templates/compendium/sources-sidebar.hbs\"\n },\n packs: {\n id: \"packs\",\n template: \"systems/dnd5e/templates/compendium/sources-packs.hbs\"\n }\n };\n\n /* -------------------------------------------- */\n\n /**\n * The number of milliseconds to delay between user keypresses before executing the package filter.\n * @type {number}\n */\n static FILTER_DELAY = 200;\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /**\n * The current package filter.\n * @type {string}\n */\n #filter = \"\";\n\n /* -------------------------------------------- */\n\n /**\n * The currently selected package.\n * @type {string}\n */\n #selected;\n\n /* -------------------------------------------- */\n\n _debouncedFilter = foundry.utils.debounce(this._onFilterPackages.bind(this), this.constructor.FILTER_DELAY);\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareContext(options) {\n const sources = this.constructor.collateSources();\n const byPackage = { world: new Set(), system: new Set() };\n\n for ( const { collection, documentName, metadata } of game.packs ) {\n if ( (documentName !== \"Actor\") && (documentName !== \"Item\") ) continue;\n let entry;\n if ( (metadata.packageType === \"world\") || (metadata.packageType === \"system\") ) {\n entry = byPackage[metadata.packageType];\n }\n else entry = byPackage[`module.${metadata.packageName}`] ??= new Set();\n entry.add(collection);\n }\n\n const packages = { };\n packages.world = this._preparePackageContext(\"world\", game.world, byPackage.world, sources);\n packages.system = this._preparePackageContext(\"system\", game.system, byPackage.system, sources);\n\n const modules = Object.entries(byPackage).reduce((arr, [k, packs]) => {\n if ( (k === \"world\") || (k === \"system\") ) return arr;\n const id = k.slice(7);\n const module = game.modules.get(id);\n arr.push(this._preparePackageContext(k, module, packs, sources));\n return arr;\n }, []);\n modules.sort((a, b) => a.title.localeCompare(b.title, game.i18n.lang));\n packages.modules = Object.fromEntries(modules.map(m => [m.id, m]));\n\n const packs = { actors: {}, items: {} };\n [[\"actors\", \"Actor\"], [\"items\", \"Item\"]].forEach(([p, type]) => {\n packs[p] = this._preparePackGroupContext(type, byPackage[this.#selected], sources);\n });\n\n return {\n ...await super._prepareContext(options),\n packages, packs,\n filter: this.#filter\n };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare render context for packages.\n * @param {string} id The package identifier.\n * @param {ClientPackage} pkg The package.\n * @param {Set} packs The packs belonging to this package.\n * @param {Set} sources The packs currently selected for inclusion.\n * @returns {CompendiumSourcePackageConfig5e}\n * @protected\n */\n _preparePackageContext(id, pkg, packs, sources) {\n const { title } = pkg;\n const all = packs.isSubsetOf(sources);\n const indeterminate = !all && packs.intersects(sources);\n return {\n id, title, indeterminate,\n checked: indeterminate || all,\n count: packs.size,\n active: this.#selected === id,\n filter: title.replace(/[^\\p{L} ]/gu, \"\").toLocaleLowerCase(game.i18n.lang)\n };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare render context for pack groups.\n * @param {string} documentType The pack group's Document type.\n * @param {Set} packs The packs provided by the selected package.\n * @param {Set} sources The packs currently selected for inclusion.\n * @returns {CompendiumSourcePackGroup5e}\n * @protected\n */\n _preparePackGroupContext(documentType, packs, sources) {\n packs = packs.filter(id => {\n const pack = game.packs.get(id);\n return pack.documentName === documentType;\n });\n const all = packs.isSubsetOf(sources);\n const indeterminate = !all && packs.intersects(sources);\n return {\n indeterminate,\n checked: indeterminate || all,\n entries: Array.from(packs.map(id => {\n const { collection, title, metadata } = game.packs.get(id);\n const { packageName, flags } = metadata;\n let tag = \"\";\n // Special case handling for D&D SRD.\n if ( packageName === \"dnd5e\" ) {\n tag = flags?.dnd5e?.sourceBook?.replace(\"SRD \", \"\");\n }\n return {\n tag, title,\n id: collection,\n checked: sources.has(id)\n };\n })).sort((a, b) => {\n return a.tag?.localeCompare(b.tag) || a.title.localeCompare(b.title, game.i18n.lang);\n })\n };\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _attachFrameListeners() {\n super._attachFrameListeners();\n this.element.addEventListener(\"keydown\", this._debouncedFilter, { passive: true });\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _attachPartListeners(partId, htmlElement, options) {\n super._attachPartListeners(partId, htmlElement, options);\n if ( partId === \"sidebar\" ) this._filterPackages();\n }\n\n /* -------------------------------------------- */\n\n /**\n * Execute the package list filter.\n * @protected\n */\n _filterPackages() {\n const query = this.#filter.replace(/[^\\p{L} ]/gu, \"\").toLocaleLowerCase(game.i18n.lang);\n this.element.querySelectorAll(\".package-list.modules > li\").forEach(item => {\n item.toggleAttribute(\"hidden\", query && !item.dataset.filter.includes(query));\n });\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _onChangeForm(formConfig, event) {\n super._onChangeForm(formConfig, event);\n if ( event.target.dataset.type ) this._onToggleSource(event.target);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle filtering the package sidebar.\n * @param {KeyboardEvent} event The triggering event.\n * @protected\n */\n _onFilterPackages(event) {\n if ( !event.target.matches(\"search > input\") ) return;\n this.#filter = event.target.value;\n this._filterPackages();\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle toggling a compendium browser source pack.\n * @param {CheckboxElement} target The element that was toggled.\n * @returns {Record}\n * @protected\n */\n _onTogglePack(target) {\n const packs = {};\n const { name, checked, indeterminate } = target;\n if ( (name === \"all-items\") || (name === \"all-actors\") ) {\n const [, documentType] = name.split(\"-\");\n const pkg = this.#selected === \"world\"\n ? game.world\n : this.#selected === \"system\"\n ? game.system\n : game.modules.get(this.#selected.slice(7));\n for ( const { id, type } of pkg.packs ) {\n if ( game[documentType].documentName === type ) packs[id] = indeterminate ? false : checked;\n }\n }\n else packs[name] = checked;\n return packs;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle toggling a compendium browser source package.\n * @param {CheckboxElement} target The element that was toggled.\n * @returns {Record}\n * @protected\n */\n _onTogglePackage(target) {\n const packs = {};\n const { name, checked, indeterminate } = target;\n const pkg = name === \"world\" ? game.world : name === \"system\" ? game.system : game.modules.get(name.slice(7));\n for ( const { id } of pkg.packs ) packs[id] = indeterminate ? false : checked;\n return packs;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Toggle a compendium browser source.\n * @param {CheckboxElement} target The element that was toggled.\n * @protected\n */\n async _onToggleSource(target) {\n let packs;\n switch ( target.dataset.type ) {\n case \"pack\": packs = this._onTogglePack(target); break;\n case \"package\": packs = this._onTogglePackage(target); break;\n default: return;\n }\n const setting = { ...game.settings.get(\"dnd5e\", \"packSourceConfiguration\"), ...packs };\n await game.settings.set(\"dnd5e\", \"packSourceConfiguration\", setting);\n this.render();\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle clearing the package filter.\n * @this {CompendiumBrowserSettingsConfig}\n * @param {PointerEvent} event The originating click event.\n * @param {HTMLElement} target The target of the click event.\n */\n static #onClearPackageFilter(event, target) {\n const input = target.closest(\"search\").querySelector(\":scope > input\");\n input.value = this.#filter = \"\";\n this._filterPackages();\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle selecting a package.\n * @this {CompendiumBrowserSettingsConfig}\n * @param {PointerEvent} event The originating click event.\n * @param {HTMLElement} target The target of the click event.\n */\n static #onSelectPackage(event, target) {\n const { packageId } = target.closest(\"[data-package-id]\")?.dataset ?? {};\n if ( !packageId ) return;\n this.#selected = packageId;\n this.render();\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /**\n * Collate sources for inclusion in the compendium browser.\n * @returns {Set} The set of packs that should be included in the compendium browser.\n */\n static collateSources() {\n const sources = new Set();\n const setting = game.settings.get(\"dnd5e\", \"packSourceConfiguration\");\n for ( const { collection, documentName } of game.packs ) {\n if ( (documentName !== \"Actor\") && (documentName !== \"Item\") ) continue;\n if ( setting[collection] !== false ) sources.add(collection);\n }\n return sources;\n }\n}\n","import * as Filter from \"../filter.mjs\";\nimport SourceField from \"../data/shared/source-field.mjs\";\nimport { getPluralRules } from \"../utils.mjs\";\nimport Application5e from \"./api/application.mjs\";\nimport CompendiumBrowserSettingsConfig from \"./settings/compendium-browser-settings.mjs\";\n\n/**\n * @import { FilterDescription } from \"../_types.mjs\";\n * @import {\n * CompendiumBrowserConfiguration, CompendiumBrowserFilterDefinition, CompendiumBrowserFilters\n * } from \"./_types.mjs\";\n */\n\n/**\n * Application for browsing, filtering, and searching for content between multiple compendiums.\n * @extends Application5e\n * @template CompendiumBrowserConfiguration\n */\nexport default class CompendiumBrowser extends Application5e {\n constructor(...args) {\n super(...args);\n\n this.#filters = this.options.filters?.initial ?? {};\n\n if ( \"mode\" in this.options ) {\n this._mode = this.options.mode;\n this._applyModeFilters(this.options.mode);\n }\n\n if ( foundry.utils.isEmpty(this.options.filters.locked) ) {\n const isAdvanced = this._mode === this.constructor.MODES.ADVANCED;\n const tab = this.constructor.TABS.find(t => t.tab === this.options.tab);\n if ( !tab || (!!tab.advanced !== isAdvanced) ) this.options.tab = isAdvanced ? \"actors\" : \"classes\";\n this._applyTabFilters(this.options.tab, { keepFilters: true });\n }\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n static DEFAULT_OPTIONS = {\n id: \"compendium-browser-{id}\",\n classes: [\"compendium-browser\", \"vertical-tabs\", \"dialog-lg\"],\n tag: \"form\",\n window: {\n title: \"DND5E.CompendiumBrowser.Title\",\n minimizable: true,\n resizable: true\n },\n actions: {\n configureSources: CompendiumBrowser.#onConfigureSources,\n clearName: CompendiumBrowser.#onClearName,\n openLink: CompendiumBrowser.#onOpenLink,\n setFilter: CompendiumBrowser.#onSetFilter,\n setType: CompendiumBrowser.#onSetType,\n toggleMode: CompendiumBrowser.#onToggleMode\n },\n form: {\n handler: CompendiumBrowser.#onHandleSubmit,\n closeOnSubmit: true\n },\n hint: null,\n position: {\n width: 850,\n height: 700\n },\n filters: {\n locked: {},\n initial: {\n documentClass: \"Item\",\n types: new Set([\"class\"])\n }\n },\n selection: {\n min: null,\n max: null\n },\n tab: \"classes\"\n };\n\n /* -------------------------------------------- */\n\n /** @override */\n static PARTS = {\n header: {\n id: \"header\",\n classes: [\"header\"],\n template: \"systems/dnd5e/templates/compendium/browser-header.hbs\"\n },\n search: {\n id: \"sidebar-search\",\n classes: [\"filter-element\"],\n container: { id: \"sidebar\", classes: [\"sidebar\", \"flexcol\"] },\n template: \"systems/dnd5e/templates/compendium/browser-sidebar-search.hbs\"\n },\n types: {\n id: \"sidebar-types\",\n container: { id: \"sidebar\", classes: [\"sidebar\", \"flexcol\"] },\n template: \"systems/dnd5e/templates/compendium/browser-sidebar-types.hbs\"\n },\n filters: {\n id: \"sidebar-filters\",\n container: { id: \"sidebar\", classes: [\"sidebar\", \"flexcol\"] },\n template: \"systems/dnd5e/templates/compendium/browser-sidebar-filters.hbs\",\n templates: [\"systems/dnd5e/templates/compendium/browser-sidebar-filter-set.hbs\"]\n },\n results: {\n id: \"results\",\n classes: [\"results\"],\n template: \"systems/dnd5e/templates/compendium/browser-results.hbs\",\n templates: [\"systems/dnd5e/templates/compendium/browser-entry.hbs\"],\n scrollable: [\"\"]\n },\n footer: {\n id: \"footer\",\n classes: [\"footer\"],\n template: \"systems/dnd5e/templates/compendium/browser-footer.hbs\"\n },\n tabs: {\n id: \"tabs\",\n classes: [\"tabs\", \"tabs-left\"],\n template: \"systems/dnd5e/templates/compendium/browser-tabs.hbs\"\n }\n };\n\n /* -------------------------------------------- */\n\n /**\n * Application tabs.\n * @type {CompendiumBrowserTabDescriptor5e[]}\n */\n static TABS = [\n {\n tab: \"classes\",\n label: \"TYPES.Item.classPl\",\n svg: \"systems/dnd5e/icons/svg/items/class.svg\",\n documentClass: \"Item\",\n types: [\"class\"]\n },\n {\n tab: \"subclasses\",\n label: \"TYPES.Item.subclassPl\",\n svg: \"systems/dnd5e/icons/svg/items/subclass.svg\",\n documentClass: \"Item\",\n types: [\"subclass\"]\n },\n {\n tab: \"races\",\n label: \"TYPES.Item.racePl\",\n svg: \"systems/dnd5e/icons/svg/items/race.svg\",\n documentClass: \"Item\",\n types: [\"race\"]\n },\n {\n tab: \"feats\",\n label: \"DND5E.CompendiumBrowser.Tabs.Feat.other\",\n icon: \"fas fa-star\",\n documentClass: \"Item\",\n types: [\"feat\"]\n },\n {\n tab: \"backgrounds\",\n label: \"TYPES.Item.backgroundPl\",\n svg: \"systems/dnd5e/icons/svg/items/background.svg\",\n documentClass: \"Item\",\n types: [\"background\"]\n },\n {\n tab: \"physical\",\n label: \"DND5E.CompendiumBrowser.Tabs.Item.other\",\n svg: \"systems/dnd5e/icons/svg/backpack.svg\",\n documentClass: \"Item\",\n types: [\"physical\"]\n },\n {\n tab: \"spells\",\n label: \"TYPES.Item.spellPl\",\n icon: \"fas fa-book\",\n documentClass: \"Item\",\n types: [\"spell\"]\n },\n {\n tab: \"monsters\",\n label: \"DND5E.CompendiumBrowser.Tabs.Monster.other\",\n svg: \"systems/dnd5e/icons/svg/actors/npc.svg\",\n documentClass: \"Actor\",\n types: [\"npc\"]\n },\n {\n tab: \"vehicles\",\n label: \"TYPES.Actor.vehiclePl\",\n svg: \"systems/dnd5e/icons/svg/actors/vehicle.svg\",\n documentClass: \"Actor\",\n types: [\"vehicle\"]\n },\n {\n tab: \"actors\",\n label: \"DOCUMENT.Actors\",\n svg: \"systems/dnd5e/icons/svg/actors/npc.svg\",\n documentClass: \"Actor\",\n advanced: true\n },\n {\n tab: \"items\",\n label: \"DOCUMENT.Items\",\n svg: \"systems/dnd5e/icons/svg/backpack.svg\",\n documentClass: \"Item\",\n advanced: true\n }\n ];\n\n /* -------------------------------------------- */\n\n /**\n * Available filtering modes.\n * @enum {number}\n */\n static MODES = {\n BASIC: 1,\n ADVANCED: 2\n };\n\n /* -------------------------------------------- */\n\n /**\n * Batching configuration.\n * @type {Record}\n */\n static BATCHING = {\n /**\n * The number of pixels before reaching the end of the scroll container to begin loading additional entries.\n */\n MARGIN: 50,\n\n /**\n * The number of entries to load per batch.\n */\n SIZE: 50\n };\n\n /* -------------------------------------------- */\n\n /**\n * The number of milliseconds to delay between user keypresses before executing a search.\n * @type {number}\n */\n static SEARCH_DELAY = 200;\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /**\n * Should the selection controls be displayed?\n * @type {boolean}\n */\n get displaySelection() {\n return !!this.options.selection.min || !!this.options.selection.max;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Currently defined filters.\n */\n #filters;\n\n /**\n * Current filters selected.\n * @type {CompendiumBrowserFilters}\n */\n get currentFilters() {\n const filters = foundry.utils.mergeObject(\n this.#filters,\n this.options.filters.locked,\n { inplace: false }\n );\n delete filters.exclusive;\n filters.documentClass ??= \"Item\";\n if ( filters.additional?.source ) {\n filters.additional.source = Object.entries(filters.additional.source).reduce((obj, [k, v]) => {\n obj[k.slugify({ strict: true })] = v;\n return obj;\n }, {});\n }\n return filters;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Fetched results.\n * @type {Promise|object[]|Document[]}\n */\n #results;\n\n /* -------------------------------------------- */\n\n /**\n * The index of the next result to render as part of batching.\n * @type {number}\n */\n #resultIndex = -1;\n\n /* -------------------------------------------- */\n\n /**\n * Whether rendering is currently throttled.\n * @type {boolean}\n */\n #renderThrottle = false;\n\n /* -------------------------------------------- */\n\n /**\n * UUIDs of currently selected documents.\n * @type {Set}\n */\n #selected = new Set();\n\n get selected() {\n return this.#selected;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Suffix used for localization selection messages based on min and max values.\n * @type {string|null}\n */\n get #selectionLocalizationSuffix() {\n const max = this.options.selection.max;\n const min = this.options.selection.min;\n if ( !min && !max ) return null;\n if ( !min && max ) return \"Max\";\n if ( min && !max ) return \"Min\";\n if ( min !== max ) return \"Range\";\n return \"Single\";\n }\n\n /* -------------------------------------------- */\n\n /**\n * The cached set of available sources to filter on.\n * @type {Record}\n */\n #sources;\n\n /* -------------------------------------------- */\n\n /**\n * The mode the browser is currently in.\n * @type {CompendiumBrowser.MODES}\n */\n _mode = this.constructor.MODES.BASIC;\n\n /* -------------------------------------------- */\n\n /**\n * The function to invoke when searching results by name.\n * @type {Function}\n */\n _debouncedSearch = foundry.utils.debounce(this._onSearchName.bind(this), this.constructor.SEARCH_DELAY);\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _configureRenderOptions(options) {\n super._configureRenderOptions(options);\n if ( options.isFirstRender ) {\n const tab = this.constructor.TABS.find(t => t.tab === this.options.tab);\n if ( tab ) foundry.utils.setProperty(options, \"dnd5e.browser.types\", tab.types);\n }\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareContext(options) {\n const context = await super._prepareContext(options);\n context.filters = this.currentFilters;\n\n let dataModels = Object.entries(CONFIG[context.filters.documentClass].dataModels);\n if ( context.filters.types?.size ) dataModels = dataModels.filter(([type]) => context.filters.types.has(type));\n context.filterDefinitions = dataModels\n .map(([, d]) => d.compendiumBrowserFilters ?? new Map())\n .reduce((final, second) => CompendiumBrowser.intersectFilters(second, final, context.filters), null) ?? new Map();\n context.filterDefinitions.set(\"source\", {\n label: \"DND5E.SOURCE.FIELDS.source.label\",\n type: \"set\",\n config: {\n keyPath: \"system.source.slug\",\n choices: foundry.utils.mergeObject(\n this.#sources ?? {},\n Object.fromEntries(Object.keys(this.options.filters?.locked?.additional?.source ?? {}).map(k => {\n return [k.slugify({ strict: true }), CONFIG.DND5E.sourceBooks[k] ?? k];\n })), { inplace: false }\n )\n }\n });\n return context;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _preparePartContext(partId, context, options) {\n await super._preparePartContext(partId, context, options);\n switch ( partId ) {\n case \"documentClass\":\n case \"search\":\n case \"types\":\n case \"filters\": return this._prepareSidebarContext(partId, context, options);\n case \"results\": return this._prepareResultsContext(context, options);\n case \"footer\": return this._prepareFooterContext(context, options);\n case \"tabs\": return this._prepareTabsContext(context, options);\n case \"header\": return this._prepareHeaderContext(context, options);\n }\n return context;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare the footer context.\n * @param {ApplicationRenderContext} context Shared context provided by _prepareContext.\n * @param {HandlebarsRenderOptions} options Options which configure application rendering behavior.\n * @returns {Promise} Context data for a specific part.\n * @protected\n */\n async _prepareFooterContext(context, options) {\n const value = this.#selected.size;\n const { max, min } = this.options.selection;\n\n context.displaySelection = this.displaySelection;\n context.invalid = (value < (min || -Infinity)) || (value > (max || Infinity));\n const suffix = this.#selectionLocalizationSuffix;\n context.summary = suffix ? game.i18n.format(\n `DND5E.CompendiumBrowser.Selection.Summary.${suffix}`, { max, min, value }\n ) : value;\n const pr = getPluralRules();\n context.invalidTooltip = game.i18n.format(`DND5E.CompendiumBrowser.Selection.Warning.${suffix}`, {\n max, min, value,\n document: game.i18n.localize(`DND5E.CompendiumBrowser.Selection.Warning.Document.${pr.select(max || min)}`)\n });\n return context;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare the header context.\n * @param {ApplicationRenderContext} context Shared context provided by _prepareContext.\n * @param {HandlebarsRenderOptions} options Options which configure rendering behavior.\n * @returns {Promise}\n * @protected\n */\n async _prepareHeaderContext(context, options) {\n context.showModeToggle = foundry.utils.isEmpty(this.options.filters.locked);\n context.isAdvanced = this._mode === this.constructor.MODES.ADVANCED;\n return context;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare the sidebar context.\n * @param {string} partId The part being rendered.\n * @param {ApplicationRenderContext} context Shared context provided by _prepareContext.\n * @param {HandlebarsRenderOptions} options Options which configure application rendering behavior.\n * @returns {Promise} Context data for a specific part.\n * @protected\n */\n async _prepareSidebarContext(partId, context, options) {\n context.isLocked = {};\n const lockExclusive = this.options.filters.locked.exclusive === true;\n context.isLocked.filters = (\"additional\" in this.options.filters.locked);\n context.isLocked.types = (\"types\" in this.options.filters.locked) || context.isLocked.filters;\n context.isLocked.documentClass = (\"documentClass\" in this.options.filters.locked) || context.isLocked.types;\n const types = foundry.utils.getProperty(options, \"dnd5e.browser.types\") ?? [];\n\n if ( partId === \"search\" ) {\n context.name = this.#filters.name;\n }\n\n else if ( partId === \"types\" ) {\n context.showTypes = (types.length !== 1) || (types[0] === \"physical\");\n context.types = CONFIG[context.filters.documentClass].documentClass.compendiumBrowserTypes({\n chosen: context.filters.types\n });\n\n // Special case handling for 'Items' tab in basic mode.\n if ( types[0] === \"physical\" ) context.types = context.types.physical.children;\n\n if ( context.isLocked.types ) {\n for ( const [key, value] of Object.entries(context.types) ) {\n if ( !value.children && !value.chosen ) delete context.types[key];\n else if ( value.children ) {\n for ( const [k, v] of Object.entries(value.children) ) {\n if ( !v.chosen ) delete value.children[k];\n }\n if ( foundry.utils.isEmpty(value.children) ) delete context.types[key];\n }\n }\n }\n }\n\n else if ( partId === \"filters\" ) {\n context.additional = Array.from(context.filterDefinitions?.entries() ?? []).reduce((arr, [key, data]) => {\n // Special case handling for 'Feats' tab in basic mode.\n if ( (types[0] === \"feat\") && (key === \"category\") ) return arr;\n\n let sort = 0;\n switch ( data.type ) {\n case \"boolean\": sort = 1; break;\n case \"range\": sort = 2; break;\n case \"set\": sort = 3; break;\n }\n\n const generateLocked = (data, filterDef) => {\n if ( lockExclusive && (data !== undefined) ) {\n if ( filterDef?.type === \"range\" ) return { min: true, max: true };\n if ( filterDef?.type === \"set\" ) {\n return Object.fromEntries(Object.keys(filterDef.config.choices).map(k => [k, true]));\n }\n return true;\n }\n if ( foundry.utils.getType(data) === \"Object\" ) {\n return Object.fromEntries(Object.entries(data).map(([k, v]) => [k, generateLocked(v)]));\n }\n return data !== undefined;\n };\n\n const pushFilter = data => arr.push(foundry.utils.mergeObject(data, {\n key, sort,\n value: context.filters.additional?.[key],\n locked: generateLocked(this.options.filters.locked?.additional?.[key], data)\n }, { inplace: false }));\n\n data.expandId = key;\n\n if ( data.type === \"set\" ) {\n const groups = Object.entries(data.config.choices).reduce((groups, [k, v]) => {\n groups[v.group] ??= {};\n groups[v.group][k] = v;\n return groups;\n }, {});\n if ( Object.keys(groups).length > 1 ) Object.entries(groups).forEach(([group, choices], index) => pushFilter({\n ...data,\n expandId: `${key}-${group}`,\n expanded: this.expandedSections.get(`${key}-${group}`) ?? !data.config.collapseGroup?.(group),\n label: game.i18n.format(\"DND5E.CompendiumBrowser.Filters.Grouped\", {\n type: game.i18n.localize(data.label), group\n }),\n config: { ...data.config, choices }\n }));\n\n else pushFilter({\n ...data, expanded: this.expandedSections.get(data.expandId) ?? !data.collapseGroup?.(null)\n });\n }\n else pushFilter(data);\n\n return arr;\n }, []);\n\n context.additional.sort((a, b) => a.sort - b.sort);\n }\n\n return context;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare the results context.\n * @param {ApplicationRenderContext} context Shared context provided by _prepareContext.\n * @param {HandlebarsRenderOptions} options Options which configure application rendering behavior.\n * @returns {Promise} Context data for a specific part.\n * @protected\n */\n async _prepareResultsContext(context, options) {\n // TODO: Determine if new set of results need to be fetched, otherwise use old results and re-sort as necessary\n // Sorting changes alone shouldn't require a re-fetch, but any change to filters will\n const filters = CompendiumBrowser.applyFilters(context.filterDefinitions, context.filters);\n // Add the name & arbitrary filters\n if ( this.#filters.name?.length ) filters.push({ k: \"name\", o: \"icontains\", v: this.#filters.name });\n if ( context.filters.arbitrary?.length ) filters.push(...context.filters.arbitrary);\n this.#results = CompendiumBrowser.fetch(CONFIG[context.filters.documentClass].documentClass, {\n filters,\n types: context.filters.types,\n indexFields: new Set([\"system.source\"])\n });\n context.displaySelection = this.displaySelection;\n context.hint = this.options.hint;\n return context;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare the tabs context.\n * @param {ApplicationRenderContext} context Shared context provided by _prepareContext.\n * @param {HandlebarsRenderOptions} options Options which configure application rendering behavior.\n * @returns {Promise}\n * @protected\n */\n async _prepareTabsContext(context, options) {\n // If we are locked to a particular filter, do not show tabs.\n if ( !foundry.utils.isEmpty(this.options.filters.locked) ) {\n context.tabs = [];\n return context;\n }\n\n const advanced = this._mode === this.constructor.MODES.ADVANCED;\n context.tabs = foundry.utils.deepClone(this.constructor.TABS.filter(t => !!t.advanced === advanced));\n const tab = options.isFirstRender ? this.options.tab : this.tabGroups.primary;\n const activeTab = context.tabs.find(t => t.tab === tab) ?? context.tabs[0];\n activeTab.active = true;\n\n return context;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _renderFrame(options) {\n const frame = await super._renderFrame(options);\n if ( game.user.isGM ) {\n frame.querySelector('[data-action=\"close\"]').insertAdjacentHTML(\"beforebegin\", `\n \n `);\n }\n return frame;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Render a single result entry.\n * @param {object|Document} entry The entry.\n * @param {string} documentClass The entry's Document class.\n * @returns {Promise}\n * @protected\n */\n async _renderResult(entry, documentClass) {\n const { img, name, type, uuid, system } = entry;\n // TODO: Provide more useful subtitles.\n const subtitle = CONFIG[documentClass].typeLabels[type] ?? \"\";\n const source = system?.source?.value ?? \"\";\n const context = {\n entry: { img, name, subtitle, uuid, source },\n displaySelection: this.displaySelection,\n selected: this.#selected.has(uuid)\n };\n const html = await foundry.applications.handlebars.renderTemplate(\n \"systems/dnd5e/templates/compendium/browser-entry.hbs\", context\n );\n const element = foundry.utils.parseHTML(html);\n if ( documentClass !== \"Item\" ) return element;\n element.dataset.tooltip = `\n \n `;\n element.dataset.tooltipClass = \"dnd5e2 dnd5e-tooltip item-tooltip\";\n element.dataset.tooltipDirection ??= \"RIGHT\";\n return element;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Render results once loaded to avoid holding up initial app display.\n * @protected\n */\n async _renderResults() {\n let rendered = [];\n const { documentClass } = this.currentFilters;\n const results = await this.#results;\n this.#results = results;\n const batchEnd = Math.min(this.constructor.BATCHING.SIZE, results.length);\n for ( let i = 0; i < batchEnd; i++ ) {\n rendered.push(this._renderResult(results[i], documentClass));\n }\n this.element.querySelector(\".results-loading\").hidden = true;\n this.element.querySelector('[data-application-part=\"results\"] .item-list')\n .replaceChildren(...(await Promise.all(rendered)));\n this.#resultIndex = batchEnd;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Show a list of applicable source filters for the available results.\n * @protected\n */\n async _renderSourceFilters() {\n const sources = [];\n for ( const result of this.#results ) {\n const source = foundry.utils.getProperty(result, \"system.source\");\n if ( foundry.utils.getType(source) !== \"Object\" ) continue;\n const { slug, value } = source;\n sources.push({ slug, value: CONFIG.DND5E.sourceBooks[value] ?? value });\n }\n sources.sort((a, b) => a.value.localeCompare(b.value, game.i18n.lang));\n this.#sources = Object.fromEntries(sources.map(({ slug, value }) => [slug, value]));\n const filters = this.element.querySelector('[data-application-part=\"filters\"]');\n filters.querySelector('[data-filter-id=\"source\"]')?.remove();\n if ( !sources.length ) return;\n const lockExclusive = this.options.filters?.locked?.exclusive === true;\n const lockedSource = this.options.filters?.locked?.additional?.source;\n const locked = lockExclusive && (lockedSource !== undefined)\n ? Object.fromEntries(Object.keys(this.#sources).map(k => [k, true]))\n : Object.entries(lockedSource ?? {}).reduce((obj, [k, v]) => {\n obj[k.slugify({ strict: true })] = v;\n return obj;\n }, {});\n const filter = await foundry.applications.handlebars.renderTemplate(\n \"systems/dnd5e/templates/compendium/browser-sidebar-filter-set.hbs\",\n {\n locked,\n value: lockExclusive && (lockedSource !== undefined) ? {} : locked,\n key: \"source\",\n expandId: \"source\",\n label: \"DND5E.SOURCE.FIELDS.source.label\",\n config: { choices: this.#sources },\n partId: `${this.id}-filters`\n }\n );\n filters.insertAdjacentElement(\"beforeend\", foundry.utils.parseHTML(filter));\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n changeTab(tab, group, options={}) {\n super.changeTab(tab, group, options);\n const target = this.element.querySelector(`nav.tabs [data-group=\"${group}\"][data-tab=\"${tab}\"]`);\n let { types } = target.dataset;\n types = types ? types.split(\",\") : [];\n this._applyTabFilters(tab);\n this.render({ parts: [\"results\", \"filters\", \"types\"], dnd5e: { browser: { types } }, changedTab: true });\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _attachFrameListeners() {\n super._attachFrameListeners();\n this.element.addEventListener(\"scroll\", this._onScrollResults.bind(this), { capture: true, passive: true });\n this.element.addEventListener(\"dragstart\", this._onDragStart.bind(this));\n this.element.addEventListener(\"keydown\", this._debouncedSearch, { passive: true });\n this.element.addEventListener(\"keydown\", this._onKeyAction.bind(this), { passive: true });\n this.element.addEventListener(\"pointerdown\", event => {\n if ( (event.button === 1) && document.getElementById(\"tooltip\")?.classList.contains(\"active\") ) {\n event.preventDefault();\n }\n });\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _attachPartListeners(partId, htmlElement, options) {\n super._attachPartListeners(partId, htmlElement, options);\n if ( partId === \"results\" ) this._renderResults().then(() => {\n if ( options.isFirstRender || options.changedTab ) this._renderSourceFilters();\n });\n else if ( partId === \"types\" ) this.#adjustCheckboxStates(htmlElement);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Apply filters based on the compendium browser's mode.\n * @param {CompendiumBrowser.MODES} mode The mode.\n * @protected\n */\n _applyModeFilters(mode) {\n const isAdvanced = mode === this.constructor.MODES.ADVANCED;\n delete this.#filters.types;\n delete this.#filters.additional;\n if ( isAdvanced ) this.#filters.documentClass = \"Actor\";\n else {\n this.#filters.documentClass = \"Item\";\n this.#filters.types = new Set([\"class\"]);\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Apply filters based on the selected tab.\n * @param {string} id The tab ID.\n * @param {object} [options] Additional options\n * @param {boolean} [options.keepFilters=false] Whether to keep the existing additional filters\n * @protected\n */\n _applyTabFilters(id, { keepFilters=false }={}) {\n const tab = this.constructor.TABS.find(t => t.tab === id);\n if ( !tab ) return;\n const { documentClass, types } = tab;\n if ( !keepFilters ) delete this.#filters.additional;\n this.#filters.documentClass = documentClass;\n this.#filters.types = new Set(types);\n\n // Special case handling for 'Items' tab in basic mode.\n if ( id === \"physical\" ) {\n const physical = Item.implementation.compendiumBrowserTypes().physical.children;\n Object.keys(physical).forEach(this.#filters.types.add, this.#filters.types);\n }\n\n // Special case handling for 'Feats' tab in basic mode.\n if ( id === \"feats\" ) {\n this.#filters.additional ??= {};\n foundry.utils.mergeObject(this.#filters.additional, { category: { feat: 1 } });\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Adjust the states of group checkboxes to make then indeterminate if only some of their children are selected.\n * @param {HTMLElement} htmlElement Element within which to find groups.\n */\n #adjustCheckboxStates(htmlElement) {\n for ( const groupArea of htmlElement.querySelectorAll(\".type-group\") ) {\n const group = groupArea.querySelector(\".type-group-header dnd5e-checkbox\");\n const children = groupArea.querySelectorAll(\".wrapper dnd5e-checkbox\");\n if ( Array.from(children).every(e => e.checked) ) {\n group.checked = true;\n group.indeterminate = false;\n } else {\n group.checked = group.indeterminate = Array.from(children).some(e => e.checked);\n }\n }\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n _onChangeForm(formConfig, event) {\n if ( event.target.name === \"selected\" ) {\n if ( event.target.checked ) this.#selected.add(event.target.value);\n else this.#selected.delete(event.target.value);\n event.target.closest(\".item\").classList.toggle(\"selected\", event.target.checked);\n this.render({ parts: [\"footer\"] });\n }\n if ( event.target.name?.startsWith(\"additional.\") ) CompendiumBrowser.#onSetFilter.call(this, event, event.target);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle dragging an entry.\n * @param {DragEvent} event The drag event.\n * @protected\n */\n _onDragStart(event) {\n const { uuid } = event.target.closest(\"[data-uuid]\")?.dataset ?? {};\n try {\n const { type } = foundry.utils.parseUuid(uuid);\n event.dataTransfer.setData(\"text/plain\", JSON.stringify({ type, uuid }));\n } catch(e) {\n console.error(e);\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle triggering an action via keyboard.\n * @param {KeyboardEvent} event The originating event.\n * @protected\n */\n _onKeyAction(event) {\n const target = event.target.closest(\"[data-action]\");\n if ( (event.key !== \" \") || !target ) return;\n const { action } = target.dataset;\n const handler = this.options.actions[action];\n if ( handler ) handler.call(this, event, target);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle rendering a new batch of results when the user scrolls to the bottom of the list.\n * @param {Event} event The originating scroll event.\n * @protected\n */\n async _onScrollResults(event) {\n if ( this.#renderThrottle || !event.target.matches('[data-application-part=\"results\"]') ) return;\n if ( (this.#results instanceof Promise) || (this.#resultIndex >= this.#results.length) ) return;\n const { scrollTop, scrollHeight, clientHeight } = event.target;\n if ( scrollTop + clientHeight < scrollHeight - this.constructor.BATCHING.MARGIN ) return;\n this.#renderThrottle = true;\n const { documentClass } = this.currentFilters;\n const rendered = [];\n const batchStart = this.#resultIndex;\n const batchEnd = Math.min(batchStart + this.constructor.BATCHING.SIZE, this.#results.length);\n for ( let i = batchStart; i < batchEnd; i++ ) {\n rendered.push(this._renderResult(this.#results[i], documentClass));\n }\n this.element.querySelector('[data-application-part=\"results\"] .item-list').append(...(await Promise.all(rendered)));\n this.#resultIndex = batchEnd;\n this.#renderThrottle = false;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle searching for a Document by name.\n * @param {KeyboardEvent} event The triggering event.\n * @protected\n */\n _onSearchName(event) {\n if ( !event.target.matches(\"search > input\") ) return;\n this.#filters.name = event.target.value;\n this.render({ parts: [\"results\"] });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle configuring compendium browser sources.\n * @this {CompendiumBrowser}\n */\n static #onConfigureSources() {\n new CompendiumBrowserSettingsConfig().render({ force: true });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle clearing the name filter.\n * @this {CompendiumBrowser}\n * @param {PointerEvent} event The originating click event.\n * @param {HTMLElement} target The target of the click event.\n */\n static async #onClearName(event, target) {\n const input = target.closest(\"search\").querySelector(\":scope > input\");\n input.value = this.#filters.name = \"\";\n this.render({ parts: [\"results\"] });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle form submission with selection.\n * @this {CompendiumBrowser}\n * @param {SubmitEvent} event The form submission event.\n * @param {HTMLFormElement} form The submitted form element.\n * @param {FormDataExtended} formData The data from the submitted form.\n */\n static async #onHandleSubmit(event, form, formData) {\n if ( !this.displaySelection ) return;\n\n const value = this.#selected.size;\n const { max, min } = this.options.selection;\n if ( (value < (min || -Infinity)) || (value > (max || Infinity)) ) {\n const suffix = this.#selectionLocalizationSuffix;\n const pr = getPluralRules();\n throw new Error(game.i18n.format(`DND5E.CompendiumBrowser.Selection.Warning.${suffix}`, {\n max, min, value,\n document: game.i18n.localize(`DND5E.CompendiumBrowser.Selection.Warning.Document.${pr.select(max || min)}`)\n }));\n }\n\n /**\n * Hook event that fires when a compendium browser is submitted with selected items.\n * @function dnd5e.compendiumBrowserSelection\n * @memberof hookEvents\n * @param {CompendiumBrowser} browser Compendium Browser application being submitted.\n * @param {Set} selected Set of document UUIDs that are selected.\n */\n Hooks.callAll(\"dnd5e.compendiumBrowserSelection\", this, this.#selected);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle opening a link to an item.\n * @this {CompendiumBrowser}\n * @param {PointerEvent} event The originating click event.\n * @param {HTMLElement} target The capturing HTML element which defined a [data-action].\n */\n static async #onOpenLink(event, target) {\n (await fromUuid(target.closest(\"[data-uuid]\")?.dataset.uuid))?.sheet?.render(true);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle setting the document class or a filter.\n * @this {CompendiumBrowser}\n * @param {PointerEvent} event The originating click event.\n * @param {HTMLElement} target The capturing HTML element which defined a [data-action].\n */\n static async #onSetFilter(event, target) {\n const name = target.name;\n const value = target.value;\n const existingValue = foundry.utils.getProperty(this.#filters, name);\n if ( value === existingValue ) return;\n foundry.utils.setProperty(this.#filters, name, value === \"\" ? undefined : value);\n\n if ( target.tagName === \"BUTTON\" ) for ( const button of this.element.querySelectorAll(`[name=\"${name}\"]`) ) {\n button.ariaPressed = button.value === value;\n }\n\n const activeTab = this.constructor.TABS.find(t => t.tab === this.tabGroups.primary);\n this.render({ parts: [\"filters\", \"results\"], dnd5e: { browser: { types: activeTab?.types } } });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle setting a type restriction.\n * @this {CompendiumBrowser}\n * @param {PointerEvent} event The originating click event.\n * @param {HTMLElement} target The capturing HTML element which defined a [data-action].\n */\n static async #onSetType(event, target) {\n this.#filters.types ??= new Set();\n\n if ( target.defaultValue ) {\n if ( target.checked ) this.#filters.types.add(target.defaultValue);\n else this.#filters.types.delete(target.defaultValue);\n this.#adjustCheckboxStates(target.closest(\".sidebar\"));\n }\n\n else {\n target.indeterminate = false;\n for ( const child of target.closest(\".type-group\").querySelectorAll(\"dnd5e-checkbox[value]\") ) {\n child.checked = target.checked;\n if ( target.checked ) this.#filters.types.add(child.defaultValue);\n else this.#filters.types.delete(child.defaultValue);\n }\n }\n\n this.render({ parts: [\"filters\", \"results\"] });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle toggling the compendium browser mode.\n * @this {CompendiumBrowser}\n * @param {PointerEvent} event The originating click event.\n * @param {HTMLElement} target The element that was clicked.\n */\n static #onToggleMode(event, target) {\n // TODO: Consider persisting this choice in a client setting.\n this._mode = target.checked ? this.constructor.MODES.ADVANCED : this.constructor.MODES.BASIC;\n const tabs = foundry.utils.deepClone(this.constructor.TABS.filter(t => !!t.advanced === target.checked));\n const activeTab = tabs.find(t => t.tab === this.tabGroups.primary) ?? tabs[0];\n const types = target.checked ? [] : (activeTab?.types ?? [\"class\"]);\n this._applyModeFilters(this._mode);\n this._applyTabFilters(activeTab?.tab);\n this.render({ parts: [\"results\", \"filters\", \"types\", \"tabs\"], dnd5e: { browser: { types } }, changedTab: true });\n }\n\n /* -------------------------------------------- */\n /* Database Access */\n /* -------------------------------------------- */\n\n /**\n * Retrieve a listing of documents from all compendiums for a specific Document type, with additional filters\n * optionally applied.\n * @param {typeof Document} documentClass Document type to fetch (e.g. Actor or Item).\n * @param {object} [options={}]\n * @param {Set} [options.types] Individual document subtypes to filter upon (e.g. \"loot\", \"class\", \"npc\").\n * @param {FilterDescription[]} [options.filters] Filters to provide further filters.\n * @param {boolean} [options.index=true] Should only the index for each document be returned, or the whole thing?\n * @param {Set} [options.indexFields] Key paths for fields to index.\n * @param {boolean|string|Function} [options.sort=true] Should the contents be sorted? By default sorting will be\n * performed using document names, but a key path can be provided to sort on\n * a specific property or a function to provide more advanced sorting.\n * @returns {object[]|Document[]}\n */\n static async fetch(documentClass, { types=new Set(), filters=[], index=true, indexFields=new Set(), sort=true }={}) {\n // Nothing within containers should be shown\n filters.push({ k: \"system.container\", o: \"in\", v: [null, undefined] });\n\n // If filters are provided, merge their keys with any other fields needing to be indexed\n if ( filters.length ) indexFields = indexFields.union(Filter.uniqueKeys(filters));\n\n // Do not attempt to index derived fields as this will throw an error server-side.\n indexFields.delete(\"system.source.slug\");\n\n // Collate compendium sources.\n const sources = CompendiumBrowserSettingsConfig.collateSources();\n\n // Iterate over all packs\n let documents = game.packs\n\n // Skip packs that have the wrong document class\n .filter(p => (p.metadata.type === documentClass.metadata.name)\n\n // Do not show entries inside compendia that are not visible to the current user.\n && p.visible\n\n && sources.has(p.collection)\n\n // If types are set and specified in compendium flag, only include those that include the correct types\n && (!types.size || !p.metadata.flags.dnd5e?.types || new Set(p.metadata.flags.dnd5e.types).intersects(types)))\n\n // Generate an index based on the needed fields\n .map(async p => await Promise.all((await p.getIndex({ fields: Array.from(indexFields) })\n\n // Apply module art to the new index\n .then(index => game.dnd5e.moduleArt.apply(index)))\n\n // Derive source values\n .map(i => {\n const source = foundry.utils.getProperty(i, \"system.source\");\n if ( (foundry.utils.getType(source) === \"Object\") && i.uuid ) SourceField.prepareData.call(source, i.uuid);\n return i;\n })\n\n // Remove any documents that don't match the specified types or the provided filters\n .filter(i =>\n (!types.size || (types.has(i.type)\n && (!p.metadata.flags.dnd5e?.types || p.metadata.flags.dnd5e.types.includes(i.type))))\n && (!filters.length || Filter.performCheck(i, filters))\n )\n\n // If full documents are required, retrieve those, otherwise stick with the indices\n .map(async i => index ? i : await fromUuid(i.uuid))\n ));\n\n // Wait for everything to finish loading and flatten the arrays\n documents = (await Promise.all(documents)).flat();\n\n if ( sort ) {\n if ( sort === true ) sort = \"name\";\n const sortFunc = foundry.utils.getType(sort) === \"function\" ? sort : (lhs, rhs) =>\n String(foundry.utils.getProperty(lhs, sort))\n .localeCompare(String(foundry.utils.getProperty(rhs, sort)), game.i18n.lang);\n documents.sort(sortFunc);\n }\n\n return documents;\n }\n\n /* -------------------------------------------- */\n /* Factory Methods */\n /* -------------------------------------------- */\n\n /**\n * Factory method used to spawn a compendium browser and wait for the results of a selection.\n * @param {Partial} [options]\n * @param {object} [renderOptions] Options passed to render.\n * @returns {Promise|null>}\n */\n static async select(options={}, renderOptions={}) {\n return new Promise(resolve => {\n const browser = new CompendiumBrowser(options);\n browser.addEventListener(\"close\", () => {\n resolve(browser.selected?.size ? browser.selected : null);\n }, { once: true });\n browser.render({ force: true, ...renderOptions });\n });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Factory method used to spawn a compendium browser and return a single selected item or null if canceled.\n * @param {Partial} [options]\n * @param {object} [renderOptions] Options passed to render.\n * @returns {Promise}\n */\n static async selectOne(options={}, renderOptions={}) {\n const result = await this.select(\n foundry.utils.mergeObject(options, { selection: { min: 1, max: 1 } }, { inplace: false }),\n renderOptions\n );\n return result?.size ? result.first() : null;\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /**\n * Transform filter definition and additional filters values into the final filters to apply.\n * @param {CompendiumBrowserFilterDefinition} definition Filter definition provided by type.\n * @param {CompendiumBrowserFilters} currentFilters Values of currently selected filters.\n * @returns {FilterDescription[]}\n */\n static applyFilters(definition, currentFilters) {\n const filters = [];\n for ( const [key, value] of Object.entries(currentFilters.additional ?? {}) ) {\n const def = definition.get(key);\n if ( !def ) continue;\n if ( foundry.utils.getType(def.createFilter) === \"function\" ) {\n def.createFilter(filters, value, def);\n continue;\n }\n switch ( def.type ) {\n case \"boolean\":\n if ( value ) filters.push({ k: def.config.keyPath, v: value === 1 });\n break;\n case \"range\":\n const min = Number(value.min);\n const max = Number(value.max);\n if ( Number.isFinite(min) ) filters.push({ k: def.config.keyPath, o: \"gte\", v: min });\n if ( Number.isFinite(max) ) filters.push({ k: def.config.keyPath, o: \"lte\", v: max });\n break;\n case \"set\":\n const choices = foundry.utils.getType(def.config.choices) === \"function\"\n ? def.config.choices(currentFilters) : foundry.utils.deepClone(def.config.choices);\n if ( def.config.blank ) choices._blank = \"\";\n const [positive, negative] = Object.entries(value ?? {}).reduce(([positive, negative], [k, v]) => {\n if ( k in choices ) {\n if ( k === \"_blank\" ) k = \"\";\n if ( v === 1 ) positive.push(k);\n else if ( v === -1 ) negative.push(k);\n }\n return [positive, negative];\n }, [[], []]);\n if ( positive.length ) filters.push(\n { k: def.config.keyPath, o: def.config.multiple ? \"hasall\" : \"in\", v: positive }\n );\n if ( negative.length ) filters.push(\n { o: \"NOT\", v: { k: def.config.keyPath, o: def.config.multiple ? \"hasany\" : \"in\", v: negative } }\n );\n break;\n default:\n console.warn(`Filter type ${def.type} not handled.`);\n break;\n }\n }\n return filters;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Inject the compendium browser button into the compendium sidebar.\n * @param {HTMLElement} html HTML of the sidebar being rendered.\n */\n static injectSidebarButton(html) {\n const button = document.createElement(\"button\");\n button.type = \"button\";\n button.classList.add(\"open-compendium-browser\");\n button.innerHTML = `\n \n ${game.i18n.localize(\"DND5E.CompendiumBrowser.Action.Open\")}\n `;\n button.addEventListener(\"click\", event => (new CompendiumBrowser()).render({ force: true }));\n\n let headerActions = html.querySelector(\".header-actions\");\n // FIXME: Workaround for 336 bug. Remove when 337 released.\n if ( !headerActions ) {\n headerActions = document.createElement(\"div\");\n headerActions.className = \"header-actions action-buttons flexrow\";\n html.querySelector(\":scope > header\").insertAdjacentElement(\"afterbegin\", headerActions);\n }\n headerActions.append(button);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Take two filter sets and find only the filters that match between the two.\n * @param {CompendiumBrowserFilterDefinition} first\n * @param {CompendiumBrowserFilterDefinition} [second]\n * @param {CompendiumBrowserFilters} [currentFilters]\n * @returns {CompendiumBrowserFilterDefinition}\n */\n static intersectFilters(first, second, currentFilters) {\n const final = new Map();\n\n // Iterate over all keys in first map\n for ( const [key, firstConfig] of first.entries() ) {\n const secondConfig = second?.get(key);\n if ( secondConfig?.type && (firstConfig.type !== secondConfig.type) ) continue;\n const finalConfig = foundry.utils.deepClone(firstConfig);\n if ( foundry.utils.getType(finalConfig.config?.choices) === \"function\" ) {\n finalConfig.config.choices = finalConfig.config.choices(currentFilters);\n }\n\n switch ( secondConfig?.type ) {\n case \"range\":\n if ( (\"min\" in firstConfig.config) || (\"min\" in secondConfig.config) ) {\n if ( !(\"min\" in firstConfig.config) || !(\"min\" in secondConfig.config) ) continue;\n finalConfig.config.min = Math.max(firstConfig.config.min, secondConfig.config.min);\n }\n if ( (\"max\" in firstConfig.config) || (\"max\" in secondConfig.config) ) {\n if ( !(\"max\" in firstConfig.config) || !(\"max\" in secondConfig.config) ) continue;\n finalConfig.config.max = Math.min(firstConfig.config.max, secondConfig.config.max);\n }\n if ( (\"min\" in finalConfig.config) && (\"max\" in finalConfig.config)\n && (finalConfig.config.min > finalConfig.config.max) ) continue;\n break;\n case \"set\":\n const choices = foundry.utils.getType(secondConfig.config.choices) === \"function\"\n ? secondConfig.config.choices(currentFilters) : secondConfig.config.choices;\n Object.keys(finalConfig.config.choices).forEach(k => {\n if ( !(k in choices) ) delete finalConfig.config.choices[k];\n });\n if ( foundry.utils.isEmpty(finalConfig.config.choices) ) continue;\n break;\n }\n\n final.set(key, finalConfig);\n }\n return final;\n }\n}\n","/**\n * @import { TokenPlacementConfiguration, TokenPlacementData } from \"./types.mjs\";\n */\n\n/**\n * Class responsible for placing one or more tokens onto the scene.\n * @param {TokenPlacementConfiguration} config Configuration information for placement.\n */\nexport default class TokenPlacement {\n constructor(config) {\n this.config = config;\n }\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /**\n * Configuration information for the placements.\n * @type {TokenPlacementConfiguration}\n */\n config;\n\n /* -------------------------------------------- */\n\n /**\n * Index of the token configuration currently being placed in the scene.\n * @param {number}\n */\n #currentPlacement = -1;\n\n /* -------------------------------------------- */\n\n /**\n * Track the bound event handlers so they can be properly canceled later.\n * @type {object}\n */\n #events;\n\n /* -------------------------------------------- */\n\n /**\n * Track the timestamp when the last mouse move event was captured.\n * @type {number}\n */\n #moveTime = 0;\n\n /* -------------------------------------------- */\n\n /**\n * Placements that have been generated.\n * @type {TokenPlacementData[]}\n */\n #placements;\n\n /* -------------------------------------------- */\n\n /**\n * Preview tokens. Should match 1-to-1 with placements.\n * @type {Token[]}\n */\n #previews;\n\n /* -------------------------------------------- */\n\n /**\n * Is the system currently being throttled to the next animation frame?\n * @type {boolean}\n */\n #throttle = false;\n\n /* -------------------------------------------- */\n /* Placement */\n /* -------------------------------------------- */\n\n /**\n * Perform the placement, asking player guidance when necessary.\n * @param {TokenPlacementConfiguration} config\n * @returns {Promise}\n */\n static place(config) {\n const placement = new this(config);\n return placement.place();\n }\n\n /**\n * Perform the placement, asking player guidance when necessary.\n * @returns {Promise}\n */\n async place() {\n this.#createPreviews();\n try {\n const placements = [];\n let total = 0;\n const uniqueTokens = new Map();\n while ( this.#currentPlacement < this.config.tokens.length - 1 ) {\n this.#currentPlacement++;\n const obj = canvas.tokens.preview.addChild(this.#previews[this.#currentPlacement].object);\n await obj.draw();\n obj.eventMode = \"none\";\n const placement = await this.#requestPlacement();\n if ( placement ) {\n const actorId = placement.prototypeToken.parent.id;\n uniqueTokens.set(actorId, (uniqueTokens.get(actorId) ?? -1) + 1);\n placement.index = { total: total++, unique: uniqueTokens.get(actorId) };\n placements.push(placement);\n } else obj.destroy();\n }\n return placements;\n } finally {\n this.#destroyPreviews();\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Create token previews based on the prototype tokens in config.\n */\n #createPreviews() {\n this.#placements = [];\n this.#previews = [];\n for ( const prototypeToken of this.config.tokens ) {\n const tokenData = prototypeToken.toObject();\n tokenData.sight.enabled = false;\n tokenData._id = foundry.utils.randomID();\n if ( tokenData.randomImg ) tokenData.texture.src = prototypeToken.actor.img;\n const cls = getDocumentClass(\"Token\");\n const doc = new cls(tokenData, { parent: canvas.scene });\n doc.object._previewType = \"creation\";\n this.#placements.push({\n prototypeToken, x: 0, y: 0, elevation: this.config.origin?.elevation ?? 0, rotation: tokenData.rotation ?? 0\n });\n this.#previews.push(doc);\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Clear any previews from the scene.\n */\n #destroyPreviews() {\n this.#previews.forEach(p => {\n if ( p.object && !p.object.destroyed ) p.object.destroy();\n });\n }\n\n /* -------------------------------------------- */\n /* Event Handlers */\n /* -------------------------------------------- */\n\n /**\n * Activate listeners for the placement preview.\n * @returns {Promise} A promise that resolves with the final placement if created,\n * or false if the placement was skipped.\n */\n #requestPlacement() {\n return new Promise((resolve, reject) => {\n this.#events = {\n confirm: this.#onConfirmPlacement.bind(this),\n move: this.#onMovePlacement.bind(this),\n resolve,\n reject,\n rotate: this.#onRotatePlacement.bind(this),\n skip: this.#onSkipPlacement.bind(this)\n };\n\n // Activate listeners\n canvas.stage.on(\"mousemove\", this.#events.move);\n canvas.stage.on(\"mousedown\", this.#events.confirm);\n canvas.app.view.oncontextmenu = this.#events.skip;\n canvas.app.view.onwheel = this.#events.rotate;\n });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Shared code for when token placement ends by being confirmed or canceled.\n * @param {Event} event Triggering event that ended the placement.\n */\n async #finishPlacement(event) {\n canvas.stage.off(\"mousemove\", this.#events.move);\n canvas.stage.off(\"mousedown\", this.#events.confirm);\n canvas.app.view.oncontextmenu = null;\n canvas.app.view.onwheel = null;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Move the token preview when the mouse moves.\n * @param {Event} event Triggering mouse event.\n */\n #onMovePlacement(event) {\n event.stopPropagation();\n if ( this.#throttle ) return;\n this.#throttle = true;\n const idx = this.#currentPlacement;\n const preview = this.#previews[idx];\n const clone = preview.object;\n const local = event.data.getLocalPosition(canvas.tokens);\n local.x = local.x - (clone.w / 2);\n local.y = local.y - (clone.h / 2);\n const dest = !event.shiftKey ? clone.getSnappedPosition(local) : local;\n preview.updateSource({x: dest.x, y: dest.y});\n this.#placements[idx].x = preview.x;\n this.#placements[idx].y = preview.y;\n this.#previews[this.#currentPlacement].object.refresh();\n requestAnimationFrame(() => this.#throttle = false);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Rotate the token preview by 3˚ increments when the mouse wheel is rotated.\n * @param {Event} event Triggering mouse event.\n */\n #onRotatePlacement(event) {\n if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window\n event.stopPropagation();\n const delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;\n const snap = event.shiftKey ? delta : 5;\n const preview = this.#previews[this.#currentPlacement];\n this.#placements[this.#currentPlacement].rotation += snap * Math.sign(event.deltaY);\n preview.updateSource({ rotation: this.#placements[this.#currentPlacement].rotation });\n this.#previews[this.#currentPlacement].object.refresh();\n }\n\n /* -------------------------------------------- */\n\n /**\n * Confirm placement when the left mouse button is clicked.\n * @param {Event} event Triggering mouse event.\n */\n async #onConfirmPlacement(event) {\n await this.#finishPlacement(event);\n this.#events.resolve(this.#placements[this.#currentPlacement]);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Skip placement when the right mouse button is clicked.\n * @param {Event} event Triggering mouse event.\n */\n async #onSkipPlacement(event) {\n await this.#finishPlacement(event);\n this.#events.resolve(false);\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /**\n * Adjust the appended number on an unlinked token to account for multiple placements.\n * @param {TokenDocument|object} tokenDocument Document or data object to adjust.\n * @param {TokenPlacementData} placement Placement data associated with this token document.\n */\n static adjustAppendedNumber(tokenDocument, placement) {\n const regex = new RegExp(/\\((\\d+)\\)$/);\n const match = tokenDocument.name?.match(regex);\n if ( !match ) return;\n const name = tokenDocument.name.replace(regex, `(${Number(match[1]) + placement.index.unique})`);\n if ( tokenDocument instanceof TokenDocument ) tokenDocument.updateSource({ name });\n else tokenDocument.name = name;\n }\n}\n","import FormulaField from \"../fields/formula-field.mjs\";\nimport BaseActivityData from \"./base-activity.mjs\";\n\nconst {\n ArrayField, BooleanField, DocumentIdField, DocumentUUIDField, NumberField, SchemaField, SetField, StringField\n} = foundry.data.fields;\n\n/**\n * @import { SummonActivityData, SummonsProfile } from \"./_types.mjs\";\n */\n\n/**\n * Data model for a summon activity.\n * @extends {BaseActivityData}\n * @mixes SummonActivityData\n */\nexport default class BaseSummonActivityData extends BaseActivityData {\n /** @inheritDoc */\n static defineSchema() {\n const schema = {\n ...super.defineSchema(),\n bonuses: new SchemaField({\n ac: new FormulaField(),\n hd: new FormulaField(),\n hp: new FormulaField(),\n attackDamage: new FormulaField(),\n saveDamage: new FormulaField(),\n healing: new FormulaField()\n }),\n creatureSizes: new SetField(new StringField()),\n creatureTypes: new SetField(new StringField()),\n match: new SchemaField({\n ability: new StringField(),\n attacks: new BooleanField(),\n disposition: new BooleanField(),\n proficiency: new BooleanField(),\n saves: new BooleanField()\n }),\n profiles: new ArrayField(new SchemaField({\n _id: new DocumentIdField({ initial: () => foundry.utils.randomID() }),\n count: new FormulaField(),\n cr: new FormulaField({ deterministic: true }),\n level: new SchemaField({\n min: new NumberField({ integer: true, min: 0 }),\n max: new NumberField({ integer: true, min: 0 })\n }),\n name: new StringField(),\n types: new SetField(new StringField()),\n uuid: new DocumentUUIDField()\n })),\n summon: new SchemaField({\n mode: new StringField(),\n prompt: new BooleanField({ initial: true })\n }),\n tempHP: new FormulaField()\n };\n if ( game.release.generation > 13 ) schema.flat = new SchemaField({\n attack: new NumberField({ nullable: true, initial: null }),\n save: new NumberField({ nullable: true, initial: null })\n }, { persisted: false });\n return schema;\n }\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n get ability() {\n return this.match.ability || super.ability || this.item.abilityMod || this.actor?.system.attributes?.spellcasting;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n get actionType() {\n return \"summ\";\n }\n\n /* -------------------------------------------- */\n\n /**\n * Summons that can be performed based on spell/character/class level.\n * @type {SummonsProfile[]}\n */\n get availableProfiles() {\n const level = this.relevantLevel;\n return this.profiles.filter(e => ((e.level.min ?? -Infinity) <= level) && (level <= (e.level.max ?? Infinity)));\n }\n\n /* -------------------------------------------- */\n\n /**\n * Creatures summoned by this activity.\n * @type {Actor5e[]}\n */\n get summonedCreatures() {\n if ( !this.actor ) return [];\n return dnd5e.registry.summons.creatures(this.actor)\n .filter(i => i?.getFlag(\"dnd5e\", \"summon.origin\") === this.uuid);\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static migrateData(source) {\n super.migrateData(source);\n if ( source.summon?.identifier ) {\n foundry.utils.setProperty(source, \"visibility.identifier\", source.summon.identifier);\n delete source.summon.identifier;\n }\n return source;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n static transformTypeData(source, activityData, options) {\n return foundry.utils.mergeObject(activityData, {\n bonuses: source.system.summons?.bonuses ?? {},\n creatureSizes: source.system.summons?.creatureSizes ?? [],\n creatureTypes: source.system.summons?.creatureTypes ?? [],\n match: {\n ...(source.system.summons?.match ?? {}),\n ability: source.system.ability\n },\n profiles: source.system.summons?.profiles ?? [],\n summon: {\n mode: source.system.summons?.mode ?? \"\",\n prompt: source.system.summons?.prompt ?? true\n },\n visibility: {\n identifier: source.system.summons?.classIdentifier ?? \"\"\n }\n });\n }\n\n /* -------------------------------------------- */\n /* Socket Event Handlers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _preCreate(data) {\n super._preCreate(data);\n const { match } = data ?? {};\n if ( match && !(\"disposition\" in match) ) this.updateSource({ match: { disposition: true } });\n }\n}\n","import SummonSheet from \"../../applications/activity/summon-sheet.mjs\";\nimport SummonUsageDialog from \"../../applications/activity/summon-usage-dialog.mjs\";\nimport CompendiumBrowser from \"../../applications/compendium-browser.mjs\";\nimport TokenPlacement from \"../../canvas/token-placement.mjs\";\nimport BaseSummonActivityData from \"../../data/activity/summon-data.mjs\";\nimport { simplifyBonus, staticID } from \"../../utils.mjs\";\nimport ActivityMixin from \"./mixin.mjs\";\n\n/**\n * @import { TokenPlacementData } from \"../../canvas/_types.mjs\";\n * @import { SummonsProfile } from \"../../data/activity/_types.mjs\";\n * @import { SummoningConfiguration, SummonUsageResults, SummonUseConfiguration, TokenUpdateData } from \"./_types.mjs\";\n */\n\n/**\n * Activity for summoning creatures.\n */\nexport default class SummonActivity extends ActivityMixin(BaseSummonActivityData) {\n /* -------------------------------------------- */\n /* Model Configuration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static LOCALIZATION_PREFIXES = [...super.LOCALIZATION_PREFIXES, \"DND5E.SUMMON\"];\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static metadata = Object.freeze(\n foundry.utils.mergeObject(super.metadata, {\n type: \"summon\",\n img: \"systems/dnd5e/icons/svg/activity/summon.svg\",\n title: \"DND5E.SUMMON.Title\",\n hint: \"DND5E.SUMMON.Hint\",\n sheetClass: SummonSheet,\n usage: {\n actions: {\n placeSummons: SummonActivity.#placeSummons\n },\n dialog: SummonUsageDialog\n }\n }, { inplace: false })\n );\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /**\n * Does the user have permissions to summon?\n * @type {boolean}\n */\n get canSummon() {\n return game.user.can(\"TOKEN_CREATE\") && (game.user.isGM || game.settings.get(\"dnd5e\", \"allowSummoning\"));\n }\n\n /* -------------------------------------------- */\n /* Activation */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _prepareUsageConfig(config) {\n config = super._prepareUsageConfig(config);\n const summons = this.availableProfiles;\n if ( config.create !== false ) {\n config.create ??= {};\n config.create.summons ??= this.canSummon && canvas.scene && summons.length && this.summon.prompt;\n }\n config.summons ??= {};\n config.summons.profile ??= summons[0]?._id ?? null;\n config.summons.creatureSize ??= this.creatureSizes.first() ?? null;\n config.summons.creatureType ??= this.creatureTypes.first() ?? null;\n return config;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _finalizeMessageConfig(usageConfig, messageConfig, results) {\n super._finalizeMessageConfig(usageConfig, messageConfig, results);\n delete messageConfig.data.system?.effects;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n _usageChatButtons(message) {\n if ( !this.availableProfiles.length ) return super._usageChatButtons(message);\n return [{\n label: game.i18n.localize(\"DND5E.SUMMON.Action.Summon\"),\n icon: ' ',\n dataset: {\n action: \"placeSummons\"\n }\n }].concat(super._usageChatButtons(message));\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n shouldHideChatButton(button, message) {\n if ( button.dataset.action === \"placeSummons\" ) return !this.canSummon;\n return super.shouldHideChatButton(button, message);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _finalizeUsage(config, results) {\n await super._finalizeUsage(config, results);\n if ( config.create?.summons ) {\n try {\n results.summoned = await this.placeSummons(config.summons);\n } catch(err) {\n results.summoned = [];\n Hooks.onError(\"SummonActivity#use\", err, { log: \"error\", notify: \"error\" });\n }\n }\n }\n\n /* -------------------------------------------- */\n /* Summoning */\n /* -------------------------------------------- */\n\n /**\n * Process for summoning actor to the scene.\n * @param {SummoningConfiguration} options Configuration data for summoning behavior.\n * @returns {Token5e[]|void}\n */\n async placeSummons(options) {\n if ( !this.canSummon || !canvas.scene ) return;\n\n const profile = this.profiles.find(p => p._id === options?.profile);\n if ( !profile ) throw new Error(\n game.i18n.format(\"DND5E.SUMMON.Warning.NoProfile\", { profileId: options.profile, item: this.item.name })\n );\n\n /**\n * A hook event that fires before summoning is performed.\n * @function dnd5e.preSummon\n * @memberof hookEvents\n * @param {SummonActivity} activity The activity that is performing the summoning.\n * @param {SummonsProfile} profile Profile used for summoning.\n * @param {SummoningConfiguration} options Additional summoning options.\n * @returns {boolean} Explicitly return `false` to prevent summoning.\n */\n if ( Hooks.call(\"dnd5e.preSummon\", this, profile, options) === false ) return;\n\n // Fetch the actor that will be summoned\n const summonUuid = this.summon.mode === \"cr\" ? await this.queryActor(profile) : profile.uuid;\n if ( !summonUuid ) return;\n const actor = await dnd5e.documents.Actor5e.fetchExisting(summonUuid, {\n origin: { key: \"flags.dnd5e.summon.origin\", value: this.item?.uuid }\n });\n\n // Verify ownership of actor\n if ( !actor.isOwner ) {\n throw new Error(game.i18n.format(\"DND5E.SUMMON.Warning.NoOwnership\", { actor: actor.name }));\n }\n\n const tokensData = [];\n const sheet = this.actor?.sheet;\n const { windowId } = (sheet?.parent ?? sheet)?.window ?? {};\n const minimize = (game.release.generation < 14 || !windowId) && !sheet?._minimized;\n if ( minimize ) await sheet?.minimize();\n try {\n // Figure out where to place the summons\n const placements = await this.getPlacement(actor.prototypeToken, profile, options);\n\n for ( const placement of placements ) {\n // Prepare changes to actor data, re-calculating per-token for potentially random values\n const tokenUpdateData = {\n actor,\n placement,\n ...(await this.getChanges(actor, profile, options))\n };\n\n /**\n * A hook event that fires before a specific token is summoned. After placement has been determined but before\n * the final token data is constructed.\n * @function dnd5e.preSummonToken\n * @memberof hookEvents\n * @param {SummonActivity} activity The activity that is performing the summoning.\n * @param {SummonsProfile} profile Profile used for summoning.\n * @param {TokenUpdateData} config Configuration for creating a modified token.\n * @param {SummoningConfiguration} options Additional summoning options.\n * @returns {boolean} Explicitly return `false` to prevent this token from being summoned.\n */\n if ( Hooks.call(\"dnd5e.preSummonToken\", this, profile, tokenUpdateData, options) === false ) continue;\n\n // Create a token document and apply updates\n const tokenData = await this.getTokenData(tokenUpdateData);\n\n /**\n * A hook event that fires after token creation data is prepared, but before summoning occurs.\n * @function dnd5e.summonToken\n * @memberof hookEvents\n * @param {SummonActivity} activity The activity that is performing the summoning.\n * @param {SummonsProfile} profile Profile used for summoning.\n * @param {object} tokenData Data for creating a token.\n * @param {SummoningConfiguration} options Additional summoning options.\n */\n Hooks.callAll(\"dnd5e.summonToken\", this, profile, tokenData, options);\n\n tokensData.push(tokenData);\n }\n } finally {\n if ( minimize ) sheet?.maximize();\n }\n\n const createdTokens = await canvas.scene.createEmbeddedDocuments(\"Token\", tokensData, {\n dnd5e: { autoRollNPCHP: \"no\" }\n });\n\n /**\n * A hook event that fires when summoning is complete.\n * @function dnd5e.postSummon\n * @memberof hookEvents\n * @param {SummonActivity} activity The activity that is performing the summoning.\n * @param {SummonsProfile} profile Profile used for summoning.\n * @param {Token5e[]} tokens Tokens that have been created.\n * @param {SummoningConfiguration} options Additional summoning options.\n */\n Hooks.callAll(\"dnd5e.postSummon\", this, profile, createdTokens, options);\n\n return createdTokens;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Request a specific actor to summon from the player.\n * @param {SummonsProfile} profile Profile used for summoning.\n * @returns {Promise} UUID of the concrete actor to summon or `null` if canceled.\n */\n async queryActor(profile) {\n const locked = {\n documentClass: \"Actor\",\n types: new Set([\"npc\"]),\n additional: {\n cr: { max: simplifyBonus(profile.cr, this.getRollData({ deterministic: true })) }\n }\n };\n if ( profile.types.size ) locked.additional.type = Array.from(profile.types).reduce((obj, type) => {\n obj[type] = 1;\n return obj;\n }, {});\n return CompendiumBrowser.selectOne({ filters: { locked } });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare the updates to apply to the summoned actor and its token.\n * @param {Actor5e} actor Actor that will be modified.\n * @param {SummonsProfile} profile Summoning profile used to summon the actor.\n * @param {SummoningConfiguration} options Configuration data for summoning behavior.\n * @returns {Promise<{actorChanges: object, tokenChanges: object}>} Changes that will be applied to the actor,\n * its items, and its token.\n */\n async getChanges(actor, profile, options) {\n const actorUpdates = { effects: [], items: [] };\n const tokenUpdates = {};\n const rollData = { ...this.getRollData(), summon: actor.getRollData() };\n const prof = rollData.attributes?.prof ?? 0;\n\n // Add flags\n actorUpdates[\"flags.dnd5e.summon\"] = {\n level: this.relevantLevel,\n mod: rollData.mod,\n origin: this.item.uuid,\n activity: this.id,\n profile: profile._id\n };\n\n // Match proficiency\n if ( this.match.proficiency ) {\n const proficiencyEffect = new ActiveEffect({\n _id: staticID(\"dnd5eMatchProficiency\"),\n changes: [{\n key: \"system.attributes.prof\",\n mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,\n value: prof\n }],\n disabled: false,\n icon: \"icons/skills/targeting/crosshair-bars-yellow.webp\",\n name: game.i18n.localize(\"DND5E.SUMMON.FIELDS.match.proficiency.label\")\n });\n actorUpdates.effects.push(proficiencyEffect.toObject());\n }\n\n // Match disposition\n if ( this.match.disposition && this.actor ) {\n const { disposition } = this.actor.isToken ? this.actor.token : this.actor.prototypeToken;\n tokenUpdates.disposition = disposition;\n }\n\n // Add bonus to AC\n if ( this.bonuses.ac ) {\n const acBonus = new Roll(this.bonuses.ac, rollData);\n await acBonus.evaluate();\n if ( acBonus.total ) {\n if ( actor.system.attributes.ac.calc === \"flat\" ) {\n actorUpdates[\"system.attributes.ac.flat\"] = (actor.system.attributes.ac.flat ?? 0) + acBonus.total;\n } else {\n actorUpdates.effects.push((new ActiveEffect({\n _id: staticID(\"dnd5eACBonus\"),\n changes: [{\n key: \"system.attributes.ac.bonus\",\n mode: CONST.ACTIVE_EFFECT_MODES.ADD,\n value: acBonus.total\n }],\n disabled: false,\n icon: \"icons/magic/defensive/shield-barrier-blue.webp\",\n name: game.i18n.localize(\"DND5E.SUMMON.FIELDS.bonuses.ac.label\")\n })).toObject());\n }\n }\n }\n\n // Add bonus to HD\n if ( this.bonuses.hd && actor.system.isNPC ) {\n const hdBonus = new Roll(this.bonuses.hd, rollData);\n await hdBonus.evaluate();\n if ( hdBonus.total ) {\n actorUpdates.effects.push((new ActiveEffect({\n _id: staticID(\"dnd5eHDBonus\"),\n changes: [{\n key: \"system.attributes.hd.max\",\n mode: CONST.ACTIVE_EFFECT_MODES.ADD,\n value: hdBonus.total\n }],\n disabled: false,\n icon: \"icons/sundries/gaming/dice-runed-brown.webp\",\n name: game.i18n.localize(\"DND5E.SUMMON.FIELDS.bonuses.hd.label\")\n })).toObject());\n }\n }\n\n // Add bonus to HP\n if ( this.bonuses.hp ) {\n const hpBonus = new Roll(this.bonuses.hp, rollData);\n await hpBonus.evaluate();\n\n // If non-zero hp bonus, apply as needed for this actor.\n // Note: Only unlinked actors will have their current HP set to their new max HP\n if ( hpBonus.total ) {\n\n // Helper function for modifying max HP ('bonuses.overall' or 'max')\n const maxHpEffect = hpField => {\n return (new ActiveEffect({\n _id: staticID(\"dnd5eHPBonus\"),\n changes: [{\n key: `system.attributes.hp.${hpField}`,\n mode: CONST.ACTIVE_EFFECT_MODES.ADD,\n value: hpBonus.total\n }],\n disabled: false,\n icon: \"icons/magic/life/heart-glowing-red.webp\",\n name: game.i18n.localize(\"DND5E.SUMMON.FIELDS.bonuses.hp.label\")\n })).toObject();\n };\n\n if ( !foundry.utils.isEmpty(actor.classes) && !actor._source.system.attributes.hp.max ) {\n // Actor has classes without a hard-coded max -- apply bonuses to 'overall'\n actorUpdates.effects.push(maxHpEffect(\"bonuses.overall\"));\n } else if ( actor.prototypeToken.actorLink ) {\n // Otherwise, linked actors boost HP via 'max' AE\n actorUpdates.effects.push(maxHpEffect(\"max\"));\n } else {\n // Unlinked actors assumed to always be \"fresh\" copies with bonus HP added to both\n // Max HP and Current HP\n actorUpdates[\"system.attributes.hp.max\"] = actor.system.attributes.hp.max + hpBonus.total;\n actorUpdates[\"system.attributes.hp.value\"] = actor.system.attributes.hp.value + hpBonus.total;\n }\n }\n }\n\n // Add temp HP\n if ( this.tempHP ) {\n const tempHP = new Roll(this.tempHP, rollData);\n await tempHP.evaluate();\n actorUpdates[\"system.attributes.hp.temp\"] = tempHP.total;\n }\n\n // Change creature size\n if ( this.creatureSizes.size ) {\n const size = this.creatureSizes.has(options.creatureSize) ? options.creatureSize : this.creatureSizes.first();\n const config = CONFIG.DND5E.actorSizes[size];\n if ( config ) {\n actorUpdates[\"system.traits.size\"] = size;\n tokenUpdates.width = config.token ?? 1;\n tokenUpdates.height = config.token ?? 1;\n }\n }\n\n // Change creature type\n if ( this.creatureTypes.size ) {\n const type = this.creatureTypes.has(options.creatureType) ? options.creatureType : this.creatureTypes.first();\n if ( actor.system.details?.race instanceof Item ) {\n actorUpdates.items.push({ _id: actor.system.details.race.id, \"system.type.value\": type });\n } else {\n actorUpdates[\"system.details.type.value\"] = type;\n }\n }\n\n const attackDamageBonus = CONFIG.Dice.BasicRoll.replaceFormulaData(this.bonuses.attackDamage ?? \"\", rollData);\n const saveDamageBonus = CONFIG.Dice.BasicRoll.replaceFormulaData(this.bonuses.saveDamage ?? \"\", rollData);\n const healingBonus = CONFIG.Dice.BasicRoll.replaceFormulaData(this.bonuses.healing ?? \"\", rollData);\n for ( const item of actor.items ) {\n if ( !item.system.activities?.size ) continue;\n const changes = [];\n\n // Match attacks\n if ( this.match.attacks && item.system.hasAttack ) {\n let attack = this.flat?.attack;\n if ( (attack === undefined) || (attack === null) ) {\n const actionType = item.system.activities.getByType(\"attack\")[0].actionType;\n const typeMapping = { mwak: \"msak\", rwak: \"rsak\" };\n const parts = [\n rollData.abilities?.[this.ability]?.mod,\n prof,\n CONFIG.Dice.BasicRoll.replaceFormulaData(\n rollData.bonuses?.[typeMapping[actionType] ?? actionType]?.attack ?? \"\", rollData\n )\n ].filter(p => p);\n attack = parts.join(\" + \");\n }\n changes.push({\n key: \"activities[attack].attack.bonus\",\n mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,\n value: attack\n }, {\n key: \"activities[attack].attack.flat\",\n mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,\n value: true\n });\n }\n\n // Match saves\n if ( this.match.saves && item.hasSave ) {\n changes.push({\n key: \"activities[save].save.dc.formula\",\n mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,\n value: this.flat?.save ?? rollData.abilities?.[this.ability]?.dc ?? rollData.attributes.spell.dc\n }, {\n key: \"activities[save].save.dc.calculation\",\n mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE,\n value: \"\"\n });\n }\n\n // Damage bonus\n let damageBonus;\n if ( item.hasAttack ) damageBonus = attackDamageBonus;\n else if ( item.hasSave ) damageBonus = saveDamageBonus;\n else if ( item.isHealing ) damageBonus = healingBonus;\n if ( damageBonus && item.system.activities.find(a => a.damage?.parts?.length || a.healing?.formula) ) {\n changes.push({\n key: \"system.damage.bonus\",\n mode: CONST.ACTIVE_EFFECT_MODES.ADD,\n value: damageBonus\n });\n }\n\n if ( changes.length ) {\n const effect = (new ActiveEffect({\n _id: staticID(\"dnd5eItemChanges\"),\n changes,\n disabled: false,\n icon: \"icons/skills/melee/strike-slashes-orange.webp\",\n name: game.i18n.localize(\"DND5E.SUMMON.ItemChanges.Label\"),\n origin: this.uuid,\n type: \"enchantment\"\n })).toObject();\n actorUpdates.items.push({ _id: item.id, effects: [effect, ...item.effects.map(e => e.toObject())] });\n }\n }\n\n // Add applied effects\n actorUpdates.effects.push(...this.applicableEffects.map(e => e.toObject()));\n\n return { actorUpdates, tokenUpdates };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Determine where the summons should be placed on the scene.\n * @param {PrototypeToken} token Token to be placed.\n * @param {SummonsProfile} profile Profile used for summoning.\n * @param {SummoningConfiguration} options Additional summoning options.\n * @returns {Promise}\n */\n async getPlacement(token, profile, options) {\n // Ensure the token matches the final size\n if ( this.creatureSizes.size ) {\n const size = this.creatureSizes.has(options.creatureSize) ? options.creatureSize : this.creatureSizes.first();\n const config = CONFIG.DND5E.actorSizes[size];\n if ( config ) token = token.clone({ width: config.token ?? 1, height: config.token ?? 1 });\n }\n\n const rollData = this.getRollData();\n const count = new Roll(profile.count || \"1\", rollData);\n await count.evaluate();\n return TokenPlacement.place({\n origin: this.getUsageToken(), tokens: Array(parseInt(count.total)).fill(token)\n });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Create token data ready to be summoned.\n * @param {TokenUpdateData} config Configuration for creating a modified token.\n * @returns {object}\n */\n async getTokenData({ actor, placement, tokenUpdates, actorUpdates }) {\n if ( actor.prototypeToken.randomImg && !game.user.can(\"FILES_BROWSE\") ) {\n tokenUpdates.texture ??= {};\n tokenUpdates.texture.src ??= actor.img;\n ui.notifications.warn(\"DND5E.SUMMON.Warning.Wildcard\", { localize: true });\n }\n\n delete placement.prototypeToken;\n const tokenDocument = await actor.getTokenDocument(foundry.utils.mergeObject(placement, tokenUpdates));\n\n // Linked summons require more explicit updates before token creation.\n // Unlinked summons can take actor delta directly.\n if ( tokenDocument.actorLink ) {\n const { effects, items, ...rest } = actorUpdates;\n await tokenDocument.actor.update(rest);\n await tokenDocument.actor.updateEmbeddedDocuments(\"Item\", items);\n\n const { newEffects, oldEffects } = effects.reduce((acc, curr) => {\n const target = tokenDocument.actor.effects.get(curr._id) ? \"oldEffects\" : \"newEffects\";\n acc[target].push(curr);\n return acc;\n }, { newEffects: [], oldEffects: [] });\n\n await tokenDocument.actor.updateEmbeddedDocuments(\"ActiveEffect\", oldEffects);\n await tokenDocument.actor.createEmbeddedDocuments(\"ActiveEffect\", newEffects, { keepId: true });\n } else {\n if ( game.release.generation > 13 ) tokenDocument.updateSource({ delta: actorUpdates });\n else tokenDocument.delta.updateSource(actorUpdates);\n if ( actor.prototypeToken.appendNumber ) TokenPlacement.adjustAppendedNumber(tokenDocument, placement);\n }\n\n return tokenDocument.toObject();\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /**\n * Handle placing a summons from the chat card.\n * @this {SummonActivity}\n * @param {PointerEvent} event Triggering click event.\n * @param {HTMLElement} target The capturing HTML element which defined a [data-action].\n * @param {ChatMessage5e} message Message associated with the activation.\n */\n static async #placeSummons(event, target, message) {\n const config = {\n create: { summons: true },\n summons: {}\n };\n let needsConfiguration = false;\n\n // No profile specified and only one profile on item, use that one\n const profiles = this.availableProfiles;\n if ( profiles.length === 1 ) config.summons.profile = profiles[0]._id;\n else needsConfiguration = true;\n\n // More than one creature size or type requires configuration\n if ( (this.creatureSizes.size > 1) || (this.creatureTypes.size > 1) ) needsConfiguration = true;\n\n if ( needsConfiguration ) {\n try {\n await SummonUsageDialog.create(this, config, {\n button: {\n icon: \"fa-solid fa-spaghetti-monster-flying\",\n label: \"DND5E.SUMMON.Action.Summon\"\n },\n display: {\n all: false,\n create: { summons: true }\n }\n });\n } catch(err) {\n return;\n }\n }\n\n try {\n await this.placeSummons(config.summons);\n } catch(err) {\n Hooks.onError(\"SummonsActivity#placeSummons\", err, { log: \"error\", notify: \"error\" });\n }\n }\n}\n","/**\n * Create a checkbox input for a BooleanField.\n * @param {BooleanField} field The field.\n * @param {FormInputConfig} config The input configuration.\n * @returns {HTMLElement}\n */\nexport function createCheckboxInput(field, config) {\n const input = document.createElement(\"dnd5e-checkbox\");\n input.name = config.name;\n if ( config.value ) input.checked = true;\n foundry.applications.fields.setInputAttributes(input, config);\n if ( \"ariaLabel\" in config ) input.ariaLabel = config.ariaLabel;\n if ( \"classes\" in config ) input.className = config.classes;\n return input;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Create a grid of checkboxes.\n * @param {DataField} field The field.\n * @param {FormInputConfig} config The input configuration.\n * @returns {HTMLCollection}\n */\nexport function createMultiCheckboxInput(field, config) {\n const template = document.createElement(\"template\");\n for ( const option of config.options || [] ) {\n const { label, value, selected } = option;\n const element = document.createElement(\"label\");\n element.classList.add(\"checkbox\");\n element.innerHTML = `\n \n ${label} \n `;\n template.content.append(element);\n }\n return template.content.children;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Create a number input for a NumberField.\n * @param {NumberField} field The field.\n * @param {FormInputConfig} config The input configuration.\n * @returns {HTMLElement|HTMLCollection}\n */\nexport function createNumberInput(field, config) {\n delete config.input;\n const input = field.toInput(config);\n if ( \"ariaLabel\" in config ) input.ariaLabel = config.ariaLabel;\n if ( \"classes\" in config ) input.className = config.classes;\n return input;\n}\n\n/* -------------------------------------------- */\n\n/**\n * Create a text input for a StringField.\n * @param {StringField} field The field.\n * @param {FormInputConfig} config The input configuration.\n * @returns {HTMLElement|HTMLCollection}\n */\nexport function createTextInput(field, config) {\n delete config.input;\n const input = field.toInput(config);\n if ( \"classes\" in config ) input.className = config.classes;\n return input;\n}\n","import { createCheckboxInput } from \"../../applications/fields.mjs\";\nimport FormulaField from \"../fields/formula-field.mjs\";\n\nconst { BooleanField, SetField, StringField } = foundry.data.fields;\n\n/**\n * @import { TransformationSettingData } from \"./_types.mjs\";\n */\n\n/**\n * A data model that represents the previous transformation preset.\n * @extends {foundry.abstract.DataModel}\n * @mixes TransformationSettingData\n */\nexport default class TransformationSetting extends foundry.abstract.DataModel {\n\n /** @override */\n static LOCALIZATION_PREFIXES = [\"DND5E.TRANSFORM.Setting\"];\n\n /* -------------------------------------------- */\n\n /** @override */\n static defineSchema() {\n return {\n effects: new SetField(new StringField(), { initial: () => TransformationSetting.#initial(\"effects\") }),\n keep: new SetField(new StringField(), { initial: () => TransformationSetting.#initial(\"keep\") }),\n merge: new SetField(new StringField(), { initial: () => TransformationSetting.#initial(\"merge\") }),\n minimumAC: new FormulaField({ deterministic: true }),\n other: new SetField(new StringField(), { initial: () => TransformationSetting.#initial(\"other\") }),\n preset: new StringField({ initial: null, nullable: true }),\n spellLists: new SetField(new StringField()),\n tempFormula: new FormulaField({ determinstic: true }),\n transformTokens: new BooleanField({ initial: true })\n };\n }\n\n /* -------------------------------------------- */\n\n /**\n * Categories that define sets of booleans.\n * @type {string[]}\n */\n static BOOLEAN_CATEGORIES = Object.seal([\"keep\", \"merge\", \"effects\", \"other\"]);\n\n /* -------------------------------------------- */\n\n /**\n * Populate the initial value for \"effects\", \"keep\", \"merge\", & \"other\" based on the settings.\n * @param {\"effects\"|\"keep\"|\"merge\"|\"other\"} category\n * @returns {string[]}\n */\n static #initial(category) {\n return Object.entries(CONFIG.DND5E.transformation[category])\n .filter(([, config]) => config.default)\n .map(([key]) => key);\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /**\n * Generate form categories populated with data from this settings object.\n * @param {object} [options={}]\n * @param {Actor5e} [options.host] Actor being transformed. Should only be provided if using the dialog.\n * @param {string} [options.prefix] Prefix before the field name.\n * @returns {{ category: string, title: string, hint: string, settings: object[] }[]}\n */\n createFormCategories({ host, prefix=\"\" }={}) {\n const disabledFields = TransformationSetting.BOOLEAN_CATEGORIES.reduce((fields, cat) => {\n for ( const value of this[cat] ) {\n for ( const disable of CONFIG.DND5E.transformation[cat][value]?.disables ?? [] ) {\n if ( disable.includes(\"*\") ) Object.keys(CONFIG.DND5E.transformation[disable.replace(\".*\", \"\")] ?? {})\n .filter(k => k !== value).forEach(k => fields.add(`${cat}.${k}`));\n else fields.add(disable);\n }\n }\n return fields;\n }, new Set());\n\n const otherSettings = Object.entries(TransformationSetting.schema.fields)\n .map(([name, field]) => name !== \"preset\" && !TransformationSetting.BOOLEAN_CATEGORIES.includes(name)\n ? this.createFormField(name, field, { host, prefix }) : null)\n .filter(_ => _);\n\n return TransformationSetting.BOOLEAN_CATEGORIES.map(cat => ({\n category: cat,\n title: `DND5E.TRANSFORM.Setting.FIELDS.${cat}.label`,\n hint: game.i18n.has(`DND5E.TRANSFORM.Setting.FIELDS.${cat}.hint`)\n ? `DND5E.TRANSFORM.Setting.FIELDS.${cat}.hint` : \"\",\n settings: [\n ...Object.entries(CONFIG.DND5E.transformation[cat]).map(([name, config]) => ({\n field: new BooleanField({ label: config.label, hint: config.hint }),\n disabled: disabledFields.has(`${cat}.${name}`),\n input: createCheckboxInput,\n name: `${prefix}${cat}.${name}`,\n value: disabledFields.has(`${cat}.${name}`) ? undefined : this[cat]?.has(name)\n })),\n ...(cat === \"other\" ? otherSettings : [])\n ]\n }));\n }\n\n /* -------------------------------------------- */\n\n /**\n * Create a field entry for form rendering for a non-boolean field.\n * @param {string} name Name of the field.\n * @param {DataField} field Underlying data field.\n * @param {object} options\n * @param {Actor5e} [options.host] Actor being transformed.\n * @param {string} [options.prefix] Prefix before the field name.\n * @returns {object}\n */\n createFormField(name, field, { prefix=\"\", host }) {\n const descriptor = {\n field,\n name: `${prefix}${name}`,\n input: field instanceof BooleanField ? createCheckboxInput : undefined,\n value: this[name]\n };\n if ( name === \"spellLists\" ) descriptor.options = dnd5e.registry.spellLists.options.filter(o => {\n if ( !host ) return true;\n const [type, identifier] = o.value.split(\":\");\n return host.identifiedItems.get(identifier, type)?.size > 0;\n });\n return descriptor;\n }\n}\n","import TransformationSetting from \"../../data/settings/transformation-setting.mjs\";\nimport { filteredKeys } from \"../../utils.mjs\";\nimport ActivitySheet from \"./activity-sheet.mjs\";\n\n/**\n * Sheet for the summon activity.\n */\nexport default class TransformSheet extends ActivitySheet {\n\n /** @inheritDoc */\n static DEFAULT_OPTIONS = {\n classes: [\"transform-activity\"],\n actions: {\n addProfile: TransformSheet.#addProfile,\n deleteProfile: TransformSheet.#deleteProfile\n }\n };\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static PARTS = {\n ...super.PARTS,\n effect: {\n template: \"systems/dnd5e/templates/activity/transform-effect.hbs\",\n templates: [\n ...super.PARTS.effect.templates,\n \"systems/dnd5e/templates/activity/parts/transform-profiles.hbs\",\n \"systems/dnd5e/templates/activity/parts/transform-settings.hbs\"\n ]\n }\n };\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static CLEAN_ARRAYS = [...super.CLEAN_ARRAYS, \"profiles\"];\n\n /* -------------------------------------------- */\n\n /** @override */\n tabGroups = {\n sheet: \"identity\",\n activation: \"time\",\n effect: \"profiles\"\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareEffectContext(context, options) {\n context = await super._prepareEffectContext(context, options);\n\n const settings = new TransformationSetting({\n ...(context.source.transform.customize ? context.source.settings\n : CONFIG.DND5E.transformation.presets[context.source.transform.preset]?.settings ?? {}),\n preset: context.source.transform.preset\n });\n context.categories = settings.createFormCategories({ prefix: \"settings.\" });\n context.presetOptions = [\n { value: \"\", label: game.i18n.localize(\"DND5E.TRANSFORM.Preset.Default\") },\n { rule: true },\n ...Object.entries(CONFIG.DND5E.transformation.presets)\n .map(([value, { label }]) => ({ value, label }))\n ];\n\n context.creatureSizeOptions = Object.entries(CONFIG.DND5E.actorSizes)\n .map(([value, { label }]) => ({ value, label }));\n context.creatureTypeOptions = Object.entries(CONFIG.DND5E.creatureTypes)\n .map(([value, { label }]) => ({ value, label }));\n context.movementTypeOptions = Object.entries(CONFIG.DND5E.movementTypes)\n .map(([value, { label }]) => ({ value, label }));\n\n context.profileModes = [\n { value: \"\", label: game.i18n.localize(\"DND5E.TRANSFORM.FIELDS.transform.mode.Direct\") },\n { value: \"cr\", label: game.i18n.localize(\"DND5E.TRANSFORM.FIELDS.transform.mode.CR\") }\n ];\n context.profiles = context.source.profiles.map((data, index) => ({\n data, index,\n collapsed: this.expandedSections.get(`profiles.${data._id}`) ? \"\" : \"collapsed\",\n document: data.uuid ? fromUuidSync(data.uuid) : null,\n fields: this.activity.schema.fields.profiles.element.fields,\n prefix: `profiles.${index}.`,\n source: context.source.profiles[index] ?? data\n })).sort((lhs, rhs) => (lhs.name || \"\").localeCompare(rhs.name || \"\", game.i18n.lang));\n\n return context;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _getTabs() {\n const tabs = super._getTabs();\n tabs.effect.label = \"DND5E.TRANSFORM.SECTIONS.Transformation\";\n tabs.effect.icon = \"fa-solid fa-frog\";\n tabs.effect.tabs = this._markTabs({\n profiles: {\n id: \"profiles\", group: \"effect\", icon: \"fa-solid fa-address-card\",\n label: \"DND5E.TRANSFORM.SECTIONS.Profiles\"\n },\n settings: {\n id: \"settings\", group: \"effect\", icon: \"fa-solid fa-sliders\",\n label: \"DND5E.TRANSFORM.SECTIONS.Settings\"\n }\n });\n return tabs;\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _onRender(context, options) {\n await super._onRender(context, options);\n this.element.querySelector(\".activity-profiles\").addEventListener(\"drop\", this.#onDrop.bind(this));\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle adding a new entry to the transform profiles list.\n * @this {TransformSheet}\n * @param {Event} event Triggering click event.\n * @param {HTMLElement} target Button that was clicked.\n */\n static #addProfile(event, target) {\n this.activity.update({ profiles: [...this.activity.toObject().profiles, {}] });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle removing an entry from the transform profiles list.\n * @this {TransformSheet}\n * @param {Event} event Triggering click event.\n * @param {HTMLElement} target Button that was clicked.\n */\n static #deleteProfile(event, target) {\n const profiles = this.activity.toObject().profiles;\n profiles.splice(target.closest(\"[data-index]\").dataset.index, 1);\n this.activity.update({ profiles });\n }\n\n /* -------------------------------------------- */\n /* Form Handling */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _prepareSubmitData(event, formData) {\n const submitData = super._prepareSubmitData(event, formData);\n if ( submitData.settings ) {\n for ( const category of [\"keep\", \"merge\", \"effects\", \"other\"] ) {\n submitData.settings[category] = filteredKeys(submitData.settings[category] ?? {});\n }\n }\n return submitData;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _processSubmitData(event, submitData) {\n // If customize is set but no settings set, save defaults\n if ( submitData.transform?.customize && !this.activity._source.settings ) {\n const preset = submitData.transform.preset ?? this.activity.transform.preset;\n submitData.settings = new TransformationSetting(foundry.utils.mergeObject({\n ...(CONFIG.DND5E.transformation.presets[preset]?.settings ?? {}),\n preset: this.activity.transform.preset\n }, submitData.settings ?? {}, { inplace: false })).toObject();\n }\n\n // If customize is unchecked and settings set, remove settings\n else if ( (submitData.transform?.customize === false) && this.activity.settings ) submitData.settings = null;\n\n await super._processSubmitData(event, submitData);\n }\n\n /* -------------------------------------------- */\n /* Drag & Drop */\n /* -------------------------------------------- */\n\n /**\n * Handle dropping actors onto the sheet.\n * @param {Event} event Triggering drop event.\n */\n async #onDrop(event) {\n // Try to extract the data\n const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event);\n\n // Handle dropping linked items\n if ( data?.type !== \"Actor\" ) return;\n const actor = await Actor.implementation.fromDropData(data);\n\n // If dropped onto existing profile, add or replace link\n const profileId = event.target.closest(\"[data-profile-id]\")?.dataset.profileId;\n if ( profileId ) {\n const profiles = this.activity.toObject().profiles;\n const profile = profiles.find(p => p._id === profileId);\n profile.uuid = actor.uuid;\n this.activity.update({ profiles });\n }\n\n // Otherwise create a new profile\n else this.activity.update({ profiles: [...this.activity.toObject().profiles, { uuid: actor.uuid }] });\n }\n}\n","import { formatCR, simplifyBonus } from \"../../utils.mjs\";\nimport ActivityUsageDialog from \"./activity-usage-dialog.mjs\";\n\nconst { StringField } = foundry.data.fields;\n\n/**\n * @import { ActivityRollData } from \"../../documents/_types.mjs\";\n */\n\n/**\n * Dialog for configuring the usage of the transform activity.\n */\nexport default class TransformUsageDialog extends ActivityUsageDialog {\n\n /** @inheritDoc */\n static PARTS = {\n ...super.PARTS,\n creation: {\n template: \"systems/dnd5e/templates/activity/transform-usage-creation.hbs\"\n }\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareCreationContext(context, options) {\n context = await super._prepareCreationContext(context, options);\n\n const profiles = this.activity.availableProfiles;\n if ( this._shouldDisplay(\"create.transform\") && (profiles.length > 1) ) {\n const rollData = this.activity.getRollData();\n let options = profiles.map(profile => ({\n value: profile._id, label: this.getProfileLabel(profile, rollData)\n }));\n context.hasCreation = true;\n context.transformFields = [{\n field: new StringField({\n required: true, blank: false, label: game.i18n.localize(\"DND5E.TRANSFORM.Profile.Label\")\n }),\n name: \"transform.profile\",\n value: this.config.transform?.profile,\n options\n }];\n } else if ( profiles.length ) {\n context.transformProfile = profiles[0]._id;\n }\n\n return context;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Determine the label for a profile in the ability use dialog.\n * @param {SummonsProfile} profile Profile for which to generate the label.\n * @param {ActivityRollData} rollData Roll data used to prepare the count.\n * @returns {string}\n */\n getProfileLabel(profile, rollData) {\n if ( profile.name ) return profile.name;\n switch ( this.activity.transform.mode ) {\n case \"cr\":\n const cr = simplifyBonus(profile.cr, rollData);\n return game.i18n.format(\"DND5E.TRANSFORM.Profile.ChallengeRatingLabel\", { cr: formatCR(cr) });\n default:\n const doc = fromUuidSync(profile.uuid);\n if ( doc ) return doc.name;\n }\n return \"—\";\n }\n}\n","import FormulaField from \"../fields/formula-field.mjs\";\nimport TransformationSetting from \"../settings/transformation-setting.mjs\";\nimport BaseActivityData from \"./base-activity.mjs\";\n\nconst {\n ArrayField, BooleanField, DocumentIdField, DocumentUUIDField, EmbeddedDataField,\n NumberField, SchemaField, SetField, StringField\n} = foundry.data.fields;\n\n/**\n * @import { TransformActivityData, TransformProfile } from \"./_types.mjs\";\n */\n\n/**\n * Data model for a transform activity.\n * @extends {BaseActivityData}\n * @mixes TransformActivityData\n */\nexport default class BaseTransformActivityData extends BaseActivityData {\n /** @inheritDoc */\n static defineSchema() {\n return {\n ...super.defineSchema(),\n profiles: new ArrayField(new SchemaField({\n _id: new DocumentIdField({ initial: () => foundry.utils.randomID() }),\n cr: new FormulaField({ deterministic: true }),\n level: new SchemaField({\n min: new NumberField({ integer: true, min: 0 }),\n max: new NumberField({ integer: true, min: 0 })\n }),\n movement: new SetField(new StringField()),\n name: new StringField(),\n sizes: new SetField(new StringField()),\n types: new SetField(new StringField()),\n uuid: new DocumentUUIDField({ type: \"Actor\" })\n })),\n settings: new EmbeddedDataField(TransformationSetting, { nullable: true, initial: null }),\n transform: new SchemaField({\n customize: new BooleanField(),\n mode: new StringField({ initial: \"cr\" }),\n preset: new StringField()\n })\n };\n }\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /** @override */\n get applicableEffects() {\n return null;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Transform profiles that can be performed based on spell/character/class level.\n * @type {TransformProfile[]}\n */\n get availableProfiles() {\n const level = this.relevantLevel;\n return this.profiles.filter(e => ((e.level.min ?? -Infinity) <= level) && (level <= (e.level.max ?? Infinity)));\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static migrateData(source) {\n super.migrateData(source);\n if ( source.transform?.identifier ) {\n foundry.utils.setProperty(source, \"visibility.identifier\", source.transform.identifier);\n delete source.transform.identifier;\n }\n return source;\n }\n\n /* -------------------------------------------- */\n /* Data Preparation */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareFinalData(rollData) {\n super.prepareFinalData(rollData);\n if ( this.transform.customize && !this.settings ) {\n this.settings = new TransformationSetting({ preset: this.transform.preset });\n }\n else if ( !this.transform.customize ) this.settings = new TransformationSetting({\n ...(CONFIG.DND5E.transformation.presets[this.transform.preset]?.settings ?? {}),\n preset: this.transform.preset\n });\n }\n}\n","import TransformSheet from \"../../applications/activity/transform-sheet.mjs\";\nimport TransformUsageDialog from \"../../applications/activity/transform-usage-dialog.mjs\";\nimport CompendiumBrowser from \"../../applications/compendium-browser.mjs\";\nimport BaseTransformActivityData from \"../../data/activity/transform-data.mjs\";\nimport { getSceneTargets, simplifyBonus } from \"../../utils.mjs\";\nimport ActivityMixin from \"./mixin.mjs\";\n\n/**\n * @import { TransformProfile } from \"../../data/activity/_types.mjs\";\n */\n\n/**\n * Activity for transforming an actor into something else.\n */\nexport default class TransformActivity extends ActivityMixin(BaseTransformActivityData) {\n /* -------------------------------------------- */\n /* Model Configuration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static LOCALIZATION_PREFIXES = [...super.LOCALIZATION_PREFIXES, \"DND5E.TRANSFORM\"];\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static metadata = Object.freeze(\n foundry.utils.mergeObject(super.metadata, {\n type: \"transform\",\n img: \"systems/dnd5e/icons/svg/activity/transform.svg\",\n title: \"DND5E.TRANSFORM.Title\",\n hint: \"DND5E.TRANSFORM.Hint\",\n sheetClass: TransformSheet,\n usage: {\n actions: {\n transformActor: TransformActivity.#transformActor\n },\n dialog: TransformUsageDialog\n }\n }, { inplace: false })\n );\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /**\n * Does the user have permissions to transform?\n * @type {boolean}\n */\n get canTransform() {\n return game.user.can(\"ACTOR_CREATE\") && (game.user.isGM || game.settings.get(\"dnd5e\", \"allowPolymorphing\"));\n }\n\n /* -------------------------------------------- */\n /* Activation */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _prepareUsageConfig(config) {\n config = super._prepareUsageConfig(config);\n config.transform ??= {};\n config.transform.profile ??= this.availableProfiles[0]?._id ?? null;\n return config;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _requiresConfigurationDialog(config) {\n return super._requiresConfigurationDialog(config) || (this.availableProfiles.length > 1);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _finalizeMessageConfig(usageConfig, messageConfig, results) {\n await super._finalizeMessageConfig(usageConfig, messageConfig, results);\n if ( usageConfig.transform?.profile ) {\n foundry.utils.setProperty(messageConfig.data, \"flags.dnd5e.transform.profile\", usageConfig.transform.profile);\n }\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n _usageChatButtons(message) {\n if ( !this.availableProfiles.length ) return super._usageChatButtons(message);\n return [{\n label: game.i18n.localize(\"DND5E.TRANSFORM.Action.Transform\"),\n icon: ' ',\n dataset: {\n action: \"transformActor\"\n }\n }].concat(super._usageChatButtons(message));\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n shouldHideChatButton(button, message) {\n if ( button.dataset.action === \"transformActor\" ) return !this.canTransform;\n return super.shouldHideChatButton(button, message);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _finalizeUsage(config, results) {\n const profile = this.profiles.find(p => p._id === config.transform?.profile);\n if ( profile ) {\n const uuid = !this.transform.mode ? profile.uuid : await this.queryActor(profile);\n if ( uuid ) {\n if ( results.message instanceof ChatMessage ) results.message.setFlag(\"dnd5e\", \"transform.uuid\", uuid);\n else foundry.utils.setProperty(results.message, \"flags.dnd5e.transform.uuid\", uuid);\n }\n }\n await super._finalizeUsage(config, results);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Request a specific actor to transform into from the player.\n * @param {TransformProfile} profile Profile used for transformation.\n * @returns {Promise} UUID of the actor to transform into or `null` if canceled.\n */\n async queryActor(profile) {\n const locked = { documentClass: \"Actor\", types: new Set([\"npc\"]), additional: {} };\n if ( profile.cr !== \"\" ) locked.additional = {\n cr: { max: simplifyBonus(profile.cr, this.getRollData({ deterministic: true })) }\n };\n const makeFilter = (data, key, negative) => locked.additional[key] = Array.from(data).reduce((obj, type) => {\n obj[type] = negative ? -1 : 1;\n return obj;\n }, {});\n if ( profile.sizes.size ) makeFilter(profile.sizes, \"size\");\n if ( profile.types.size ) makeFilter(profile.types, \"type\");\n if ( profile.movement.size ) makeFilter(profile.movement, \"movement\", true);\n return CompendiumBrowser.selectOne({ filters: { locked }});\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /**\n * Handle transforming selected actors from the chat card.\n * @this {TransformActivity}\n * @param {PointerEvent} event Triggering click event.\n * @param {HTMLElement} target The capturing HTML element which defined a [data-action].\n * @param {ChatMessage5e} message Message associated with the activation.\n */\n static async #transformActor(event, target, message) {\n const targets = getSceneTargets();\n if ( !targets.length && game.user.character ) targets.push(game.user.character);\n if ( !targets.length ) {\n ui.notifications.warn(\"DND5E.ActionWarningNoToken\", { localize: true });\n return;\n }\n\n const profileId = message.getFlag(\"dnd5e\", \"transform.profile\");\n const profile = this.profiles.find(p => p._id === profileId) || this.profiles[0];\n const uuid = message.getFlag(\"dnd5e\", \"transform.uuid\") ?? await this.queryActor(profile);\n const source = await fromUuid(uuid);\n if ( !source ) {\n ui.notifications.warn(\"DND5E.TRANSFORM.Warning.SourceActor\", { localize: true });\n return;\n }\n\n for ( const token of targets ) {\n const actor = token instanceof Actor ? token : token.actor;\n await actor.transformInto(source, this.settings);\n // TODO: Create message for transformed actors\n }\n }\n}\n","import ActivitySheet from \"./activity-sheet.mjs\";\n\n/**\n * Sheet for the utility activity.\n */\nexport default class UtilitySheet extends ActivitySheet {\n\n /** @inheritDoc */\n static DEFAULT_OPTIONS = {\n classes: [\"utility-activity\"]\n };\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static PARTS = {\n ...super.PARTS,\n effect: {\n template: \"systems/dnd5e/templates/activity/utility-effect.hbs\",\n templates: super.PARTS.effect.templates\n }\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareIdentityContext(context, options) {\n context = await super._prepareIdentityContext(context, options);\n context.behaviorFields.push({\n field: context.fields.roll.fields.prompt,\n value: context.source.roll.prompt,\n input: context.inputs.createCheckboxInput\n });\n return context;\n }\n}\n","import FormulaField from \"../fields/formula-field.mjs\";\nimport BaseActivityData from \"./base-activity.mjs\";\n\nconst { BooleanField, SchemaField, StringField } = foundry.data.fields;\n\n/**\n * @import { UtilityActivityData } from \"./_types.mjs\";\n */\n\n/**\n * Data model for an utility activity.\n * @extends {BaseActivityData}\n * @mixes UtilityActivityData\n */\nexport default class BaseUtilityActivityData extends BaseActivityData {\n /** @inheritDoc */\n static defineSchema() {\n return {\n ...super.defineSchema(),\n roll: new SchemaField({\n formula: new FormulaField(),\n name: new StringField(),\n prompt: new BooleanField(),\n visible: new BooleanField()\n })\n };\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /** @override */\n static transformTypeData(source, activityData, options) {\n return foundry.utils.mergeObject(activityData, {\n roll: {\n formula: source.system.formula ?? \"\",\n name: \"\",\n prompt: false,\n visible: false\n }\n });\n }\n}\n","import UtilitySheet from \"../../applications/activity/utility-sheet.mjs\";\nimport BaseUtilityActivityData from \"../../data/activity/utility-data.mjs\";\nimport ActivityMixin from \"./mixin.mjs\";\n\n/**\n * @import {\n * BasicRollDialogConfiguration, BasicRollMessageConfiguration, BasicRollProcessConfiguration\n * } from \"../../dice/_types.mjs\";\n */\n\n/**\n * Generic activity for applying effects and rolling an arbitrary die.\n */\nexport default class UtilityActivity extends ActivityMixin(BaseUtilityActivityData) {\n /* -------------------------------------------- */\n /* Model Configuration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static LOCALIZATION_PREFIXES = [...super.LOCALIZATION_PREFIXES, \"DND5E.UTILITY\"];\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n static metadata = Object.freeze(\n foundry.utils.mergeObject(super.metadata, {\n type: \"utility\",\n img: \"systems/dnd5e/icons/svg/activity/utility.svg\",\n title: \"DND5E.UTILITY.Title\",\n hint: \"DND5E.UTILITY.Hint\",\n sheetClass: UtilitySheet,\n usage: {\n actions: {\n rollFormula: UtilityActivity.#rollFormula\n }\n }\n }, { inplace: false })\n );\n\n /* -------------------------------------------- */\n /* Activation */\n /* -------------------------------------------- */\n\n /** @override */\n _usageChatButtons(message) {\n if ( !this.roll.formula ) return super._usageChatButtons(message);\n return [{\n label: this.roll.name || game.i18n.localize(\"DND5E.Roll\"),\n icon: ' ',\n dataset: {\n action: \"rollFormula\",\n visibility: this.roll.visible ? \"all\" : undefined\n }\n }].concat(super._usageChatButtons(message));\n }\n\n /* -------------------------------------------- */\n /* Rolling */\n /* -------------------------------------------- */\n\n /**\n * Roll the formula attached to this utility.\n * @param {BasicRollProcessConfiguration} [config] Configuration information for the roll.\n * @param {BasicRollDialogConfiguration} [dialog] Configuration for the roll dialog.\n * @param {BasicRollMessageConfiguration} [message] Configuration for the roll message.\n * @returns {Promise} The created Roll instances.\n */\n async rollFormula(config={}, dialog={}, message={}) {\n if ( !this.roll.formula ) {\n console.warn(`No formula defined for the activity ${this.name} on ${this.item.name} (${this.uuid}).`);\n return;\n }\n\n const rollConfig = foundry.utils.deepClone(config);\n rollConfig.hookNames = [...(config.hookNames ?? []), \"formula\"];\n rollConfig.rolls = [{ parts: [this.roll.formula], data: this.getRollData() }].concat(config.rolls ?? []);\n rollConfig.subject = this;\n\n const dialogConfig = foundry.utils.mergeObject({\n configure: this.roll.prompt,\n options: {\n window: {\n title: this.item.name,\n subtitle: \"DND5E.RollConfiguration.Title\",\n icon: this.item.img\n }\n }\n }, dialog);\n\n const messageConfig = foundry.utils.mergeObject({\n create: true,\n data: {\n flavor: `${this.item.name} - ${this.roll.label || game.i18n.localize(\"DND5E.OtherFormula\")}`,\n flags: {\n dnd5e: {\n ...this.messageFlags,\n messageType: \"roll\",\n roll: { type: \"generic\" }\n }\n }\n }\n }, message);\n\n const rolls = await CONFIG.Dice.BasicRoll.build(rollConfig, dialogConfig, messageConfig);\n if ( !rolls.length ) return;\n\n /**\n * A hook event that fires after a formula has been rolled for a Utility activity.\n * @function dnd5e.rollFormula\n * @memberof hookEvents\n * @param {BasicRoll[]} rolls The resulting rolls.\n * @param {object} data\n * @param {UtilityActivity} data.subject The Activity that performed the roll.\n */\n Hooks.callAll(\"dnd5e.rollFormula\", rolls, { subject: this });\n Hooks.callAll(\"dnd5e.rollFormulaV2\", rolls, { subject: this });\n\n return rolls;\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /**\n * Handle rolling the formula attached to this utility.\n * @this {UtilityActivity}\n * @param {PointerEvent} event Triggering click event.\n * @param {HTMLElement} target The capturing HTML element which defined a [data-action].\n * @param {ChatMessage5e} message Message associated with the activation.\n */\n static #rollFormula(event, target, message) {\n this.rollFormula({ event });\n }\n}\n","import D20RollConfigurationDialog from \"./d20-configuration-dialog.mjs\";\n\n/**\n * @import { SkillToolRollConfigurationDialogOptions } from \"../../dice/_types.mjs\";\n */\n\n/**\n * Extended roll configuration dialog that allows selecting abilities.\n * @extends D20RollConfigurationDialog\n */\nexport default class SkillToolRollConfigurationDialog extends D20RollConfigurationDialog {\n /** @override */\n static DEFAULT_OPTIONS = {\n chooseAbility: true\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n async _prepareConfigurationContext(context, options) {\n context = await super._prepareConfigurationContext(context, options);\n if ( this.options.chooseAbility ) context.fields.unshift({\n field: new foundry.data.fields.StringField({\n required: true, blank: false, label: game.i18n.localize(\"DND5E.Abilities\")\n }),\n name: \"ability\",\n options: Object.entries(CONFIG.DND5E.abilities).map(([value, { label }]) => ({ value, label })),\n value: this.config.ability\n });\n return context;\n }\n\n /* -------------------------------------------- */\n /* Event Listeners and Handlers */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _onChangeForm(formConfig, event) {\n super._onChangeForm(formConfig, event);\n if ( this.config.skill && (event.target?.name === \"ability\") ) {\n const skillLabel = CONFIG.DND5E.skills[this.config.skill]?.label ?? \"\";\n const ability = event.target.value ?? this.config.ability;\n const abilityLabel = CONFIG.DND5E.abilities[ability]?.label ?? \"\";\n const flavor = game.i18n.format(\"DND5E.SkillPromptTitle\", { skill: skillLabel, ability: abilityLabel });\n foundry.utils.setProperty(this.message, \"data.flavor\", flavor);\n this._updateFrame({ window: { title: flavor } });\n }\n }\n}\n","import Application5e from \"./api/application.mjs\";\n\n/**\n * @import { PropertyAttributionDescription } from \"./_types.mjs\";\n */\n\n/**\n * Interface for viewing what factors went into determining a specific property.\n *\n * @param {Document} object The Document that owns the property being attributed.\n * @param {PropertyAttributionDescription[]} attributions An array of all the attribution data.\n * @param {string} property Dot separated path to the property.\n * @param {object} [options={}] Application rendering options.\n */\nexport default class PropertyAttribution extends Application5e {\n constructor(object, attributions, property, options={}) {\n super(options);\n this.object = object;\n this.attributions = attributions;\n this.property = property;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n static DEFAULT_OPTIONS = {\n classes: [\"property-attribution\"],\n window: {\n frame: false,\n positioned: false\n }\n };\n\n /* -------------------------------------------- */\n\n /** @override */\n static PARTS = {\n attribution: {\n template: \"systems/dnd5e/templates/apps/property-attribution.hbs\"\n }\n };\n\n /* -------------------------------------------- */\n /* Rendering */\n /* -------------------------------------------- */\n\n /**\n * Prepare tooltip contents.\n * @returns {Promise}\n */\n async renderTooltip() {\n await this.render({ force: true });\n return this.element.innerHTML;\n }\n\n /* -------------------------------------------- */\n\n /** @override */\n _insertElement(element) {}\n\n /* -------------------------------------------- */\n\n /** @override */\n async _prepareContext(options) {\n const property = foundry.utils.getProperty(this.object.system, this.property);\n let total;\n if ( Number.isNumeric(property)) total = property;\n else if ( typeof property === \"object\" && Number.isNumeric(property.value) ) total = property.value;\n const sources = foundry.utils.duplicate(this.attributions);\n return {\n caption: game.i18n.localize(this.options.title),\n sources: sources.map(entry => {\n if ( entry.label.startsWith(\"@\") ) entry.label = this.getPropertyLabel(entry.label.slice(1));\n if ( (entry.mode === CONST.ACTIVE_EFFECT_MODES.ADD) && (entry.value < 0) ) {\n entry.negative = true;\n entry.value = entry.value * -1;\n }\n return entry;\n }),\n total: total\n };\n }\n\n /* -------------------------------------------- */\n /* Helpers */\n /* -------------------------------------------- */\n\n /**\n * Produce a human-readable and localized name for the provided property.\n * @param {string} property Dot separated path to the property.\n * @returns {string} Property name for display.\n */\n getPropertyLabel(property) {\n const parts = property.split(\".\");\n if ( parts[0] === \"abilities\" && parts[1] ) {\n return CONFIG.DND5E.abilities[parts[1]]?.label ?? property;\n } else if ( (property === \"attributes.ac.dex\") && CONFIG.DND5E.abilities.dex ) {\n return CONFIG.DND5E.abilities.dex.label;\n } else if ( (parts[0] === \"prof\") || (property === \"attributes.prof\") ) {\n return game.i18n.localize(\"DND5E.Proficiency\");\n }\n return property;\n }\n}\n","const { SetField, StringField } = foundry.data.fields;\n\n/**\n * @import { ActivationsData } from \"./_types.mjs\";\n */\n\n/**\n * A field for storing relative UUIDs to activations on the actor.\n */\nexport default class ActivationsField extends SetField {\n constructor() {\n super(new StringField());\n }\n\n /* -------------------------------------------- */\n\n /**\n * Find any activity relative UUIDs on this actor that can be used during a set of periods.\n * @param {Actor5e} actor\n * @param {string[]} periods\n * @returns {string[]}\n */\n static getActivations(actor, periods) {\n return actor.items\n .map(i => i.system.activities\n ?.filter(a => periods.includes(a.activation?.type) && a.canUse)\n .map(a => a.relativeUUID) ?? [])\n .flat();\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare activations for display on chat card.\n * @this {ActivationsData}\n * @param {Actor5e} actor Actor to which this activations can be used.\n * @returns {Activity[]}\n */\n static processActivations(actor) {\n return Array.from(this)\n .map(uuid => fromUuidSync(uuid, { relative: actor, strict: false }))\n .filter(_ => _)\n .sort((lhs, rhs) => (lhs.item.sort - rhs.item.sort) || (lhs.sort - rhs.sort));\n }\n}\n","import BaseRestDialog from \"../../applications/actor/rest/base-rest-dialog.mjs\";\nimport CreateDocumentDialog from \"../../applications/create-document-dialog.mjs\";\nimport SkillToolRollConfigurationDialog from \"../../applications/dice/skill-tool-configuration-dialog.mjs\";\nimport PropertyAttribution from \"../../applications/property-attribution.mjs\";\nimport TravelField from \"../../data/actor/fields/travel-field.mjs\";\nimport ActivationsField from \"../../data/chat-message/fields/activations-field.mjs\";\nimport { ActorDeltasField } from \"../../data/chat-message/fields/deltas-field.mjs\";\nimport AdvantageModeField from \"../../data/fields/advantage-mode-field.mjs\";\nimport TransformationSetting from \"../../data/settings/transformation-setting.mjs\";\nimport { createRollLabel } from \"../../enrichers.mjs\";\nimport {\n convertTime, defaultUnits, formatLength, formatNumber, formatTime, simplifyBonus, staticID\n} from \"../../utils.mjs\";\nimport ActiveEffect5e from \"../active-effect.mjs\";\nimport Item5e from \"../item.mjs\";\nimport SystemDocumentMixin from \"../mixins/document.mjs\";\nimport Proficiency from \"./proficiency.mjs\";\nimport SelectChoices from \"./select-choices.mjs\";\nimport * as Trait from \"./trait.mjs\";\n\n/**\n * @import { RequestOptions5e } from \"../../_types.mjs\";\n * @import { AttributionDescription } from \"../../applications/_types.mjs\";\n * @import { TravelPace5e } from \"../../data/actor/fields/_types.mjs\";\n * @import { SkillData } from \"../../data/actor/templates/_types.mjs\";\n * @import {\n * AbilityRollProcessConfiguration,\n * BasicRollDialogConfiguration, BasicRollMessageConfiguration,\n * HitDieRollProcessConfiguration, InitiativeRollOptions,\n * SkillToolRollDialogConfiguration, SkillToolRollProcessConfiguration\n * } from \"../../dice/_types.mjs\";\n * @import {\n * ActorRollData, DamageAffectCategory, DamageApplicationOptions, DamageDescription, DamageSummary,\n * RestConfiguration, RestResult, RollDataOptions, SpellcastingDescription\n * } from \"../_types.mjs\";\n */\n\n/**\n * Extend the base Actor class to implement additional system-specific logic.\n */\nexport default class Actor5e extends SystemDocumentMixin(Actor) {\n\n /** @override */\n static DEFAULT_ICON = \"systems/dnd5e/icons/svg/documents/actor.svg\";\n\n /* -------------------------------------------- */\n\n /**\n * Lazily computed store of classes, subclasses, background, and species.\n * @type {Record>}\n */\n _lazy = {};\n\n /* -------------------------------------------- */\n\n /**\n * Cached copy of the preferred artwork.\n * @type {{ src: string, isToken: boolean, isRandom: boolean, isVideo: boolean }|null}\n */\n _preferredArtwork = this._preferredArtwork;\n\n /* -------------------------------------------- */\n\n /**\n * Mapping of item identifiers to the items.\n * @type {IdentifiedItemsMap>}\n */\n identifiedItems = this.identifiedItems;\n\n /* -------------------------------------------- */\n\n /**\n * Mapping of item compendium source UUIDs to the items.\n * @type {SourcedItemsMap>}\n */\n sourcedItems = this.sourcedItems;\n\n /* -------------------------------------------- */\n\n /**\n * Types that can be selected within the compendium browser.\n * @param {object} [options={}]\n * @param {Set} [options.chosen] Types that have been selected.\n * @returns {SelectChoices}\n */\n static compendiumBrowserTypes({ chosen=new Set() }={}) {\n return new SelectChoices(Actor.TYPES.filter(t => t !== CONST.BASE_DOCUMENT_TYPE).reduce((obj, type) => {\n obj[type] = {\n label: CONFIG.Actor.typeLabels[type],\n chosen: chosen.has(type)\n };\n return obj;\n }, {}));\n }\n\n /* -------------------------------------------- */\n /* Properties */\n /* -------------------------------------------- */\n\n /**\n * A mapping of classes belonging to this Actor.\n * @type {Record}\n */\n get classes() {\n if ( this._lazy?.classes !== undefined ) return this._lazy.classes;\n return this._lazy.classes = Object.fromEntries(this.itemTypes.class.map(cls => [cls.identifier, cls]));\n }\n\n /* -------------------------------------------- */\n\n /**\n * Calculate the bonus from any cover the actor is affected by.\n * @type {number} The cover bonus to AC and dexterity saving throws.\n */\n get coverBonus() {\n const { coverHalf, coverThreeQuarters } = CONFIG.DND5E.statusEffects;\n if ( this.statuses.has(\"coverThreeQuarters\") ) return coverThreeQuarters?.coverBonus;\n else if ( this.statuses.has(\"coverHalf\") ) return coverHalf?.coverBonus;\n return 0;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Get all classes which have spellcasting ability.\n * @type {Record}\n */\n get spellcastingClasses() {\n if ( this._lazy.spellcastingClasses !== undefined ) return this._lazy.spellcastingClasses;\n return this._lazy.spellcastingClasses = Object.entries(this.classes).reduce((obj, [identifier, cls]) => {\n if ( cls.spellcasting && (cls.spellcasting.progression !== \"none\") ) obj[identifier] = cls;\n return obj;\n }, {});\n }\n\n /* -------------------------------------------- */\n\n /**\n * A mapping of subclasses belonging to this Actor.\n * @type {Record}\n */\n get subclasses() {\n if ( this._lazy?.subclasses !== undefined ) return this._lazy.subclasses;\n return this._lazy.subclasses = Object.fromEntries(this.itemTypes.subclass.map(i => [i.identifier, i]));\n }\n\n /* -------------------------------------------- */\n\n /**\n * Is this Actor currently polymorphed into some other creature?\n * @type {boolean}\n */\n get isPolymorphed() {\n return this.getFlag(\"dnd5e\", \"isPolymorphed\") || false;\n }\n\n /* -------------------------------------------- */\n\n /**\n * The Actor's currently equipped armor, if any.\n * @type {Item5e|null}\n */\n get armor() {\n return this.system.attributes?.ac?.equippedArmor ?? null;\n }\n\n /* -------------------------------------------- */\n\n /**\n * The Actor's currently equipped shield, if any.\n * @type {Item5e|null}\n */\n get shield() {\n return this.system.attributes?.ac?.equippedShield ?? null;\n }\n\n /* -------------------------------------------- */\n\n /**\n * The items this actor is concentrating on, and the relevant effects.\n * @type {{items: Set, effects: Set}}\n */\n get concentration() {\n const concentration = {\n items: new Set(),\n effects: new Set()\n };\n\n const limit = this.system.attributes?.concentration?.limit ?? 0;\n if ( !limit ) return concentration;\n\n for ( const effect of this.effects ) {\n if ( !effect.statuses.has(CONFIG.specialStatusEffects.CONCENTRATING) ) continue;\n const data = effect.getFlag(\"dnd5e\", \"item\");\n concentration.effects.add(effect);\n if ( data ) {\n let item = this.items.get(data.id);\n if ( !item && (foundry.utils.getType(data.data) === \"Object\") ) {\n item = new Item.implementation(data.data, { keepId: true, parent: this });\n }\n if ( item ) concentration.items.add(item);\n }\n }\n return concentration;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Creatures summoned by this actor.\n * @type {Actor5e[]}\n */\n get summonedCreatures() {\n return dnd5e.registry.summons.creatures(this);\n }\n\n /* -------------------------------------------- */\n /* Data Migration */\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n _initializeSource(source, options={}) {\n if ( source instanceof foundry.abstract.DataModel ) source = source.toObject();\n\n /**\n * A hook event that fires before source data is initialized for an Actor in a compendium.\n * @function dnd5e.initializeActorSource\n * @memberof hookEvents\n * @param {Actor5e} actor Actor for which the data is being initialized.\n * @param {object} source Source data being initialized.\n * @param {object} options Additional data initialization options.\n */\n if ( options.pack ) Hooks.callAll(\"dnd5e.initializeActorSource\", this, source, options);\n\n // Migrate encounter groups to their own Actor type.\n if ( (source.type === \"group\") && (source.system?.type?.value === \"encounter\") ) {\n source.type = \"encounter\";\n foundry.utils.setProperty(source, \"flags.dnd5e.persistSourceMigration\", true);\n }\n\n source = super._initializeSource(source, options);\n const pack = game.packs.get(options.pack);\n if ( !source._id || !pack || !game.compendiumArt.enabled ) return source;\n const uuid = pack.getUuid(source._id);\n const art = game.dnd5e.moduleArt.map.get(uuid);\n if ( art?.actor || art?.token ) {\n if ( art.actor ) source.img = art.actor;\n if ( typeof art.token === \"string\" ) source.prototypeToken.texture.src = art.token;\n else if ( art.token ) foundry.utils.mergeObject(source.prototypeToken, art.token);\n Actor5e.applyCompendiumArt(source, pack, art);\n }\n return source;\n }\n\n /* -------------------------------------------- */\n /* Methods */\n /* -------------------------------------------- */\n\n /**\n * Apply package-provided art to a compendium Document.\n * @param {object} source The Document's source data.\n * @param {CompendiumCollection} pack The Document's compendium.\n * @param {CompendiumArtInfo} art The art being applied.\n */\n static applyCompendiumArt(source, pack, art) {\n const biography = source.system.details?.biography;\n if ( art.credit && biography ) {\n if ( typeof biography.value !== \"string\" ) biography.value = \"\";\n biography.value += `${art.credit}
`;\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Format a type object into a string.\n * @param {object} typeData The type data to convert to a string.\n * @returns {string}\n */\n static formatCreatureType(typeData) {\n if ( typeof typeData === \"string\" ) return typeData; // Backwards compatibility\n let localizedType;\n if ( typeData.value === \"custom\" ) localizedType = typeData.custom;\n else if ( typeData.value in CONFIG.DND5E.creatureTypes ) {\n const code = CONFIG.DND5E.creatureTypes[typeData.value];\n localizedType = game.i18n.localize(typeData.swarm ? code.plural : code.label);\n }\n let type = localizedType;\n if ( typeData.swarm ) {\n type = game.i18n.format(\"DND5E.CreatureSwarmPhrase\", {\n size: game.i18n.localize(CONFIG.DND5E.actorSizes[typeData.swarm].label),\n type: localizedType\n });\n }\n if ( typeData.subtype ) type = `${type} (${typeData.subtype})`;\n return type;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareData() {\n if ( this.system.modelProvider !== dnd5e ) return super.prepareData();\n this._clearCachedValues();\n this._preparationWarnings = [];\n this.labels = {};\n super.prepareData();\n this.items.forEach(item => item.prepareFinalAttributes());\n this._prepareSpellcasting();\n }\n\n /* --------------------------------------------- */\n\n /**\n * Clear cached class collections.\n * @internal\n */\n _clearCachedValues() {\n this._lazy = {};\n this._preferredArtwork = null;\n this.identifiedItems = new IdentifiedItemsMap();\n this.sourcedItems = new SourcedItemsMap();\n }\n\n /* --------------------------------------------- */\n\n /** @inheritDoc */\n prepareEmbeddedDocuments() {\n this._embeddedPreparation = true;\n super.prepareEmbeddedDocuments();\n delete this._embeddedPreparation;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepares data for a specific skill.\n * @param {string} skillId The id of the skill to prepare data for.\n * @param {object} [options] Additional options passed to {@link CreatureTemplate#prepareSkill}.\n * @returns {SkillData}\n * @internal\n */\n _prepareSkill(skillId, options) {\n return this.system.prepareSkill?.(skillId, options) ?? {};\n }\n\n /* --------------------------------------------- */\n\n /** @inheritDoc */\n applyActiveEffects(phase) {\n if ( game.release.generation < 14 ) phase ??= \"initial\";\n if ( (this.system?.prepareEmbeddedData instanceof Function) && (phase === \"initial\") ) {\n this.system.prepareEmbeddedData();\n }\n return super.applyActiveEffects(phase);\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n *allApplicableEffects() {\n for ( const effect of super.allApplicableEffects() ) {\n if ( effect.type === \"enchantment\" ) continue;\n if ( effect.parent?.getFlag(\"dnd5e\", \"riders.effect\")?.includes(effect.id) ) continue;\n yield effect;\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Fetch an Actor by UUID and obtain a version of it in the World. If the Actor is inside a compendium, check if a\n * version has already been imported before importing it again.\n * @param {string} uuid The Actor's UUID.\n * @param {object} [options]\n * @param {object} [options.origin] Optionally check if the Actor has a specific origin. If not supplied, any\n * Actor that matches the criteria will be returned.\n * @param {string} [options.origin.key] The origin property.\n * @param {any} [options.origin.value] The origin value.\n * @returns {Promise}\n * @throws {Error} If the Actor cannot be found, or cannot be imported.\n */\n static async fetchExisting(uuid, options={}) {\n const { origin } = options;\n const actor = await fromUuid(uuid);\n if ( !actor ) throw new Error(game.i18n.format(\"DND5E.ACTOR.Warning.NoActor\", { uuid }));\n\n const { actorLink } = actor.prototypeToken;\n const matchesOrigin = !origin || (foundry.utils.getProperty(actor, origin.key) === origin.value);\n if ( !actor.pack && (!actorLink || matchesOrigin) ) return actor;\n\n // Search world actors to see if any had been previously imported for this purpose.\n // Linked actors must match the origin to be considered.\n const localActor = game.actors.find(a => {\n const matchesOrigin = !origin || (foundry.utils.getProperty(a, origin.key) === origin.value);\n // Has been auto-imported by this process.\n return (a.getFlag(\"dnd5e\", \"isAutoImported\") || a.getFlag(\"dnd5e\", \"summonedCopy\")) // Back-compat\n // User has ownership of existing actor\n && a.isOwner\n // Sourced from the desired actor UUID.\n && ((a._stats?.compendiumSource === uuid) || (a._stats?.duplicateSource === uuid))\n // Unlinked or created from a specific source.\n && (!a.prototypeToken.actorLink || matchesOrigin);\n });\n if ( localActor ) return localActor;\n\n // Check permissions to create actors.\n if ( !game.user.can(\"ACTOR_CREATE\") ) throw new Error(game.i18n.localize(\"DND5E.ACTOR.Warning.CreateActor\"));\n\n // No suitable world actor was found, create one.\n if ( actor.pack ) {\n // Template actor resides only in a compendium, import the actor into the world.\n return game.actors.importFromCompendium(game.packs.get(actor.pack), actor.id, {\n \"flags.dnd5e.isAutoImported\": true\n });\n } else {\n // A linked world actor was found. Create a copy to avoid affecting the original.\n return actor.clone({\n \"flags.dnd5e.isAutoImported\": true,\n \"_stats.compendiumSource\": actor._stats.compendiumSource,\n \"_stats.duplicateSource\": actor.uuid\n }, { save: true });\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Select appropriate artwork to display on sheet & chat cards based on `showTokenPortrait` flag.\n * @returns {Promise<{ src: string, token: boolean, isRandom: boolean, isVideo: boolean }>}\n */\n async getPreferredArtwork() {\n if ( !this._preferredArtwork ) {\n const showTokenPortrait = this.getFlag(\"dnd5e\", \"showTokenPortrait\") === true;\n const token = this.isToken ? this.token : this.prototypeToken;\n const defaultArtwork = Actor.implementation.getDefaultArtwork(this._source)?.img;\n let texture = token?.texture.src;\n if ( showTokenPortrait && token?.randomImg ) {\n const images = await this.getTokenImages();\n texture = images[Math.floor(Math.random() * images.length)];\n }\n const src = (showTokenPortrait ? texture : this.img) ?? defaultArtwork;\n this._preferredArtwork = {\n src,\n isRandom: showTokenPortrait && token?.randomImg,\n isToken: showTokenPortrait,\n isVideo: foundry.helpers.media.VideoHelper.hasVideoExtension(src)\n };\n }\n return this._preferredArtwork;\n }\n\n /* -------------------------------------------- */\n\n /** @inheritDoc */\n prepareDerivedData() {\n const origin = this.getFlag(\"dnd5e\", \"summon.origin\");\n if ( origin && this.token?.id ) {\n const { collection, primaryId } = foundry.utils.parseUuid(origin);\n dnd5e.registry.summons.track(collection?.get?.(primaryId)?.uuid, this.uuid);\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Calculate the DC of a concentration save required for a given amount of damage.\n * @param {number} damage Amount of damage taken.\n * @returns {number} DC of the required concentration save.\n */\n getConcentrationDC(damage) {\n return Math.clamp(\n Math.floor(damage / 2), 10, dnd5e.settings.rulesVersion === \"modern\" ? 30 : Infinity\n );\n }\n\n /* -------------------------------------------- */\n\n /**\n * Return the amount of experience required to gain a certain character level.\n * @param {number} level The desired level.\n * @returns {number} The XP required.\n */\n getLevelExp(level) {\n const levels = CONFIG.DND5E.CHARACTER_EXP_LEVELS;\n return levels[Math.min(level, levels.length - 1)];\n }\n\n /* -------------------------------------------- */\n\n /**\n * Return the amount of experience granted by killing a creature of a certain CR.\n * @param {number|null} cr The creature's challenge rating.\n * @returns {number|null} The amount of experience granted per kill.\n */\n getCRExp(cr) {\n if ( cr === null ) return null;\n if ( cr < 1.0 ) return Math.max(200 * cr, 10);\n return CONFIG.DND5E.CR_EXP_LEVELS[cr] ?? Object.values(CONFIG.DND5E.CR_EXP_LEVELS).pop();\n }\n\n /* -------------------------------------------- */\n\n /**\n * @inheritdoc\n * @param {RollDataOptions} [options]\n * @returns {ActorRollData}\n */\n getRollData({ deterministic=false }={}) {\n let data;\n if ( this.system.getRollData ) data = this.system.getRollData({ deterministic });\n else data = {...super.getRollData()};\n data.flags = {...this.flags};\n data.name = this.name;\n data.statuses = {};\n for ( const status of this.statuses ) {\n data.statuses[status] = status === \"exhaustion\"\n ? this.system.attributes?.exhaustion ?? 1\n : status === \"concentrating\" ? this.concentration.effects.size : 1;\n }\n return data;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Is this actor under the effect of this property from some status or due to its level of exhaustion?\n * @param {string} key A key in `DND5E.conditionEffects`.\n * @returns {boolean} Whether the actor is affected.\n */\n hasConditionEffect(key) {\n const props = CONFIG.DND5E.conditionEffects[key] ?? new Set();\n const level = this.system.attributes?.exhaustion ?? null;\n const imms = this.system.traits?.ci?.value ?? new Set();\n const applyExhaustion = (level !== null) && !imms.has(\"exhaustion\")\n && (dnd5e.settings.rulesVersion === \"legacy\");\n const statuses = this.statuses;\n return props.some(k => {\n const l = Number(k.split(\"-\").pop());\n return (statuses.has(k) && !imms.has(k)) || (applyExhaustion && Number.isInteger(l) && (level >= l));\n });\n }\n\n /* -------------------------------------------- */\n /* Spellcasting Preparation */\n /* -------------------------------------------- */\n\n /**\n * Prepare data related to the spell-casting capabilities of the Actor.\n * Mutates the value of the system.spells object. Must be called after final item preparation.\n * @protected\n */\n _prepareSpellcasting() {\n if ( !this.system.spells ) return;\n\n // Translate the list of classes into spellcasting progression\n const progression = Object.values(CONFIG.DND5E.spellcasting).reduce((acc, model) => {\n if ( model.slots ) acc[model.key] = 0;\n return acc;\n }, {});\n const types = {};\n\n // Grab all classes with spellcasting\n const classes = this.itemTypes.class.filter(cls => {\n const type = cls.spellcasting.type;\n if ( !type ) return false;\n types[type] = (types[type] ?? 0) + 1;\n return true;\n });\n\n for ( const cls of classes ) {\n this.constructor.computeClassProgression(progression, cls, { actor: this, count: types[cls.spellcasting.type] });\n }\n\n if ( this.system.isNPC && (\"spell\" in (this.system.attributes ?? {})) ) {\n const level = Object.values(progression).find(_ => _);\n if ( level ) this.system.attributes.spell.level = level;\n else if ( this.system.attributes.spell.level > 0 ) {\n const methods = this.itemTypes.spell.reduce((m, s) => {\n if ( s.system.level && CONFIG.DND5E.spellcasting[s.system.method]?.slots ) m.add(s.system.method);\n return m;\n }, new Set());\n if ( methods.size ) methods.forEach(k => progression[k] = this.system.attributes.spell.level);\n else progression.spell = this.system.attributes.spell.level;\n }\n }\n\n for ( const [type, model] of Object.entries(CONFIG.DND5E.spellcasting) ) {\n if ( !model.slots ) continue;\n // Assume spellcasting methods without progression are based on character level rather than class level.\n if ( foundry.utils.isEmpty(model.progression) ) model.computeProgression(progression, this);\n this.constructor.prepareSpellcastingSlots(this.system.spells, type, progression, { actor: this });\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Contribute to the actor's spellcasting progression.\n * @param {object} progression Spellcasting progression data. *Will be mutated.*\n * @param {Item5e} cls Class for whom this progression is being computed.\n * @param {object} [config={}]\n * @param {Actor5e} [config.actor] Actor for whom the data is being prepared.\n * @param {SpellcastingDescription} [config.spellcasting] Spellcasting descriptive object.\n * @param {number} [config.count=1] Number of classes with this type of spellcasting.\n */\n static computeClassProgression(progression, cls, { actor, spellcasting, count=1 }={}) {\n const type = cls.spellcasting.type;\n spellcasting ??= cls.spellcasting;\n\n /**\n * A hook event that fires while computing the spellcasting progression for each class on each actor.\n * The actual hook names include the spellcasting type (e.g. `dnd5e.computeLeveledProgression`).\n * @param {object} progression Spellcasting progression data. *Will be mutated.*\n * @param {Actor5e|void} actor Actor for whom the data is being prepared.\n * @param {Item5e} cls Class for whom this progression is being computed.\n * @param {SpellcastingDescription} spellcasting Spellcasting descriptive object.\n * @param {number} count Number of classes with this type of spellcasting.\n * @returns {boolean} Explicitly return false to prevent default progression from being calculated.\n * @function dnd5e.computeSpellcastingProgression\n * @memberof hookEvents\n */\n const allowed = Hooks.call(\n `dnd5e.compute${type.capitalize()}Progression`, progression, actor, cls, spellcasting, count\n );\n const model = CONFIG.DND5E.spellcasting[type];\n if ( (allowed === false) || !model.slots ) return;\n\n // Check for deprecated overrides.\n if ( model.isSingleLevel ) {\n if ( foundry.utils.getDefiningClass(this, \"computePactProgression\") !== Actor5e ) {\n foundry.utils.logCompatibilityWarning(\"Actor5e.computePactProgression is deprecated. Please use \"\n + \"SpellcastingModel#computeProgression instead.\", { since: \"DnD5e 5.1\", until: \"DnD5e 6.0\" });\n this.computePactProgression(progression, actor, cls, spellcasting, count);\n return;\n }\n } else if ( foundry.utils.getDefiningClass(this, \"computeLeveledProgression\") !== Actor5e ) {\n foundry.utils.logCompatibilityWarning(\"Actor5e.computeLeveledProgression is deprecated. Please use \"\n + \"SpellcastingModel#computeProgression instead.\", { since: \"DnD5e 5.1\", until: \"DnD5e 6.0\" });\n this.computeLeveledProgression(progression, actor, cls, spellcasting, count);\n return;\n }\n\n // Otherwise proceed with calculation.\n model.computeProgression(progression, actor, cls, spellcasting, count);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Prepare actor's spell slots using progression data.\n * @param {object} spells The `data.spells` object within actor's data. *Will be mutated.*\n * @param {string} type Type of spellcasting slots being prepared.\n * @param {object} progression Spellcasting progression data.\n * @param {object} [config]\n * @param {Actor5e} [config.actor] Actor for whom the data is being prepared.\n */\n static prepareSpellcastingSlots(spells, type, progression, { actor }={}) {\n /**\n * A hook event that fires to convert the provided spellcasting progression into spell slots.\n * The actual hook names include the spellcasting type (e.g. `dnd5e.prepareLeveledSlots`).\n * @param {object} spells The `data.spells` object within actor's data. *Will be mutated.*\n * @param {Actor5e|void} actor Actor for whom the data is being prepared, if any.\n * @param {object} progression Spellcasting progression data.\n * @returns {boolean} Explicitly return false to prevent default preparation from being performed.\n * @function dnd5e.prepareSpellcastingSlots\n * @memberof hookEvents\n */\n const allowed = Hooks.call(`dnd5e.prepare${type.capitalize()}Slots`, spells, actor, progression);\n if ( allowed === false ) return;\n const model = CONFIG.DND5E.spellcasting[type];\n\n // Check for deprecated overrides.\n if ( model.isSingleLevel ) {\n if ( foundry.utils.getDefiningClass(this, \"preparePactSlots\") !== Actor5e ) {\n foundry.utils.logCompatibilityWarning(\"Actor5e.preparePactSlots is deprecated. Please use \"\n + \"SpellcastingModel#prepareSlots instead.\", { since: \"DnD5e 5.1\", until: \"DnD5e 6.0\" });\n this.preparePactSlots(spells, actor, progression);\n return;\n }\n } else if ( foundry.utils.getDefiningClass(this, \"prepareLeveledSlots\") !== Actor5e ) {\n foundry.utils.logCompatibilityWarning(\"Actor5e.prepareLeveledSlots is deprecated. Please use \"\n + \"SpellcastingModel#prepareSlots instead.\", { since: \"DnD5e 5.1\", until: \"DnD5e 6.0\" });\n this.prepareLeveledSlots(spells, actor, progression);\n return;\n }\n\n // Otherwise proceed with calculation.\n model.prepareSlots(spells, actor, progression);\n }\n\n /* -------------------------------------------- */\n /* Gameplay Mechanics */\n /* -------------------------------------------- */\n\n /** @override */\n async modifyTokenAttribute(attribute, value, isDelta, isBar) {\n if ( attribute === \"attributes.hp\" ) {\n const hp = this.system.attributes.hp;\n const delta = isDelta ? (-1 * value) : (hp.value + hp.temp) - value;\n return this.applyDamage(delta, { isDelta });\n } else if ( attribute.startsWith(\".\") ) {\n const item = fromUuidSync(attribute, { relative: this });\n let newValue = item?.system.uses?.value ?? 0;\n if ( isDelta ) newValue += value;\n else newValue = value;\n return item?.update({ \"system.uses.spent\": item.system.uses.max - newValue });\n }\n return super.modifyTokenAttribute(attribute, value, isDelta, isBar);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Apply a certain amount of damage or healing to the health pool for Actor\n * @param {DamageDescription[]|number} damages Damages to apply.\n * @param {DamageApplicationOptions} [options={}] Damage application options.\n * @returns {Promise} A Promise which resolves once the damage has been applied.\n */\n async applyDamage(damages, options={}) {\n const hp = this.system.attributes.hp;\n const hpSource = this.system._source.attributes.hp;\n if ( !hp ) return this; // Group actors don't have HP at the moment\n\n if ( Number.isNumeric(damages) ) {\n damages = [{ value: damages }];\n options.ignore ??= true;\n }\n\n damages = this.calculateDamage(damages, options);\n if ( !damages ) return this;\n\n const { amount, temp, tempMax } = damages;\n const deltaTemp = amount > 0 ? Math.min(hp.temp, amount) : 0;\n const deltaHP = Math.clamp(amount - deltaTemp, -hp.damage + tempMax, hp.value - tempMax);\n const updates = {\n \"system.attributes.hp.temp\": hp.temp - deltaTemp,\n \"system.attributes.hp.tempmax\": hpSource.tempmax - tempMax,\n \"system.attributes.hp.value\": hp.value - deltaHP\n };\n\n if ( temp > updates[\"system.attributes.hp.temp\"] ) updates[\"system.attributes.hp.temp\"] = temp;\n\n /**\n * A hook event that fires before damage is applied to an actor.\n * @param {Actor5e} actor Actor the damage will be applied to.\n * @param {number} amount Amount of damage that will be applied.\n * @param {object} updates Distinct updates to be performed on the actor.\n * @param {DamageApplicationOptions} options Additional damage application options.\n * @returns {boolean} Explicitly return `false` to prevent damage application.\n * @function dnd5e.preApplyDamage\n * @memberof hookEvents\n */\n if ( Hooks.call(\"dnd5e.preApplyDamage\", this, amount, updates, options) === false ) return this;\n\n // Delegate damage application to a hook\n // TODO: Replace this in the future with a better modifyTokenAttribute function in the core\n if ( Hooks.call(\"modifyTokenAttribute\", {\n attribute: \"attributes.hp\",\n value: amount,\n isDelta: false,\n isBar: true\n }, updates) === false ) return this;\n\n await this.update(updates);\n\n /**\n * A hook event that fires after damage has been applied to an actor.\n * @param {Actor5e} actor Actor that has been damaged.\n * @param {number} amount Amount of damage that has been applied.\n * @param {DamageApplicationOptions} options Additional damage application options.\n * @function dnd5e.applyDamage\n * @memberof hookEvents\n */\n Hooks.callAll(\"dnd5e.applyDamage\", this, amount, options);\n\n return this;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Calculate the damage that will be applied to this actor.\n * @param {DamageDescription[]} damages Damages to calculate.\n * @param {DamageApplicationOptions} [options={}] Damage calculation options.\n * @returns {DamageSummary|false} New damage descriptions with changes applied, or `false` if the\n * calculation was canceled.\n */\n calculateDamage(damages, options={}) {\n damages = foundry.utils.deepClone(damages);\n damages.amount = 0;\n damages.temp = 0;\n damages.tempMax = 0;\n\n /**\n * A hook event that fires before damage amount is calculated for an actor.\n * @param {Actor5e} actor The actor being damaged.\n * @param {DamageDescription[]} damages Damage descriptions.\n * @param {DamageApplicationOptions} options Additional damage application options.\n * @returns {boolean} Explicitly return `false` to prevent damage application.\n * @function dnd5e.preCalculateDamage\n * @memberof hookEvents\n */\n if ( Hooks.call(\"dnd5e.preCalculateDamage\", this, damages, options) === false ) return false;\n\n const multiplier = options.multiplier ?? 1;\n const treatAs = options.originatingMessage?.flags?.dnd5e?.roll?.type\n ? options.originatingMessage.flags.dnd5e.roll.type === \"healing\" ? \"healing\" : \"damage\"\n : options.only ?? \"damage\";\n\n const skipped = type => {\n if ( type === \"maximum\" ) return options.only ? options.only !== treatAs : false;\n if ( options.only === \"damage\" ) return type in CONFIG.DND5E.healingTypes;\n if ( options.only === \"healing\" ) return type in CONFIG.DND5E.damageTypes;\n return false;\n };\n\n const dm = this.system.traits?.dm ?? {};\n const rollData = this.getRollData({ deterministic: true });\n const modifications = Object.entries(dm.amount ?? {}).reduce((obj, [type, formula]) => {\n obj[type] = simplifyBonus(formula, rollData);\n return obj;\n }, {});\n const applyModification = (d, type=d.type) => {\n if ( !modifications[type] || this.#changeIsIgnored(\"modification\", type, { options }) ) return;\n const originalValue = d.value;\n if ( Math.sign(d.value) !== Math.sign(d.value + modifications[type]) ) d.value = 0;\n else d.value += modifications[type];\n (d.active[type === \"ALL\" ? \"all\" : \"type\"] ??= {}).modification = true;\n modifications[type] += originalValue - d.value;\n };\n\n damages.forEach(d => {\n d.active ??= {};\n\n // Skip damage types with immunity\n if ( skipped(d.type) || this.#changeHasEffect(\"immunity\", d, { options }) ) {\n d.value = 0;\n d.active.multiplier = 0;\n return;\n }\n\n // Apply damage modification\n if ( !CONFIG.DND5E.damageTypes[d.type]?.isPhysical || !d.properties?.size\n || !dm.bypasses?.intersection(d.properties).size ) {\n applyModification(d);\n if ( !(d.type in CONFIG.DND5E.healingTypes) ) applyModification(d, \"ALL\");\n }\n\n let damageMultiplier = multiplier;\n let appliedDamage = d.value * multiplier;\n\n // Apply damage resistance\n if ( this.#changeHasEffect(\"resistance\", d, { options }) ) {\n damageMultiplier /= 2;\n appliedDamage = Math.trunc(appliedDamage / 2);\n }\n\n // Apply damage vulnerability\n if ( this.#changeHasEffect(\"vulnerability\", d, { options }) ) {\n damageMultiplier *= 2;\n appliedDamage *= 2;\n }\n\n // Negate healing types\n if ( (options.invertHealing !== false) && ((d.type === \"healing\")\n || ((d.type === \"maximum\") && (treatAs === \"healing\"))) ) {\n damageMultiplier *= -1;\n appliedDamage *= -1;\n }\n\n d.value = appliedDamage;\n d.active.multiplier = (d.active.multiplier ?? 1) * damageMultiplier;\n if ( d.type === \"temphp\" ) damages.temp += d.value;\n else if ( d.type === \"maximum\" ) damages.tempMax += d.value;\n else damages.amount += d.value;\n });\n\n if ( damages.tempMax < 0 ) damages.amount += damages.tempMax;\n damages.amount = Math.trunc(damages.amount);\n\n // Apply damage threshold\n if ( ((damages.amount > 0) && (damages.amount < (this.system.attributes?.hp?.dt ?? -Infinity)))\n && !((options.ignore === true) || options.ignore?.threshold) ) {\n damages.amount = 0;\n damages.forEach(d => {\n d.value = 0;\n d.active.multiplier = 0;\n d.active.threshold = true;\n });\n }\n\n /**\n * A hook event that fires after damage values are calculated for an actor.\n * @param {Actor5e} actor The actor being damaged.\n * @param {DamageSummary} damages Damage descriptions.\n * @param {DamageApplicationOptions} options Additional damage application options.\n * @returns {boolean} Explicitly return `false` to prevent damage application.\n * @function dnd5e.calculateDamage\n * @memberof hookEvents\n */\n if ( Hooks.call(\"dnd5e.calculateDamage\", this, damages, options) === false ) return false;\n\n return damages;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Determine whether a specific type of change to a damage value will have an effect.\n * @param {DamageAffectCategory} category Type of change that should be considered.\n * @param {DamageDescription|string} damage Damage description to consider or a specific type.\n * @param {object} [options={}]\n * @param {DamageApplicationOptions} [options.options={}] Damage application options.\n * @param {boolean} [options.skipDowngrade=false] Should downgrades be skipped?\n * @returns {boolean}\n */\n #changeHasEffect(category, damage, { options={}, skipDowngrade=false }={}) {\n const config = this.system.traits?.[`d${category.slice(0, 1)}`];\n const downgrade = type => options.downgrade === true || options.downgrade?.has?.(type);\n const setActive = type => {\n if ( damage.active ) {\n damage.active[type] ??= {};\n damage.active[type][category] = true;\n }\n return true;\n };\n const type = typeof damage === \"string\" ? damage : damage.type;\n const isHealingType = type in CONFIG.DND5E.healingTypes;\n\n // If category is resistance, check for downgraded immunities\n if ( category === \"resistance\" ) {\n if ( !isHealingType && downgrade(\"ALL\") && this.#changeHasEffect(\"immunity\", \"ALL\", { skipDowngrade: true }) ) {\n return setActive(\"all\");\n }\n if ( downgrade(type) && this.#changeHasEffect(\"immunity\", type, { skipDowngrade: true }) ) {\n return setActive(\"type\");\n }\n }\n\n // If damage type is physical and bypass present in properties, skip further checks\n if ( CONFIG.DND5E.damageTypes[type]?.isPhysical && damage.properties?.size\n && config?.bypasses?.intersection(damage.properties)?.size ) return false;\n\n // If all damage resistance is present and not ignored (healing types are excluded from \"All Damage\")\n if ( !isHealingType\n && !this.#changeIsIgnored(category, \"ALL\", { options, skipDowngrade })\n && config?.value.has(\"ALL\") ) {\n return setActive(\"all\");\n }\n\n // If specific type damage resistance is present and not ignored\n if ( !this.#changeIsIgnored(category, type, { options, skipDowngrade }) && config?.value.has(type) ) {\n return setActive(\"type\");\n }\n\n return false;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Determine whether a specific damage change type should be ignored.\n * @param {DamageAffectCategory} category Type of change that should be considered.\n * @param {string} type Specific damage type to consider.\n * @param {object} [options={}]\n * @param {DamageApplicationOptions} [options.options={}] Damage application options.\n * @param {boolean} [options.skipDowngrade=false] Should downgrades not be taken into account?\n * @returns {boolean}\n */\n #changeIsIgnored(category, type, { options={}, skipDowngrade=false }={}) {\n const downgrade = type => options.downgrade === true || options.downgrade?.has?.(type);\n\n // All categories are ignored\n if ( options.ignore === true ) return true;\n\n // Specific category is ignored, or specific category has this type in its ignore list\n if ( (options.ignore?.[category] === true) || (options.ignore?.[category]?.has?.(type)) ) return true;\n\n // When downgrading, always ignore immunities unless `skipDowngrade` option is set\n if ( (category === \"immunity\") && downgrade(type) && !skipDowngrade ) return true;\n\n // When downgrading, resistances should be decided by whether immunity is applied\n if ( (category === \"resistance\") && downgrade(type) && !this.#changeHasEffect(\"immunity\", type) ) return true;\n\n return false;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Apply a certain amount of temporary hit point, but only if it's more than the actor currently has.\n * @param {number} amount An amount of temporary hit points to set\n * @returns {Promise} A Promise which resolves once the temp HP has been applied\n */\n async applyTempHP(amount=0) {\n amount = parseInt(amount);\n const hp = this.system.attributes.hp;\n\n // Update the actor if the new amount is greater than the current\n const tmp = parseInt(hp.temp) || 0;\n return amount > tmp ? this.update({\"system.attributes.hp.temp\": amount}) : this;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Get a color used to represent the current hit points of an Actor.\n * @param {number} current The current HP value\n * @param {number} max The maximum HP value\n * @returns {Color} The color used to represent the HP percentage\n */\n static getHPColor(current, max) {\n const pct = Math.clamp(current, 0, max) / max;\n return Color.fromRGB([(1-(pct/2)), pct, 0]);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Initiate concentration on an item.\n * @param {Activity} activity The activity on which to being concentration.\n * @param {object} [effectData] Effect data to merge into the created effect.\n * @returns {Promise} A promise that resolves to the created effect.\n */\n async beginConcentrating(activity, effectData={}) {\n effectData = ActiveEffect5e.createConcentrationEffectData(activity, effectData);\n\n /**\n * A hook that is called before a concentration effect is created.\n * @function dnd5e.preBeginConcentrating\n * @memberof hookEvents\n * @param {Actor5e} actor The actor initiating concentration.\n * @param {Item5e} item The item that will be concentrated on.\n * @param {object} effectData Data used to create the ActiveEffect.\n * @param {Activity} activity The activity that triggered the concentration.\n * @returns {boolean} Explicitly return false to prevent the effect from being created.\n */\n if ( Hooks.call(\"dnd5e.preBeginConcentrating\", this, activity.item, effectData, activity) === false ) return;\n\n const effect = await ActiveEffect5e.create(effectData, { parent: this });\n\n /**\n * A hook that is called after a concentration effect is created.\n * @function dnd5e.createConcentrating\n * @memberof hookEvents\n * @param {Actor5e} actor The actor initiating concentration.\n * @param {Item5e} item The item that is being concentrated on.\n * @param {ActiveEffect5e} effect The created ActiveEffect instance.\n * @param {Activity} activity The activity that triggered the concentration.\n */\n Hooks.callAll(\"dnd5e.beginConcentrating\", this, activity.item, effect, activity);\n\n return effect;\n }\n\n /* -------------------------------------------- */\n\n /**\n * End concentration on an item.\n * @param {Item5e|ActiveEffect5e|string} [target] An item or effect to end concentration on, or id of an effect.\n * If not provided, all maintained effects are removed.\n * @returns {Promise} A promise that resolves to the deleted effects.\n */\n async endConcentration(target) {\n let effect;\n const { effects } = this.concentration;\n\n if ( !target ) {\n return effects.reduce(async (acc, effect) => {\n acc = await acc;\n return acc.concat(await this.endConcentration(effect));\n }, []);\n }\n\n if ( foundry.utils.getType(target) === \"string\" ) effect = effects.find(e => e.id === target);\n else if ( target instanceof ActiveEffect5e ) effect = effects.has(target) ? target : null;\n else if ( target instanceof Item5e ) {\n effect = effects.find(e => {\n const data = e.getFlag(\"dnd5e\", \"item\") ?? {};\n return (data.id === target._id) || (data.data?._id === target._id);\n });\n }\n if ( !effect ) return [];\n\n /**\n * A hook that is called before a concentration effect is deleted.\n * @function dnd5e.preEndConcentration\n * @memberof hookEvents\n * @param {Actor5e} actor The actor ending concentration.\n * @param {ActiveEffect5e} effect The ActiveEffect that will be deleted.\n * @returns {boolean} Explicitly return false to prevent the effect from being deleted.\n */\n if ( Hooks.call(\"dnd5e.preEndConcentration\", this, effect) === false) return [];\n\n await effect.delete();\n\n /**\n * A hook that is called after a concentration effect is deleted.\n * @function dnd5e.endConcentration\n * @memberof hookEvents\n * @param {Actor5e} actor The actor ending concentration.\n * @param {ActiveEffect5e} effect The ActiveEffect that was deleted.\n */\n Hooks.callAll(\"dnd5e.endConcentration\", this, effect);\n\n return [effect];\n }\n\n /* -------------------------------------------- */\n\n /**\n * Create a chat message for this actor with a prompt to challenge concentration.\n * @param {object} [options]\n * @param {number} [options.dc] The target value of the saving throw.\n * @param {string} [options.ability] An ability to use instead of the default.\n * @returns {Promise} A promise that resolves to the created chat message.\n */\n async challengeConcentration({ dc=10, ability=null }={}) {\n const isConcentrating = this.concentration.effects.size > 0;\n if ( !isConcentrating ) return null;\n\n const dataset = {\n action: \"concentration\",\n dc: dc\n };\n if ( ability in CONFIG.DND5E.abilities ) dataset.ability = ability;\n\n const config = {\n type: \"concentration\",\n format: \"short\",\n icon: true\n };\n\n return ChatMessage.implementation.create({\n content: await foundry.applications.handlebars.renderTemplate(\n \"systems/dnd5e/templates/chat/roll-request-card.hbs\",\n {\n buttons: [{\n dataset: { ...dataset, type: \"concentration\", visbility: \"all\" },\n buttonLabel: createRollLabel({ ...dataset, ...config }),\n hiddenLabel: createRollLabel({ ...dataset, ...config, hideDC: true })\n }]\n }\n ),\n whisper: game.users.filter(user => this.testUserPermission(user, \"OWNER\")),\n speaker: ChatMessage.implementation.getSpeaker({ actor: this })\n });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Determine whether the provided ability is usable for remarkable athlete.\n * @param {string} ability Ability type to check.\n * @returns {boolean} Whether the actor has the remarkable athlete flag and the ability is physical.\n * @private\n */\n _isRemarkableAthlete(ability) {\n return (dnd5e.settings.rulesVersion === \"legacy\") && this.getFlag(\"dnd5e\", \"remarkableAthlete\")\n && CONFIG.DND5E.characterFlags.remarkableAthlete.abilities.includes(ability);\n }\n\n /* -------------------------------------------- */\n /* Rolling */\n /* -------------------------------------------- */\n\n /**\n * Add the reduction to this roll from exhaustion if using the modern rules.\n * @param {string[]} parts Roll parts.\n * @param {object} data Roll data.\n */\n addRollExhaustion(parts, data) {\n if ( (dnd5e.settings.rulesVersion !== \"modern\") || !this.system.attributes?.exhaustion\n || this.system.traits?.ci?.value?.has(\"exhaustion\") ) return;\n const amount = this.system.attributes.exhaustion * (CONFIG.DND5E.conditionTypes.exhaustion?.reduction?.rolls ?? 0);\n if ( amount ) {\n parts.push(\"@exhaustion\");\n data.exhaustion = -amount;\n }\n }\n\n /* -------------------------------------------- */\n\n /**\n * Handle rolling a skill as part of a requested group check.\n * @param {Actor5e} actor The actor.\n * @param {ChatMessage5e} request The request message.\n * @param {Partial} config Roll configuration.\n * @param {RequestOptions5e} [requestOptions]\n * @returns {Promise}\n */\n static async handleSkillCheckRequest(actor, request, config, { event }={}) {\n const data = {};\n foundry.utils.setProperty(data, \"flags.dnd5e.requestResult\", { actorUuid: actor.uuid, requestId: request.id });\n const [roll] = (await actor.rollSkill({ ...config, event }, {}, { data })) ?? [];\n return roll?.parent ?? null;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Roll an ability check with a skill.\n * @param {Partial} config Configuration information for the roll.\n * @param {Partial} dialog Configuration for the roll dialog.\n * @param {Partial} message Configuration for the roll message.\n * @returns {Promise} A Promise which resolves to the created Roll instance.\n */\n async rollSkill(config={}, dialog={}, message={}) {\n if ( (typeof this.system.rollSkill === \"function\")\n && (await this.system.rollSkill(config, dialog, message) === false) ) return null;\n if ( !this.system.skills ) return null;\n const skillLabel = CONFIG.DND5E.skills[config.skill]?.label ?? \"\";\n const ability = config.ability ?? this.system.skills[config.skill]?.ability ?? CONFIG.DND5E.skills[config.skill]?.ability ?? \"\";\n const abilityLabel = CONFIG.DND5E.abilities[ability]?.label ?? \"\";\n const dialogConfig = foundry.utils.mergeObject({\n options: {\n window: {\n title: game.i18n.format(\"DND5E.SkillPromptTitle\", { skill: skillLabel, ability: abilityLabel }),\n subtitle: this.name\n }\n }\n }, dialog);\n return this.#rollSkillTool(\"skill\", config, dialogConfig, message);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Roll an ability check with a tool.\n * @param {Partial} config Configuration information for the roll.\n * @param {Partial} dialog Configuration for the roll dialog.\n * @param {Partial} message Configuration for the roll message.\n * @returns {Promise} A Promise which resolves to the created Roll instance.\n */\n async rollToolCheck(config={}, dialog={}, message={}) {\n const toolLabel = Trait.keyLabel(config.tool, { trait: \"tool\" }) ?? \"\";\n const dialogConfig = foundry.utils.mergeObject({\n options: {\n window: {\n title: game.i18n.format(\"DND5E.ToolPromptTitle\", { tool: toolLabel }),\n subtitle: this.name\n }\n }\n }, dialog);\n return this.#rollSkillTool(\"tool\", config, dialogConfig, message);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Shared rolling functionality between skill & tool checks.\n * @param {\"skill\"|\"tool\"} type Type of roll.\n * @param {Partial} config Configuration information for the roll.\n * @param {Partial} dialog Configuration for the roll dialog.\n * @param {Partial} message Configuration for the roll message.\n * @returns {Promise} A Promise which resolves to the created Roll instance.\n */\n async #rollSkillTool(type, config={}, dialog={}, message={}) {\n let oldFormat = false;\n const name = type === \"skill\" ? \"Skill\" : \"ToolCheck\";\n\n const skillConfig = CONFIG.DND5E.skills[config.skill];\n const toolConfig = CONFIG.DND5E.tools[config.tool] ?? CONFIG.DND5E.vehicleTypes[config.tool];\n if ( ((type === \"skill\") && !skillConfig) || ((type === \"tool\") && !toolConfig) ) {\n return this.rollAbilityCheck(config, dialog, message);\n }\n\n const relevant = type === \"skill\" ? this.system.skills?.[config.skill] : this.system.tools?.[config.tool];\n const alternate = type === \"skill\" ? this.system.tools?.[config.tool] : this.system.skills?.[config.skill];\n const abilityId = config.ability ?? relevant?.ability ?? (type === \"skill\" ? skillConfig.ability : toolConfig.ability);\n const ability = this.system.abilities?.[abilityId];\n const hostActor = this.isPolymorphed && this.flags?.dnd5e?.transformOptions?.mergeSkills && (type === \"skill\")\n ? game.actors.get(this.flags.dnd5e?.originalActor) : null;\n const buildConfig = this._buildSkillToolConfig.bind(this, type, hostActor);\n const doubleProf = !!relevant?.prof.hasProficiency && !!alternate?.prof.hasProficiency;\n const pace = TravelField.getTravelPaceMode(config.pace, config.skill);\n\n const { advantage, disadvantage } = AdvantageModeField.combineFields(this.system, [\n `abilities.${abilityId}.check.roll.mode`,\n `${type}s.${type === \"skill\" ? config.skill : config.tool}.roll.mode`\n ], {\n advantages: { count: Number(doubleProf) + Number(pace.advantage) },\n disadvantages: { count: Number(pace.disadvantage) }\n });\n\n const rollConfig = foundry.utils.mergeObject({\n advantage, disadvantage,\n ability: relevant?.ability ?? (type === \"skill\" ? skillConfig.ability : toolConfig?.ability),\n halflingLucky: this.getFlag(\"dnd5e\", \"halflingLucky\"),\n reliableTalent: (relevant?.value >= 1) && this.getFlag(\"dnd5e\", \"reliableTalent\")\n }, config);\n rollConfig.hookNames = [...(config.hookNames ?? []), type, \"abilityCheck\", \"d20Test\"];\n rollConfig.rolls = [CONFIG.Dice.D20Roll.mergeConfigs({\n options: {\n maximum: Math.min(relevant?.roll.max ?? Infinity, ability?.check.roll.max ?? Infinity),\n minimum: Math.max(relevant?.roll.min ?? -Infinity, ability?.check.roll.min ?? -Infinity)\n }\n }, config.rolls?.shift())].concat(config.rolls ?? []);\n rollConfig.subject = this;\n\n const dialogConfig = foundry.utils.mergeObject({\n applicationClass: SkillToolRollConfigurationDialog,\n options: {\n buildConfig,\n chooseAbility: true\n }\n }, dialog);\n\n const abilityLabel = CONFIG.DND5E.abilities[abilityId]?.label ?? \"\";\n\n const messageConfig = foundry.utils.mergeObject({\n create: true,\n data: {\n flags: {\n dnd5e: {\n messageType: \"roll\",\n roll: {\n [`${type}Id`]: config[type],\n type\n }\n }\n },\n flavor: type === \"skill\"\n ? game.i18n.format(\"DND5E.SkillPromptTitle\", { skill: skillConfig.label, ability: abilityLabel })\n : game.i18n.format(\"DND5E.ToolPromptTitle\", { tool: Trait.keyLabel(config.tool, { trait: \"tool\" }) ?? \"\" }),\n speaker: ChatMessage.getSpeaker({ actor: this })\n }\n }, message);\n\n const rolls = await CONFIG.Dice.D20Roll.build(rollConfig, dialogConfig, messageConfig);\n if ( !rolls.length ) return null;\n\n /**\n * A hook event that fires after a skill or tool check has been rolled.\n * @function dnd5e.rollSkill\n * @function dnd5e.rollToolCheck\n * @memberof hookEvents\n * @param {D20Roll[]} rolls The resulting rolls.\n * @param {object} data\n * @param {string} data.ability Ability used as defined in `CONFIG.DND5E.abilities`.\n * @param {string} [data.skill] ID of the skill that was rolled as defined in `CONFIG.DND5E.skills`.\n * @param {string} [data.tool] ID of the tool that was rolled as defined in `CONFIG.DND5E.tools`.\n * @param {Actor5e} data.subject Actor for which the roll has been performed.\n */\n const data = { ability: rollConfig.ability, [type]: rollConfig[type], subject: this };\n Hooks.callAll(`dnd5e.roll${name}`, rolls, data);\n Hooks.callAll(`dnd5e.roll${name}V2`, rolls, data);\n\n return oldFormat ? rolls[0] : rolls;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Configure a roll config for each roll performed as part of the skill or tool check process. Will be called once\n * per roll in the process each time an option is changed in the roll configuration interface.\n * @param {\"skill\"|\"tool\"} type Type of roll.\n * @param {Actor5e|null} hostActor The original actor from which this one was transformed.\n * @param {D20RollProcessConfiguration} process Configuration for the entire rolling process.\n * @param {D20RollConfiguration} config Configuration for a specific roll.\n * @param {FormDataExtended} [formData] Any data entered into the rolling prompt.\n * @param {number} index Index of the roll within all rolls being prepared.\n */\n _buildSkillToolConfig(type, hostActor, process, config, formData, index) {\n const relevant = type === \"skill\" ? this.system.skills?.[process.skill] : this.system.tools?.[process.tool];\n const rollData = this.getRollData();\n const abilityId = formData?.get(\"ability\") ?? process.ability;\n const ability = this.system.abilities?.[abilityId];\n const { calculateSkillToolProficiency } = dnd5e.dataModels.actor.CommonTemplate;\n let prof = calculateSkillToolProficiency(this, abilityId, process);\n const originalProf = calculateSkillToolProficiency(hostActor, abilityId, process);\n if ( originalProf?.multiplier > prof.multiplier ) prof = originalProf;\n\n let { parts, data } = CONFIG.Dice.D20Roll.constructParts({\n mod: ability?.mod,\n prof: prof?.hasProficiency ? prof.term : null,\n [`${config[type]}Bonus`]: relevant?.bonuses?.check,\n extraBonus: process.bonus,\n [`${abilityId}CheckBonus`]: ability?.bonuses?.check,\n [`${type}Bonus`]: this.system.bonuses?.abilities?.[type],\n abilityCheckBonus: this.system.bonuses?.abilities?.check\n }, { ...rollData });\n\n // Add exhaustion reduction\n this.addRollExhaustion(parts, data);\n\n config.parts = [...(config.parts ?? []), ...parts];\n config.data = { ...data, ...(config.data ?? {}) };\n config.data.abilityId = abilityId;\n }\n\n /* -------------------------------------------- */\n\n /**\n * Roll a generic ability test or saving throw.\n * Prompt the user for input on which variety of roll they want to do.\n * @param {Partial} config Configuration information for the roll.\n * @param {Partial} dialog Configuration for the roll dialog.\n * @param {Partial} message Configuration for the roll message.\n */\n rollAbility(config={}, dialog={}, message={}) {\n const abilityId = config.ability;\n const label = CONFIG.DND5E.abilities[abilityId]?.label ?? \"\";\n new foundry.applications.api.Dialog({\n window: { title: `${game.i18n.format(\"DND5E.AbilityPromptTitle\", { ability: label })}: ${this.name}` },\n position: { width: 400 },\n content: `${game.i18n.format(\"DND5E.AbilityPromptText\", { ability: label })}
`,\n buttons: [\n {\n action: \"test\",\n label: game.i18n.localize(\"DND5E.ActionAbil\"),\n callback: () => this.rollAbilityCheck(config, dialog, message)\n },\n {\n action: \"save\",\n label: game.i18n.localize(\"DND5E.ActionSave\"),\n callback: () => this.rollSavingThrow(config, dialog, message)\n }\n ]\n }).render({ force: true });\n }\n\n /* -------------------------------------------- */\n\n /**\n * Roll an Ability Check.\n * @param {Partial} config Configuration information for the roll.\n * @param {Partial} dialog Configuration for the roll dialog.\n * @param {Partial} message Configuration for the roll message.\n * @returns {Promise} A Promise which resolves to the created Roll instance.\n */\n async rollAbilityCheck(config={}, dialog={}, message={}) {\n const abilityLabel = CONFIG.DND5E.abilities[config.ability]?.label ?? \"\";\n const dialogConfig = foundry.utils.mergeObject({\n options: {\n window: {\n title: game.i18n.format(\"DND5E.AbilityPromptTitle\", { ability: abilityLabel }),\n subtitle: this.name\n }\n }\n }, dialog);\n return this.#rollD20Test(\"check\", config, dialogConfig, message);\n }\n\n /* -------------------------------------------- */\n\n /**\n * Roll a Saving Throw.\n * @param {Partial} config Configuration information for the roll.\n * @param {Partial