From d0d52405338dbea75fac10eb705f18ec4b2f995b Mon Sep 17 00:00:00 2001 From: Jeff Hitchcock Date: Mon, 5 Aug 2024 11:57:24 -0700 Subject: [PATCH 1/2] [#3914] Add `EnchantActivity` data and sheet Copies over the data structure from `EnchantmentField` and the UI from `EnchantmentConfig`. Introduces a new `enchantment` Active Effect type and added a migration to automatically convert effects with the old flag to the new type. The data that was once stored in the flags on the old enchantment effects has been moved to the `effects` object on the enchant activity, which seems like a more logical place to store this information than on each enchantment effect because it has to do with the application process, not the final enchantment. When activity migrations are performed the old data in flags will be moved to the newly created `EnchantActivity`. **Note:** Does not contain the functionality for enchanting items. --- lang/en.json | 112 ++++++++----- module/applications/activity/_module.mjs | 1 + .../applications/activity/activity-sheet.mjs | 32 +++- .../applications/activity/enchant-sheet.mjs | 104 ++++++++++++ module/applications/activity/summon-sheet.mjs | 3 +- module/applications/components/effects.mjs | 6 +- .../applications/item/enchantment-config.mjs | 149 +----------------- module/applications/item/item-sheet.mjs | 6 +- module/config.mjs | 3 + module/data/activity/_module.mjs | 1 + module/data/activity/enchant-data.mjs | 52 ++++++ module/data/item/fields/enchantment-field.mjs | 4 +- module/documents/active-effect.mjs | 23 ++- module/documents/activity/_module.mjs | 1 + module/documents/activity/enchant.mjs | 50 ++++++ module/documents/actor/actor.mjs | 2 +- module/documents/item.mjs | 2 +- system.json | 3 + templates/activity/enchant-effect.hbs | 5 + templates/activity/parts/activity-effects.hbs | 2 +- .../activity/parts/enchant-enchantments.hbs | 68 ++++++++ .../activity/parts/enchant-restrictions.hbs | 9 ++ templates/activity/parts/summon-profiles.hbs | 2 +- templates/actors/limited-sheet-2.hbs | 1 - templates/apps/enchantment-config.hbs | 95 ----------- templates/items/parts/item-action.hbs | 6 - 26 files changed, 431 insertions(+), 311 deletions(-) create mode 100644 module/applications/activity/enchant-sheet.mjs create mode 100644 module/data/activity/enchant-data.mjs create mode 100644 module/documents/activity/enchant.mjs create mode 100644 templates/activity/enchant-effect.hbs create mode 100644 templates/activity/parts/enchant-enchantments.hbs create mode 100644 templates/activity/parts/enchant-restrictions.hbs delete mode 100644 templates/apps/enchantment-config.hbs diff --git a/lang/en.json b/lang/en.json index 6916e4ba0d..ac3881f89d 100644 --- a/lang/en.json +++ b/lang/en.json @@ -20,6 +20,9 @@ "TYPES.Actor.group": "Group", "TYPES.Actor.groupPl": "Groups", +"TYPES.ActiveEffect.enchantment": "Enchantment", +"TYPES.ActiveEffect.enchantmentPl": "Enchantments", + "ITEM.TypeBackground": "Background", "ITEM.TypeBackgroundPl": "Backgrounds", "ITEM.TypeContainer": "Container", @@ -1098,20 +1101,83 @@ "DND5E.EffectApplyWarningConcentration": "Applying an effect that is being concentrated on by another character requires GM permissions.", "DND5E.EffectApplyWarningOwnership": "Effects cannot be applied to tokens you are not the owner of.", "DND5E.EffectsSearch": "Search effects", -"DND5E.Enchantment": { + +"DND5E.ENCHANT": { + "Title": "Enchant", + "FIELDS": { + "effects": { + "FIELDS": { + "level": { + "label": "Level Limit", + "hint": "Range of levels required to use this enchantment.", + "max": { + "label": "Maximum Level" + }, + "min": { + "label": "Minimum Level" + } + }, + "riders": { + "label": "Attached", + "effect": { + "label": "Additional Effects", + "hint": "These additional effects will be added to the enchanted item when this enchantment is added, and removed when the enchantment is removed." + }, + "item": { + "label": "Additional Items", + "hint": "These additional items will be added to the creature when one of its items is enchanted, and will be removed if the enchantment is ever removed." + } + } + } + }, + "enchant": { + "label": "Enchantment Configuration", + "identifier": { + "label": "Class Identifier", + "hint": "Identifier used to determine whether the character level or a specific class level should be used for enchantment level limits." + } + }, + "restrictions": { + "label": "Restrictions", + "hint": "Restrictions on the type of item to which this enchantment can be applied.", + "allowMagical": { + "label": "Allow Magical", + "hint": "Allow items that are already magical to be enchanted." + }, + "type": { + "label": "Item Type", + "hint": "Type of item to which this enchantment can be applied.", + "Any": "Any Enchantable Type" + } + } + }, + "SECTIONS": { + "Enchanting": "Enchanting", + "Enchantments": "Enchantments", + "Restrictions": "Restrictions" + }, + "Enchantment": { + "Action": { + "Create": "Create Enchantment", + "Delete": "Delete Enchantment" + }, + "Empty": "No associated enchantments, use the button below to create one or select an existing enchantment from the control above." + } +}, + +"DND5E.ENCHANTMENT": { "Action": { "Apply": "Apply Enchantment", - "Configure": "Configure Enchantment", - "Create": "Create Enchantment", - "Delete": "Delete Enchantment", "Disable": "Disable Enchantment", "Edit": "Edit Enchantment", "Enable": "Enable Enchantment", "Remove": "Remove Enchantment" - }, + } +}, + +"DND5E.Enchantment": { "Category": { "Active": "Active Enchantments", - "Empty": "No enchantments have been created, use the button above to create one.", "General": "Enchantments", "Inactive": "Inactive Enchantments" }, @@ -1121,9 +1187,6 @@ "FIELDS": { "enchantment": { "label": "Enchantment Configuration", - "classIdentifier": { - "hint": "Identifier used to determine whether the character level or a specific class level should be used for enchantment level limits." - }, "items": { "max": { "label": "Item Limit", @@ -1133,38 +1196,13 @@ "label": "Replacement Period", "hint": "How frequently the enchantments of this type can be re-bound to different items." } - }, - "restrictions": { - "label": "Restrictions", - "hint": "Restrictions on the type of item to which this enchantment can be applied.", - "allowMagical": { - "label": "Allow Magical", - "hint": "Allow items that are already magical to be enchanted." - }, - "type": { - "label": "Item Type", - "hint": "Type of item to which this enchantment can be applied." - } } } }, "Label": "Enchantment", - "Level": { - "Hint": "Range of levels required to use this enchantment." - }, "Items": { "Entry": "{item} on {actor}" }, - "Riders": { - "Effect": { - "Label": "Additional Effects", - "Hint": "These additional effects will be added to the enchanted item when this enchantment is added, and removed when the enchantment is removed." - }, - "Item": { - "Label": "Additional Items", - "Hint": "These additional items will be added to the creature when one of its items is enchanted, and will be removed if the enchantment is ever removed." - } - }, "Warning": { "ConcentrationEnded": "Cannot apply this enchantment because concentration has ended.", "NoMagicalItems": "Items that are already magical cannot be enchanted.", @@ -2119,11 +2157,11 @@ "level": { "label": "Level Limit", "hint": "Range of levels required to use this profile.", - "min": { - "label": "Minimum Level" - }, "max": { "label": "Maximum Level" + }, + "min": { + "label": "Minimum Level" } }, "name": { diff --git a/module/applications/activity/_module.mjs b/module/applications/activity/_module.mjs index 23a19afcf3..c731d5c8de 100644 --- a/module/applications/activity/_module.mjs +++ b/module/applications/activity/_module.mjs @@ -1,5 +1,6 @@ export {default as ActivitySheet} from "./activity-sheet.mjs"; export {default as AttackSheet} from "./attack-sheet.mjs"; +export {default as EnchantSheet} from "./enchant-sheet.mjs"; export {default as SummonSheet} from "./summon-sheet.mjs"; export {default as UtilitySheet} from "./utility-sheet.mjs"; diff --git a/module/applications/activity/activity-sheet.mjs b/module/applications/activity/activity-sheet.mjs index 19acfdeaa6..0bc76e987a 100644 --- a/module/applications/activity/activity-sheet.mjs +++ b/module/applications/activity/activity-sheet.mjs @@ -116,6 +116,10 @@ export default class ActivitySheet extends Application5e { */ #expandedSections = new Map(); + get expandedSections() { + return this.#expandedSections; + } + /* -------------------------------------------- */ /** @@ -334,12 +338,15 @@ export default class ActivitySheet extends Application5e { if ( context.activity.effects ) { const appliedEffects = new Set(context.activity.effects?.map(e => e._id) ?? []); - context.allEffects = this.item.effects.map(effect => ({ - value: effect.id, label: effect.name, selected: appliedEffects.has(effect.id) - })); + context.allEffects = this.item.effects + .filter(e => e.type !== "enchantment") + .map(effect => ({ + value: effect.id, label: effect.name, selected: appliedEffects.has(effect.id) + })); context.appliedEffects = context.activity.effects.map((data, index) => { const effect = { data, + collapsed: this.expandedSections.get(`effects.${data._id}`) ? "" : "collapsed", effect: data.effect, fields: this.activity.schema.fields.effects.element.fields, prefix: `effects.${index}.`, @@ -541,14 +548,25 @@ export default class ActivitySheet extends Application5e { * @param {HTMLElement} target Button that was clicked. */ static async #addEffect(event, target) { - const effectData = { + const effectData = this._addEffectData(); + const [created] = await this.item.createEmbeddedDocuments("ActiveEffect", [effectData]); + this.activity.update({ effects: [...this.activity.toObject().effects, { _id: created.id }] }); + } + + /* -------------------------------------------- */ + + /** + * The data for a newly created applied effect. + * @returns {object} + * @protected + */ + _addEffectData() { + return { name: this.item.name, img: this.item.img, origin: this.item.uuid, transfer: false }; - const [created] = await this.item.createEmbeddedDocuments("ActiveEffect", [effectData]); - this.activity.update({ effects: [...this.activity.toObject().effects, { _id: created.id }] }); } /* -------------------------------------------- */ @@ -646,7 +664,7 @@ export default class ActivitySheet extends Application5e { target.classList.toggle("collapsed"); this.#expandedSections.set( target.closest("[data-expand-id]")?.dataset.expandId, - !event.currentTarget.classList.contains("collapsed") + !target.classList.contains("collapsed") ); } diff --git a/module/applications/activity/enchant-sheet.mjs b/module/applications/activity/enchant-sheet.mjs new file mode 100644 index 0000000000..94a40db960 --- /dev/null +++ b/module/applications/activity/enchant-sheet.mjs @@ -0,0 +1,104 @@ +import ActivitySheet from "./activity-sheet.mjs"; + +/** + * Sheet for the enchant activity. + */ +export default class EnchantSheet extends ActivitySheet { + + /** @inheritDoc */ + static DEFAULT_OPTIONS = { + classes: ["enchant-activity"] + }; + + /* -------------------------------------------- */ + + /** @inheritDoc */ + static PARTS = { + ...super.PARTS, + effect: { + template: "systems/dnd5e/templates/activity/enchant-effect.hbs", + templates: [ + "systems/dnd5e/templates/activity/parts/enchant-enchantments.hbs", + "systems/dnd5e/templates/activity/parts/enchant-restrictions.hbs" + ] + } + }; + + /* -------------------------------------------- */ + + /** @override */ + tabGroups = { + sheet: "identity", + activation: "time", + effect: "enchantments" + }; + + /* -------------------------------------------- */ + /* Rendering */ + /* -------------------------------------------- */ + + /** @override */ + _prepareAppliedEffectContext(context, effect) { + effect.effectOptions = context.allEffects.map(e => ({ + ...e, selected: effect.data.riders.effect.has(e.value) + })); + return effect; + } + + /* -------------------------------------------- */ + + /** @inheritDoc */ + async _prepareEffectContext(context) { + context = await super._prepareEffectContext(context); + + const appliedEnchantments = new Set(context.activity.effects?.map(e => e._id) ?? []); + context.allEnchantments = this.item.effects + .filter(e => e.type === "enchantment") + .map(effect => ({ + value: effect.id, label: effect.name, selected: appliedEnchantments.has(effect.id) + })); + const enchantableTypes = this.activity.enchantableTypes; + context.typeOptions = [ + { value: "", label: game.i18n.localize("DND5E.ENCHANT.FIELDS.restrictions.type.Any") }, + ...Object.keys(CONFIG.Item.dataModels) + .filter(t => enchantableTypes.has(t)) + .map(value => ({ value, label: game.i18n.localize(CONFIG.Item.typeLabels[value]) })) + ]; + + return context; + } + + /* -------------------------------------------- */ + + /** @inheritDoc */ + _getTabs() { + const tabs = super._getTabs(); + tabs.effect.label = "DND5E.ENCHANT.SECTIONS.Enchanting"; + tabs.effect.icon = "fa-solid fa-wand-sparkles"; + tabs.effect.tabs = this._markTabs({ + enchantments: { + id: "enchantments", group: "effect", icon: "fa-solid fa-star", + label: "DND5E.ENCHANT.SECTIONS.Enchantments" + }, + restrictions: { + id: "restrictions", group: "effect", icon: "fa-solid fa-ban", + label: "DND5E.ENCHANT.SECTIONS.Restrictions" + } + }); + return tabs; + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** @override */ + _addEffectData() { + return { + type: "enchantment", + name: this.item.name, + img: this.item.img, + disabled: true + }; + } +} diff --git a/module/applications/activity/summon-sheet.mjs b/module/applications/activity/summon-sheet.mjs index 3c7e789de3..43402195be 100644 --- a/module/applications/activity/summon-sheet.mjs +++ b/module/applications/activity/summon-sheet.mjs @@ -1,7 +1,7 @@ import ActivitySheet from "./activity-sheet.mjs"; /** - * Default sheet for activities. + * Sheet for the summon activity. */ export default class SummonSheet extends ActivitySheet { @@ -67,6 +67,7 @@ export default class SummonSheet extends ActivitySheet { ]; context.profiles = this.activity.profiles.map((data, index) => ({ data, index, + collapsed: this.expandedSections.get(`profiles.${effect.id}`) ? "" : "collapsed", fields: this.activity.schema.fields.profiles.element.fields, prefix: `profiles.${index}.`, source: context.source.profiles[index] ?? data, diff --git a/module/applications/components/effects.mjs b/module/applications/components/effects.mjs index 26b6ad996c..9655db6eed 100644 --- a/module/applications/components/effects.mjs +++ b/module/applications/components/effects.mjs @@ -129,7 +129,7 @@ export default class EffectsElement extends HTMLElement { if ( e.disabled ) categories.enchantmentInactive.effects.push(e); else categories.enchantmentActive.effects.push(e); } - else if ( e.getFlag("dnd5e", "type") === "enchantment" ) categories.enchantment.effects.push(e); + else if ( e.type === "enchantment" ) categories.enchantment.effects.push(e); else if ( e.isSuppressed ) categories.suppressed.effects.push(e); else if ( e.disabled ) categories.inactive.effects.push(e); else if ( e.isTemporary ) categories.temporary.effects.push(e); @@ -280,12 +280,12 @@ export default class EffectsElement extends HTMLElement { const isActor = this.document instanceof Actor; const isEnchantment = li.dataset.effectType.startsWith("enchantment"); return this.document.createEmbeddedDocuments("ActiveEffect", [{ + type: isEnchantment ? "enchantment" : "base", name: isActor ? game.i18n.localize("DND5E.EffectNew") : this.document.name, icon: isActor ? "icons/svg/aura.svg" : this.document.img, origin: isEnchantment ? undefined : this.document.uuid, "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined, - disabled: ["inactive", "enchantmentInactive"].includes(li.dataset.effectType), - "flags.dnd5e.type": isEnchantment ? "enchantment" : undefined + disabled: ["inactive", "enchantmentInactive"].includes(li.dataset.effectType) }]); } diff --git a/module/applications/item/enchantment-config.mjs b/module/applications/item/enchantment-config.mjs index c0431ed1e3..8dcb863c04 100644 --- a/module/applications/item/enchantment-config.mjs +++ b/module/applications/item/enchantment-config.mjs @@ -1,151 +1,10 @@ -import { EnchantmentData } from "../../data/item/fields/enchantment-field.mjs"; - /** * Application for configuring enchantment information for an item. */ export default class EnchantmentConfig extends DocumentSheet { - - /** @inheritDoc */ - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - classes: ["dnd5e", "enchantment-config"], - dragDrop: [{ dropSelector: "form" }], - template: "systems/dnd5e/templates/apps/enchantment-config.hbs", - width: 500, - height: "auto", - sheetConfig: false, - closeOnSubmit: false, - submitOnChange: true, - submitOnClose: true - }); - } - - /* -------------------------------------------- */ - /* Properties */ - /* -------------------------------------------- */ - - /** - * Expanded states for each enchantment. - * @type {Map} - */ - expandedEnchantments = new Map(); - - /* -------------------------------------------- */ - - /** @inheritDoc */ - get title() { - return `${game.i18n.localize("DND5E.Enchantment.Configuration")}: ${this.document.name}`; - } - - /* -------------------------------------------- */ - /* Rendering */ - /* -------------------------------------------- */ - - /** @inheritDoc */ - async getData(options={}) { - const context = await super.getData(options); - - context.enchantableTypes = EnchantmentData.enchantableTypes.reduce((obj, k) => { - obj[k] = game.i18n.localize(CONFIG.Item.typeLabels[k]); - return obj; - }, {}); - context.enchantment = this.document.system.enchantment; - context.isSpell = this.document.type === "spell"; - context.source = this.document.toObject().system.enchantment; - - const effects = []; - context.enchantments = []; - for ( const effect of this.document.effects ) { - if ( effect.getFlag("dnd5e", "type") !== "enchantment" ) effects.push(effect); - else if ( !effect.isAppliedEnchantment ) context.enchantments.push(effect); - } - context.enchantments = context.enchantments.map(effect => ({ - id: effect.id, - uuid: effect.uuid, - name: effect.name, - flags: effect.flags, - collapsed: this.expandedEnchantments.get(effect.id) ? "" : "collapsed", - riderEffects: effects.map(({ id, name }) => ({ - id, name, selected: effect.flags.dnd5e?.enchantment?.riders?.effect?.includes(id) ? "selected" : "" - })), - riderItems: effect.flags.dnd5e?.enchantment?.riders?.item?.join(",") ?? "" - })); - - return context; - } - - /* -------------------------------------------- */ - /* Event Handling */ - /* -------------------------------------------- */ - - /** @inheritDoc */ - activateListeners(jQuery) { - super.activateListeners(jQuery); - const html = jQuery[0]; - - for ( const element of html.querySelectorAll("[data-action]") ) { - element.addEventListener("click", event => this.submit({ updateData: { - action: event.target.dataset.action, - enchantmentId: event.target.closest("[data-enchantment-id]")?.dataset.enchantmentId - } })); - } - - for ( const element of html.querySelectorAll("multi-select") ) { - element.addEventListener("change", this._onChangeInput.bind(this)); - } - - for ( const element of html.querySelectorAll(".collapsible") ) { - element.addEventListener("click", event => { - const id = event.target.closest("[data-enchantment-id]")?.dataset.enchantmentId; - if ( event.target.closest(".collapsible-content") || !id ) return; - event.currentTarget.classList.toggle("collapsed"); - this.expandedEnchantments.set(id, !event.currentTarget.classList.contains("collapsed")); - }); - } - } - - /* -------------------------------------------- */ - - /** @inheritDoc */ - async _updateObject(event, formData) { - const { action, effects, enchantmentId, ...data } = foundry.utils.expandObject(formData); - - await this.document.update({"system.enchantment": data}); - - const riderIds = new Set(); - const effectsChanges = Object.entries(effects ?? {}).map(([_id, changes]) => { - const updates = { _id, ...changes }; - // Fix bug with in V11 - if ( !foundry.utils.hasProperty(updates, "flags.dnd5e.enchantment.riders.effect") ) { - foundry.utils.setProperty(updates, "flags.dnd5e.enchantment.riders.effect", []); - } - // End bug fix - riderIds.add(...(foundry.utils.getProperty(updates, "flags.dnd5e.enchantment.riders.effect") ?? [])); - return updates; - }); - for ( const effect of this.document.effects ) { - if ( effect.getFlag("dnd5e", "type") === "enchantment" ) continue; - if ( riderIds.has(effect.id) ) effectsChanges.push({ _id: effect.id, "flags.dnd5e.rider": true }); - else effectsChanges.push({ _id: effect.id, "flags.dnd5e.-=rider": null }); - } - if ( effectsChanges.length ) await this.document.updateEmbeddedDocuments("ActiveEffect", effectsChanges); - - const enchantment = this.document.effects.get(enchantmentId); - switch ( action ) { - case "add-enchantment": - const effect = await ActiveEffect.implementation.create({ - name: this.document.name, - icon: this.document.img, - "flags.dnd5e.type": "enchantment" - }, { parent: this.document }); - effect.sheet.render(true); - break; - case "delete-enchantment": - enchantment?.deleteDialog(); - break; - case "edit-enchantment": - enchantment?.sheet.render(true); - break; - } + constructor() { + throw new Error( + "EnchantmentConfig has been deprecated. Configuring enchating should now be performed through the Enchant activity." + ); } } diff --git a/module/applications/item/item-sheet.mjs b/module/applications/item/item-sheet.mjs index 9fc6ddfc0b..32c454d51e 100644 --- a/module/applications/item/item-sheet.mjs +++ b/module/applications/item/item-sheet.mjs @@ -9,7 +9,6 @@ import AdvancementMigrationDialog from "../advancement/advancement-migration-dia import Accordion from "../accordion.mjs"; import EffectsElement from "../components/effects.mjs"; import SourceConfig from "../source-config.mjs"; -import EnchantmentConfig from "./enchantment-config.mjs"; import StartingEquipmentConfig from "./starting-equipment-config.mjs"; /** @@ -538,9 +537,6 @@ export default class ItemSheet5e extends ItemSheet { const button = event.currentTarget; let app; switch ( button.dataset.action ) { - case "enchantment": - app = new EnchantmentConfig(this.item); - break; case "movement": app = new ActorMovementConfig(this.item, { keyPath: "system.movement" }); break; @@ -706,7 +702,7 @@ export default class ItemSheet5e extends ItemSheet { let keepOrigin = false; // Validate against the enchantment's restraints on the origin item - if ( effect.getFlag("dnd5e", "type") === "enchantment" ) { + if ( effect.type === "enchantment" ) { const errors = effect.parent.system.enchantment?.canEnchant(this.item); if ( errors?.length ) { errors.forEach(err => ui.notifications.error(err.message)); diff --git a/module/config.mjs b/module/config.mjs index e42ad3fac6..645729c240 100644 --- a/module/config.mjs +++ b/module/config.mjs @@ -3236,6 +3236,9 @@ DND5E.activityTypes = { attack: { documentClass: activities.AttackActivity }, + enchant: { + documentClass: activities.EnchantActivity + }, save: { documentClass: activities.SaveActivity }, diff --git a/module/data/activity/_module.mjs b/module/data/activity/_module.mjs index f4a783cbfc..c65af89ff9 100644 --- a/module/data/activity/_module.mjs +++ b/module/data/activity/_module.mjs @@ -1,6 +1,7 @@ export {default as BaseActivityData} from "./base-activity.mjs"; export {default as AttackActivityData} from "./attack-data.mjs"; +export {default as EnchantActivityData} from "./enchant-data.mjs"; export {default as SaveActivityData} from "./save-data.mjs"; export {default as SummonActivityData} from "./summon-data.mjs"; export {default as UtilityActivityData} from "./utility-data.mjs"; diff --git a/module/data/activity/enchant-data.mjs b/module/data/activity/enchant-data.mjs new file mode 100644 index 0000000000..9c3a41804a --- /dev/null +++ b/module/data/activity/enchant-data.mjs @@ -0,0 +1,52 @@ +import IdentifierField from "../fields/identifier-field.mjs"; +import BaseActivityData from "./base-activity.mjs"; +import AppliedEffectField from "./fields/applied-effect-field.mjs"; + +const { + ArrayField, BooleanField, DocumentIdField, DocumentUUIDField, NumberField, SchemaField, SetField, StringField +} = foundry.data.fields; + +/** + * @typedef {EffectApplicationData} EnchantEffectApplicationData + * @property {object} level + * @property {number} level.min Minimum level at which this profile can be used. + * @property {number} level.max Maximum level at which this profile can be used. + * @property {object} riders + * @property {string[]} riders.effect IDs of other effects on this item that will be added with this enchantment. + * @property {string[]} riders.item UUIDs of items that will be added with this enchantment. + */ + +/** + * Data model for a enchant activity. + * + * @property {object} enchant + * @property {string} enchant.identifier Class identifier that will be used to determine applicable level. + * @property {object} restrictions + * @property {boolean} restrictions.allowMagical Allow enchantments to be applied to items that are already magical. + * @property {string} restrictions.type Item type to which this enchantment can be applied. + */ +export default class EnchantActivityData extends BaseActivityData { + /** @inheritDoc */ + static defineSchema() { + return { + ...super.defineSchema(), + effects: new ArrayField(new AppliedEffectField({ + level: new SchemaField({ + min: new NumberField({ min: 0, integer: true }), + max: new NumberField({ min: 0, integer: true }) + }), + riders: new SchemaField({ + effect: new SetField(new DocumentIdField()), + item: new SetField(new DocumentUUIDField()) + }) + })), + enchant: new SchemaField({ + identifier: new IdentifierField() + }), + restrictions: new SchemaField({ + allowMagical: new BooleanField(), + type: new StringField() + }) + }; + } +} diff --git a/module/data/item/fields/enchantment-field.mjs b/module/data/item/fields/enchantment-field.mjs index d597623931..e77d8efc3d 100644 --- a/module/data/item/fields/enchantment-field.mjs +++ b/module/data/item/fields/enchantment-field.mjs @@ -87,7 +87,7 @@ export class EnchantmentData extends foundry.abstract.DataModel { * @type {ActiveEffect5e[]} */ get enchantments() { - return this.item.effects.filter(ae => ae.getFlag("dnd5e", "type") === "enchantment"); + return this.item.effects.filter(ae => ae.type === "enchantment"); } /* -------------------------------------------- */ @@ -159,7 +159,7 @@ export class EnchantmentData extends foundry.abstract.DataModel { : "details.level"; const level = foundry.utils.getProperty(item.getRollData(), keyPath) ?? 0; return item.effects.filter(e => { - if ( (e.getFlag("dnd5e", "type") !== "enchantment") || e.isAppliedEnchantment ) return false; + if ( (e.type !== "enchantment") || e.isAppliedEnchantment ) return false; const { min, max } = e.getFlag("dnd5e", "enchantment.level") ?? {}; return ((min ?? -Infinity) <= level) && (level <= (max ?? Infinity)); }); diff --git a/module/documents/active-effect.mjs b/module/documents/active-effect.mjs index b9760e6430..709dade3a6 100644 --- a/module/documents/active-effect.mjs +++ b/module/documents/active-effect.mjs @@ -40,8 +40,7 @@ export default class ActiveEffect5e extends ActiveEffect { * @type {boolean} */ get isAppliedEnchantment() { - return (this.getFlag("dnd5e", "type") === "enchantment") - && !!this.origin && (this.origin !== this.parent.uuid); + return (this.type === "enchantment") && !!this.origin && (this.origin !== this.parent.uuid); } /* -------------------------------------------- */ @@ -271,7 +270,7 @@ export default class ActiveEffect5e extends ActiveEffect { */ determineSuppression() { this.isSuppressed = false; - if ( this.getFlag("dnd5e", "type") === "enchantment" ) return; + if ( this.type === "enchantment" ) return; if ( this.parent instanceof dnd5e.documents.Item5e ) this.isSuppressed = this.parent.areEffectsSuppressed; } @@ -293,6 +292,20 @@ export default class ActiveEffect5e extends ActiveEffect { return this.uuid; } + /* -------------------------------------------- */ + /* Data Migration */ + /* -------------------------------------------- */ + + /** @inheritDoc */ + static migrateData(source) { + source = super.migrateData(source); + if ( source.flags?.dnd5e?.type === "enchantment" ) { + source.type = "enchantment"; + delete source.flags.dnd5e.type; + } + return source; + } + /* -------------------------------------------- */ /* Lifecycle */ /* -------------------------------------------- */ @@ -410,7 +423,7 @@ export default class ActiveEffect5e extends ActiveEffect { if ( options.keepOrigin === false ) this.updateSource({ origin: this.parent.uuid }); // Enchantments cannot be added directly to actors - if ( (this.getFlag("dnd5e", "type") === "enchantment") && (this.parent instanceof Actor) ) { + if ( (this.type === "enchantment") && (this.parent instanceof Actor) ) { ui.notifications.error("DND5E.Enchantment.Warning.NotOnActor", { localize: true }); return false; } @@ -736,7 +749,7 @@ export default class ActiveEffect5e extends ActiveEffect { else if ( this.disabled ) properties.push("DND5E.EffectType.Inactive"); else if ( this.isTemporary ) properties.push("DND5E.EffectType.Temporary"); else properties.push("DND5E.EffectType.Passive"); - if ( this.getFlag("dnd5e", "type") === "enchantment" ) properties.push("DND5E.Enchantment.Label"); + if ( this.type === "enchantment" ) properties.push("DND5E.Enchantment.Label"); return { content: await renderTemplate( diff --git a/module/documents/activity/_module.mjs b/module/documents/activity/_module.mjs index 2d3b497ae4..de7c2ebe94 100644 --- a/module/documents/activity/_module.mjs +++ b/module/documents/activity/_module.mjs @@ -1,6 +1,7 @@ export {default as ActivityMixin} from "./mixin.mjs"; export {default as AttackActivity} from "./attack.mjs"; +export {default as EnchantActivity} from "./enchant.mjs"; export {default as SaveActivity} from "./save.mjs"; export {default as SummonActivity} from "./summon.mjs"; export {default as UtilityActivity} from "./utility.mjs"; diff --git a/module/documents/activity/enchant.mjs b/module/documents/activity/enchant.mjs new file mode 100644 index 0000000000..ce470975c8 --- /dev/null +++ b/module/documents/activity/enchant.mjs @@ -0,0 +1,50 @@ +import EnchantSheet from "../../applications/activity/enchant-sheet.mjs"; +import EnchantActivityData from "../../data/activity/enchant-data.mjs"; +import ActivityMixin from "./mixin.mjs"; + +/** + * Activity for enchanting items. + */ +export default class EnchantActivity extends ActivityMixin(EnchantActivityData) { + /* -------------------------------------------- */ + /* Model Configuration */ + /* -------------------------------------------- */ + + /** @inheritDoc */ + static LOCALIZATION_PREFIXES = [...super.LOCALIZATION_PREFIXES, "DND5E.ENCHANT"]; + + /* -------------------------------------------- */ + + /** @inheritDoc */ + static metadata = Object.freeze( + foundry.utils.mergeObject(super.metadata, { + type: "enchant", + img: "systems/dnd5e/icons/svg/activity/enchant.svg", + title: "DND5E.ENCHANT.Title", + sheetClass: EnchantSheet + }, { inplace: false }) + ); + + /* -------------------------------------------- */ + + /** @inheritDoc */ + static localize() { + super.localize(); + this._localizeSchema(this.schema.fields.effects.element, ["DND5E.ENCHANT.FIELDS.effects"]); + } + + /* -------------------------------------------- */ + /* Properties */ + /* -------------------------------------------- */ + + /** + * List of item types that are enchantable. + * @type {Set} + */ + get enchantableTypes() { + return Object.entries(CONFIG.Item.dataModels).reduce((set, [k, v]) => { + if ( v.metadata?.enchantable ) set.add(k); + return set; + }, new Set()); + } +} diff --git a/module/documents/actor/actor.mjs b/module/documents/actor/actor.mjs index 31bc691504..feab43017e 100644 --- a/module/documents/actor/actor.mjs +++ b/module/documents/actor/actor.mjs @@ -229,7 +229,7 @@ export default class Actor5e extends SystemDocumentMixin(Actor) { /** @inheritDoc */ *allApplicableEffects() { for ( const effect of super.allApplicableEffects() ) { - if ( (effect.getFlag("dnd5e", "type") !== "enchantment") && !effect.getFlag("dnd5e", "rider") ) yield effect; + if ( (effect.type !== "enchantment") && !effect.getFlag("dnd5e", "rider") ) yield effect; } } diff --git a/module/documents/item.mjs b/module/documents/item.mjs index 372cfcfda5..289332fc41 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -1273,7 +1273,7 @@ export default class Item5e extends SystemDocumentMixin(Item) { config: CONFIG.DND5E, tokenId: token?.uuid || null, item: this, - effects: this.effects.filter(e => (e.getFlag("dnd5e", "type") !== "enchantment") && !e.getFlag("dnd5e", "rider")), + effects: this.effects.filter(e => (e.type !== "enchantment") && !e.getFlag("dnd5e", "rider")), data: await this.system.getCardData(), labels: this.labels, hasAttack: this.hasAttack, diff --git a/system.json b/system.json index 6e1853b56c..d8d4039684 100644 --- a/system.json +++ b/system.json @@ -24,6 +24,9 @@ "dnd5e.css" ], "documentTypes": { + "ActiveEffect": { + "enchantment": {} + }, "Actor": { "character": { "htmlFields": ["details.biography.value", "details.biography.public"] diff --git a/templates/activity/enchant-effect.hbs b/templates/activity/enchant-effect.hbs new file mode 100644 index 0000000000..7a6d2c782a --- /dev/null +++ b/templates/activity/enchant-effect.hbs @@ -0,0 +1,5 @@ +
+ {{> "templates/generic/tab-navigation.hbs" tabs=tabs.effect.tabs }} + {{> "systems/dnd5e/templates/activity/parts/enchant-enchantments.hbs" tab=tabs.effect.tabs.enchantments }} + {{> "systems/dnd5e/templates/activity/parts/enchant-restrictions.hbs" tab=tabs.effect.tabs.restrictions }} +
diff --git a/templates/activity/parts/activity-effects.hbs b/templates/activity/parts/activity-effects.hbs index 389a712580..d14b61488c 100644 --- a/templates/activity/parts/activity-effects.hbs +++ b/templates/activity/parts/activity-effects.hbs @@ -29,7 +29,7 @@ {{#if additionalSettings}} -
+ -
+