From 4628b020f79a3a3c856530ccf305f760f391da58 Mon Sep 17 00:00:00 2001 From: Jeff Hitchcock Date: Mon, 5 Sep 2022 15:42:10 -0700 Subject: [PATCH 1/5] [#1465] Allow item advancement to be migrated using drag-and-drop --- module/advancement/advancement-manager.mjs | 65 +++++++++++++++++++++- module/applications/item/item-sheet.mjs | 42 +++++++++++++- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/module/advancement/advancement-manager.mjs b/module/advancement/advancement-manager.mjs index 282f41c826..d39a6d2dad 100644 --- a/module/advancement/advancement-manager.mjs +++ b/module/advancement/advancement-manager.mjs @@ -200,7 +200,7 @@ export default class AdvancementManager extends Application { /** * Construct a manager for an item that needs to be deleted. * @param {Actor5e} actor Actor from which the item should be deleted. - * @param {object} itemId ID of the item to be deleted. + * @param {string} itemId ID of the item to be deleted. * @param {object} options Rendering options passed to the application. * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed. */ @@ -303,6 +303,69 @@ export default class AdvancementManager extends Application { .map(a => new a.constructor.metadata.apps.flow(item, a.id, level)); } + /* -------------------------------------------- */ + + /** + * Construct a manager for a newly added advancement from drag-drop. + * @param {Actor5e} actor Actor from which the advancement should be updated. + * @param {string} itemId ID of the item to which the advancements are being dropped. + * @param {Advancement[]} advancements Dropped advancements to add. + * @param {object} options Rendering options passed to the application. + * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed. + */ + static forMigration(actor, itemId, advancements, options) { + const manager = new this(actor, options); + const clonedItem = manager.clone.items.get(itemId); + if ( !clonedItem ) return manager; + + const currentLevel = clonedItem.system.levels ?? cloneItem.class?.system.levels + ?? manager.clone.system.details.level; + let minimumLevel = Infinity; + + // Determine which advancements need to be added + const advancementsToAdd = []; + for ( const advancement of advancements ) { + if ( clonedItem.advancement.byId[advancement.id] ) continue; + advancementsToAdd.push(advancement); + minimumLevel = Math.min(advancement.levels[0] ?? Infinity, minimumLevel); + } + if ( !advancementsToAdd.length ) return manager; + + const advancementArray = clonedItem.toObject().system.advancement; + + // If no advancement changes need to be immediately applied, just add the new advancements + if ( minimumLevel > currentLevel ) { + advancementArray.push(...advancementsToAdd.map(a => a.data)); + actor.items.get(itemId).update({"system.advancement": advancementArray}); + return manager; + } + + const oldFlows = Array.fromRange(currentLevel + 1).slice(minimumLevel) + .flatMap(l => this.flowsForLevel(clonedItem, l)); + + // Revert advancements trough minimum level + oldFlows.reverse().forEach(flow => manager.steps.push({ type: "reverse", flow, automatic: true })); + + // Add new advancements + advancementArray.push(...advancementsToAdd.map(a => a.data)); + clonedItem.updateSource({"system.advancement": advancementArray}); + + const newFlows = Array.fromRange(currentLevel + 1).slice(minimumLevel) + .flatMap(l => this.flowsForLevel(clonedItem, l)); + + // Restore existing advancements and apply new advancements + newFlows.forEach(flow => { + const matchingFlow = oldFlows.find(f => (f.advancement.id === flow.advancement.id) && (f.level === flow.level)); + if ( matchingFlow ) { + manager.steps.push({ type: "restore", flow: matchingFlow, automatic: true }); + } else { + manager.steps.push({ type: "forward", flow }); + } + }); + + return manager; + } + /* -------------------------------------------- */ /* Form Rendering */ /* -------------------------------------------- */ diff --git a/module/applications/item/item-sheet.mjs b/module/applications/item/item-sheet.mjs index 2e52a249a3..0662075662 100644 --- a/module/applications/item/item-sheet.mjs +++ b/module/applications/item/item-sheet.mjs @@ -31,7 +31,7 @@ export default class ItemSheet5e extends ItemSheet { resizable: true, scrollY: [".tab.details"], tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}], - dragDrop: [{dragSelector: "[data-effect-id]", dropSelector: ".effects-list"}], + dragDrop: [{dragSelector: "[data-effect-id]", dropSelector: ".effects-list"}, { dropSelector: ".advancement" }] }); } @@ -533,7 +533,7 @@ export default class ItemSheet5e extends ItemSheet { _onDrop(event) { const data = TextEditor.getDragEventData(event); const item = this.item; - + /** * A hook event that fires when some useful data is dropped onto an ItemSheet5e. * @function dnd5e.dropItemSheetData @@ -549,6 +549,8 @@ export default class ItemSheet5e extends ItemSheet { switch ( data.type ) { case "ActiveEffect": return this._onDropActiveEffect(event, data); + case "Item": + return this._onDropAdvancement(event, data); } } @@ -567,12 +569,46 @@ export default class ItemSheet5e extends ItemSheet { if ( (this.item.uuid === effect.parent.uuid) || (this.item.uuid === effect.origin) ) return false; return ActiveEffect.create({ ...effect.toObject(), - origin: this.item.uuid, + origin: this.item.uuid }, {parent: this.item}); } /* -------------------------------------------- */ + /** + * Handle the dropping of an advancement or item with advancements onto the advancements tab. + * @param {DragEvent} event The concluding DragEvent which contains drop data. + * @param {object} data The data transfer extracted from the event. + */ + async _onDropAdvancement(event, data) { + let advancements; + if ( data.type === "Item" ) { + const item = await Item.implementation.fromDropData(data); + if ( !item ) return false; + advancements = Object.values(item.advancement.byId); + } else { + return false; + } + advancements = advancements.filter(a => { + return !this.item.advancement.byId[a.id] + && a.constructor.metadata.validItemTypes.has(this.item.type) + && a.constructor.availableForItem(this.item); + }); + + if ( !advancements.length ) return false; + if ( this.item.isEmbedded && !game.settings.get("dnd5e", "disableAdvancements") ) { + const manager = AdvancementManager.forMigration(this.item.actor, this.item.id, advancements); + if ( manager.steps.length ) return manager.render(true); + } + + // If no advancements need to be applied, just add them to the item + const advancementArray = foundry.utils.deepClone(this.item.system.advancement); + advancementArray.push(...advancements.map(a => a.data)); + this.item.update({"system.advancement": advancementArray}); + } + + /* -------------------------------------------- */ + /** * Handle spawning the TraitSelector application for selection various options. * @param {Event} event The click event which originated the selection. From 3758742e36f25472390e4d0dcb65295c8edddcf9 Mon Sep 17 00:00:00 2001 From: Jeff Hitchcock Date: Tue, 6 Sep 2022 11:55:16 -0700 Subject: [PATCH 2/5] [#1774] Unapply advancement's changes when it is deleted --- module/advancement/advancement-manager.mjs | 49 ++++++++++++++++++- module/advancement/advancement.mjs | 6 +-- module/applications/item/item-sheet.mjs | 10 +++- module/documents/item.mjs | 57 +++++++++++++--------- 4 files changed, 91 insertions(+), 31 deletions(-) diff --git a/module/advancement/advancement-manager.mjs b/module/advancement/advancement-manager.mjs index d39a6d2dad..a061d5a5f5 100644 --- a/module/advancement/advancement-manager.mjs +++ b/module/advancement/advancement-manager.mjs @@ -197,6 +197,40 @@ export default class AdvancementManager extends Application { /* -------------------------------------------- */ + /** + * Construct a manager for an advancement that needs to be deleted. + * @param {Actor5e} actor Actor from which the advancement should be unapplied. + * @param {string} itemId ID of the item from which the advancement should be deleted. + * @param {string} advancementId ID of the advancement to delete. + * @param {object} options Rendering options passed to the application. + * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed. + */ + static forDeletedAdvancement(actor, itemId, advancementId, options) { + const manager = new this(actor, options); + const clonedItem = manager.clone.items.get(itemId); + const advancement = clonedItem?.advancement.byId[advancementId]; + if ( !advancement ) return manager; + + const minimumLevel = advancement.levels[0]; + const currentLevel = clonedItem.system.levels ?? cloneItem.class?.system.levels + ?? manager.clone.system.details.level; + + // If minimum level is greater than current level, no changes to remove + if ( (minimumLevel > currentLevel) || !advancement.appliesToClass ) return manager; + + advancement.levels + .reverse() + .filter(l => l <= currentLevel) + .map(l => new advancement.constructor.metadata.apps.flow(clonedItem, advancementId, l)) + .forEach(flow => manager.steps.push({ type: "reverse", flow, automatic: true })); + + if ( manager.steps.length ) manager.steps.push({ type: "delete", advancement, automatic: true }); + + return manager; + } + + /* -------------------------------------------- */ + /** * Construct a manager for an item that needs to be deleted. * @param {Actor5e} actor Actor from which the item should be deleted. @@ -314,6 +348,9 @@ export default class AdvancementManager extends Application { * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed. */ static forMigration(actor, itemId, advancements, options) { + // TODO: Split into two methods, one for determining what advancement to add, + // the second for generating steps for a provided array of new advancements + const manager = new this(actor, options); const clonedItem = manager.clone.items.get(itemId); if ( !clonedItem ) return manager; @@ -330,8 +367,10 @@ export default class AdvancementManager extends Application { minimumLevel = Math.min(advancement.levels[0] ?? Infinity, minimumLevel); } if ( !advancementsToAdd.length ) return manager; + // TODO: Verify that advancement can actually be added to item of this type and this item const advancementArray = clonedItem.toObject().system.advancement; + // TODO: Reset `data.value` on newly added advancements // If no advancement changes need to be immediately applied, just add the new advancements if ( minimumLevel > currentLevel ) { @@ -512,7 +551,10 @@ export default class AdvancementManager extends Application { const flow = this.step.flow; // Apply changes based on step type - if ( this.step.type === "delete" ) this.clone.items.delete(this.step.item.id); + if ( (this.step.type === "delete") && this.step.item ) this.clone.items.delete(this.step.item.id); + else if ( (this.step.type === "delete") && this.step.advancement ) { + this.step.advancement.item.deleteAdvancement(this.step.advancement.id, { local: true }); + } else if ( this.step.type === "restore" ) await flow.advancement.restore(flow.level, flow.retainedData); else if ( this.step.type === "reverse" ) flow.retainedData = await flow.advancement.reverse(flow.level); else if ( flow ) await flow._updateObject(event, flow._getSubmitData()); @@ -560,7 +602,10 @@ export default class AdvancementManager extends Application { const flow = this.step.flow; // Reverse step based on step type - if ( this.step.type === "delete" ) this.clone.updateSource({items: [this.step.item]}); + if ( (this.step.type === "delete") && this.step.item ) this.clone.updateSource({items: [this.step.item]}); + else if ( (this.step.type === "delete") && this.step.advancement ) { + this.advancement.item.createAdvancement(this.advancement.typeName, this.advancement.data, { local: true }); + } else if ( this.step.type === "reverse" ) await flow.advancement.restore(flow.level, flow.retainedData); else if ( flow ) flow.retainedData = await flow.advancement.reverse(flow.level); this.clone.reset(); diff --git a/module/advancement/advancement.mjs b/module/advancement/advancement.mjs index 18f7be5b79..22ff92c093 100644 --- a/module/advancement/advancement.mjs +++ b/module/advancement/advancement.mjs @@ -266,12 +266,8 @@ export default class Advancement { * @returns {Advancement} This advancement after updates have been applied. */ updateSource(updates) { - const advancement = foundry.utils.deepClone(this.item.system.advancement); - const idx = advancement.findIndex(a => a._id === this.id); - if ( idx < 0 ) throw new Error(`Advancement of ID ${this.id} could not be found to update`); + this.item.updateAdvancement(this.id, updates, { local: true }); foundry.utils.mergeObject(this.data, updates, { performDeletions: true }); - foundry.utils.mergeObject(advancement[idx], updates, { performDeletions: true }); - this.item.updateSource({"system.advancement": advancement}); return this; } diff --git a/module/applications/item/item-sheet.mjs b/module/applications/item/item-sheet.mjs index 0662075662..94edcb5f25 100644 --- a/module/applications/item/item-sheet.mjs +++ b/module/applications/item/item-sheet.mjs @@ -655,15 +655,21 @@ export default class ItemSheet5e extends ItemSheet { _onAdvancementAction(target, action) { const id = target.closest(".advancement-item")?.dataset.id; const advancement = this.item.advancement.byId[id]; + let manager; if ( ["edit", "delete", "duplicate"].includes(action) && !advancement ) return; switch (action) { case "add": return game.dnd5e.advancement.AdvancementSelection.createDialog(this.item); case "edit": return new advancement.constructor.metadata.apps.config(advancement).render(true); - case "delete": return this.item.deleteAdvancement(id); + case "delete": + if ( this.item.isEmbedded && !game.settings.get("dnd5e", "disableAdvancements") ) { + manager = AdvancementManager.forDeletedAdvancement(this.item.actor, this.item.id, id); + if ( manager.steps.length ) return manager.render(true); + } + return this.item.deleteAdvancement(id); case "duplicate": return this.item.duplicateAdvancement(id); case "modify-choices": const level = target.closest("li")?.dataset.level; - const manager = AdvancementManager.forModifyChoices(this.item.actor, this.item.id, Number(level)); + manager = AdvancementManager.forModifyChoices(this.item.actor, this.item.id, Number(level)); if ( manager.steps.length ) manager.render(true); return; case "toggle-configuration": diff --git a/module/documents/item.mjs b/module/documents/item.mjs index aa59de39f1..58d687beaf 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -1932,13 +1932,15 @@ export default class Item5e extends Item { /** * Create a new advancement of the specified type. - * @param {string} type Type of advancement to create. - * @param {object} [data] Data to use when creating the advancement. + * @param {string} type Type of advancement to create. + * @param {object} [data] Data to use when creating the advancement. * @param {object} [options] - * @param {boolean} [options.showConfig=true] Should the new advancement's configuration application be shown? - * @returns {Promise} + * @param {boolean} [options.showConfig=true] Should the new advancement's configuration application be shown? + * @param {boolean} [options.local=false] Should a local-only update be performed? + * @returns {Promise|Item5e} Promise for advancement config for new advancement if local + * is `false`, or item with newly added advancement. */ - async createAdvancement(type, data={}, { showConfig=true }={}) { + createAdvancement(type, data={}, { showConfig=true, local=false }={}) { if ( !this.system.advancement ) return; const Advancement = dnd5e.advancement.types[`${type}Advancement`]; @@ -1952,52 +1954,63 @@ export default class Item5e extends Item { const advancement = this.toObject().system.advancement; if ( !data._id ) data._id = foundry.utils.randomID(); advancement.push(data); - await this.update({"system.advancement": advancement}); - if ( !showConfig ) return; - const config = new Advancement.metadata.apps.config(this.advancement.byId[data._id]); - return config.render(true); + if ( local ) return this.updateSource({"system.advancement": advancement}); + return this.update({"system.advancement": advancement}).then(() => { + if ( !showConfig ) return this; + const config = new Advancement.metadata.apps.config(this.advancement.byId[data._id]); + return config.render(true); + }); } /* -------------------------------------------- */ /** * Update an advancement belonging to this item. - * @param {string} id ID of the advancement to update. - * @param {object} updates Updates to apply to this advancement, using the same format as `Document#update`. - * @returns {Promise} This item with the changes applied. + * @param {string} id ID of the advancement to update. + * @param {object} updates Updates to apply to this advancement. + * @param {object} [options={}] + * @param {boolean} [options.local=false] Should a local-only update be performed? + * @returns {Promise|Item5e} This item with the changes applied, promised if local is `false`. */ - async updateAdvancement(id, updates) { + updateAdvancement(id, updates, { local=false }={}) { if ( !this.system.advancement ) return; const idx = this.system.advancement.findIndex(a => a._id === id); if ( idx === -1 ) throw new Error(`Advancement of ID ${id} could not be found to update`); const advancement = this.toObject().system.advancement; foundry.utils.mergeObject(advancement[idx], updates, { performDeletions: true }); - return this.update({"system.advancement": advancement}); + if ( local ) return this.updateSource({"system.advancement": advancement}); + else return this.update({"system.advancement": advancement}); } /* -------------------------------------------- */ /** * Remove an advancement from this item. - * @param {string} id ID of the advancement to remove. - * @returns {Promise} This item with the changes applied. + * @param {string} id ID of the advancement to remove. + * @param {object} [options={}] + * @param {boolean} [options.local=false] Should a local-only update be performed? + * @returns {Promise|Item5e} This item with the changes applied. */ - async deleteAdvancement(id) { + deleteAdvancement(id, { local=false }={}) { if ( !this.system.advancement ) return; - return this.update({"system.advancement": this.system.advancement.filter(a => a._id !== id)}); + const advancement = this.system.advancement.filter(a => a._id !== id); + if ( local ) return this.updateSource({"system.advancement": advancement}); + else return this.update({"system.advancement": advancement}); } /* -------------------------------------------- */ /** * Duplicate an advancement, resetting its value to default and giving it a new ID. - * @param {string} id ID of the advancement to duplicate. + * @param {string} id ID of the advancement to duplicate. * @param {object} [options] - * @param {boolean} [options.showConfig=true] Should the new advancement's configuration application be shown? - * @returns {Promise} This item with the changes applied. + * @param {boolean} [options.showConfig=true] Should the new advancement's configuration application be shown? + * @param {boolean} [options.local=false] Should a local-only update be performed? + * @returns {Promise|Item5e} Promise for advancement config for duplicate advancement if local + * is `false`, or item with newly duplicated advancement. */ - async duplicateAdvancement(id, options) { + duplicateAdvancement(id, options) { const original = this.advancement.byId[id]; if ( !original ) return; const duplicate = foundry.utils.deepClone(original.data); From fc0d2b2fbb868074d978936eb6c75923ab9e0ad7 Mon Sep 17 00:00:00 2001 From: Jeff Hitchcock Date: Tue, 6 Sep 2022 14:08:27 -0700 Subject: [PATCH 3/5] [#1465] Rename forMigration to forNewAdvancement, perform validity check in drop handler --- module/advancement/advancement-manager.mjs | 118 +++++++++------------ module/applications/item/item-sheet.mjs | 2 +- 2 files changed, 51 insertions(+), 69 deletions(-) diff --git a/module/advancement/advancement-manager.mjs b/module/advancement/advancement-manager.mjs index a061d5a5f5..e3b2edcb3a 100644 --- a/module/advancement/advancement-manager.mjs +++ b/module/advancement/advancement-manager.mjs @@ -123,6 +123,56 @@ export default class AdvancementManager extends Application { /* Factory Methods */ /* -------------------------------------------- */ + /** + * Construct a manager for a newly added advancement from drag-drop. + * @param {Actor5e} actor Actor from which the advancement should be updated. + * @param {string} itemId ID of the item to which the advancements are being dropped. + * @param {Advancement[]} advancements Dropped advancements to add. + * @param {object} options Rendering options passed to the application. + * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed. + */ + static forNewAdvancement(actor, itemId, advancements, options) { + const manager = new this(actor, options); + const clonedItem = manager.clone.items.get(itemId); + if ( !clonedItem || !advancements.length ) return manager; + + const currentLevel = clonedItem.system.levels ?? cloneItem.class?.system.levels + ?? manager.clone.system.details.level; + const minimumLevel = advancements.reduce((min, a) => Math.min(a.levels[0] ?? Infinity, min), Infinity); + if ( minimumLevel > currentLevel ) return manager; + + const oldFlows = Array.fromRange(currentLevel + 1).slice(minimumLevel) + .flatMap(l => this.flowsForLevel(clonedItem, l)); + + // Revert advancements trough minimum level + oldFlows.reverse().forEach(flow => manager.steps.push({ type: "reverse", flow, automatic: true })); + + // Add new advancements + const advancementArray = clonedItem.toObject().system.advancement; + advancementArray.push(...advancements.map(a => { + a.data.value = foundry.utils.deepClone(a.constructor.metadata.defaults.value); + return a.data; + })); + clonedItem.updateSource({"system.advancement": advancementArray}); + + const newFlows = Array.fromRange(currentLevel + 1).slice(minimumLevel) + .flatMap(l => this.flowsForLevel(clonedItem, l)); + + // Restore existing advancements and apply new advancements + newFlows.forEach(flow => { + const matchingFlow = oldFlows.find(f => (f.advancement.id === flow.advancement.id) && (f.level === flow.level)); + if ( matchingFlow ) { + manager.steps.push({ type: "restore", flow: matchingFlow, automatic: true }); + } else { + manager.steps.push({ type: "forward", flow }); + } + }); + + return manager; + } + + /* -------------------------------------------- */ + /** * Construct a manager for a newly added item. * @param {Actor5e} actor Actor to which the item is being added. @@ -337,74 +387,6 @@ export default class AdvancementManager extends Application { .map(a => new a.constructor.metadata.apps.flow(item, a.id, level)); } - /* -------------------------------------------- */ - - /** - * Construct a manager for a newly added advancement from drag-drop. - * @param {Actor5e} actor Actor from which the advancement should be updated. - * @param {string} itemId ID of the item to which the advancements are being dropped. - * @param {Advancement[]} advancements Dropped advancements to add. - * @param {object} options Rendering options passed to the application. - * @returns {AdvancementManager} Prepared manager. Steps count can be used to determine if advancements are needed. - */ - static forMigration(actor, itemId, advancements, options) { - // TODO: Split into two methods, one for determining what advancement to add, - // the second for generating steps for a provided array of new advancements - - const manager = new this(actor, options); - const clonedItem = manager.clone.items.get(itemId); - if ( !clonedItem ) return manager; - - const currentLevel = clonedItem.system.levels ?? cloneItem.class?.system.levels - ?? manager.clone.system.details.level; - let minimumLevel = Infinity; - - // Determine which advancements need to be added - const advancementsToAdd = []; - for ( const advancement of advancements ) { - if ( clonedItem.advancement.byId[advancement.id] ) continue; - advancementsToAdd.push(advancement); - minimumLevel = Math.min(advancement.levels[0] ?? Infinity, minimumLevel); - } - if ( !advancementsToAdd.length ) return manager; - // TODO: Verify that advancement can actually be added to item of this type and this item - - const advancementArray = clonedItem.toObject().system.advancement; - // TODO: Reset `data.value` on newly added advancements - - // If no advancement changes need to be immediately applied, just add the new advancements - if ( minimumLevel > currentLevel ) { - advancementArray.push(...advancementsToAdd.map(a => a.data)); - actor.items.get(itemId).update({"system.advancement": advancementArray}); - return manager; - } - - const oldFlows = Array.fromRange(currentLevel + 1).slice(minimumLevel) - .flatMap(l => this.flowsForLevel(clonedItem, l)); - - // Revert advancements trough minimum level - oldFlows.reverse().forEach(flow => manager.steps.push({ type: "reverse", flow, automatic: true })); - - // Add new advancements - advancementArray.push(...advancementsToAdd.map(a => a.data)); - clonedItem.updateSource({"system.advancement": advancementArray}); - - const newFlows = Array.fromRange(currentLevel + 1).slice(minimumLevel) - .flatMap(l => this.flowsForLevel(clonedItem, l)); - - // Restore existing advancements and apply new advancements - newFlows.forEach(flow => { - const matchingFlow = oldFlows.find(f => (f.advancement.id === flow.advancement.id) && (f.level === flow.level)); - if ( matchingFlow ) { - manager.steps.push({ type: "restore", flow: matchingFlow, automatic: true }); - } else { - manager.steps.push({ type: "forward", flow }); - } - }); - - return manager; - } - /* -------------------------------------------- */ /* Form Rendering */ /* -------------------------------------------- */ diff --git a/module/applications/item/item-sheet.mjs b/module/applications/item/item-sheet.mjs index 94edcb5f25..5e43ce1eec 100644 --- a/module/applications/item/item-sheet.mjs +++ b/module/applications/item/item-sheet.mjs @@ -597,7 +597,7 @@ export default class ItemSheet5e extends ItemSheet { if ( !advancements.length ) return false; if ( this.item.isEmbedded && !game.settings.get("dnd5e", "disableAdvancements") ) { - const manager = AdvancementManager.forMigration(this.item.actor, this.item.id, advancements); + const manager = AdvancementManager.forNewAdvancement(this.item.actor, this.item.id, advancements); if ( manager.steps.length ) return manager.render(true); } From c88bf8b81cba7b8c1768f15b115a7e435d7c5f3c Mon Sep 17 00:00:00 2001 From: Jeff Hitchcock Date: Tue, 6 Sep 2022 14:35:49 -0700 Subject: [PATCH 4/5] [#1771] Add ability to drag advancement from one item to another --- module/advancement/advancement.mjs | 13 +++++++++++++ module/applications/item/item-sheet.mjs | 12 ++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/module/advancement/advancement.mjs b/module/advancement/advancement.mjs index 22ff92c093..04746042b8 100644 --- a/module/advancement/advancement.mjs +++ b/module/advancement/advancement.mjs @@ -282,6 +282,19 @@ export default class Advancement { return true; } + /* -------------------------------------------- */ + + /** + * Serialize salient information for this Advancement when dragging it. + * @returns {object} An object of drag data. + */ + toDragData() { + const dragData = { type: "Advancement" }; + if ( this.id ) dragData.uuid = this.uuid; + else dragData.data = this.data; + return dragData; + } + /* -------------------------------------------- */ /* Application Methods */ /* -------------------------------------------- */ diff --git a/module/applications/item/item-sheet.mjs b/module/applications/item/item-sheet.mjs index 5e43ce1eec..ffc2fb7072 100644 --- a/module/applications/item/item-sheet.mjs +++ b/module/applications/item/item-sheet.mjs @@ -31,7 +31,10 @@ export default class ItemSheet5e extends ItemSheet { resizable: true, scrollY: [".tab.details"], tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}], - dragDrop: [{dragSelector: "[data-effect-id]", dropSelector: ".effects-list"}, { dropSelector: ".advancement" }] + dragDrop: [ + {dragSelector: "[data-effect-id]", dropSelector: ".effects-list"}, + {dragSelector: ".advancement-item", dropSelector: ".advancement"} + ] }); } @@ -519,6 +522,8 @@ export default class ItemSheet5e extends ItemSheet { if ( li.dataset.effectId ) { const effect = this.item.effects.get(li.dataset.effectId); dragData = effect.toDragData(); + } else if ( li.classList.contains("advancement-item") ) { + dragData = this.item.advancement.byId[li.dataset.id]?.toDragData(); } if ( !dragData ) return; @@ -549,6 +554,7 @@ export default class ItemSheet5e extends ItemSheet { switch ( data.type ) { case "ActiveEffect": return this._onDropActiveEffect(event, data); + case "Advancement": case "Item": return this._onDropAdvancement(event, data); } @@ -582,7 +588,9 @@ export default class ItemSheet5e extends ItemSheet { */ async _onDropAdvancement(event, data) { let advancements; - if ( data.type === "Item" ) { + if ( data.type === "Advancement" ) { + advancements = [await fromUuid(data.uuid)]; + } else if ( data.type === "Item" ) { const item = await Item.implementation.fromDropData(data); if ( !item ) return false; advancements = Object.values(item.advancement.byId); From 33b381a05c854375468832403df023590436a004 Mon Sep 17 00:00:00 2001 From: Jeff Hitchcock Date: Wed, 7 Sep 2022 14:18:12 -0700 Subject: [PATCH 5/5] [#1465] Add advancement migration dialog --- dnd5e.css | 37 +++--- lang/en.json | 3 + less/advancement.less | 6 + less/items.less | 110 +++++++++--------- module/advancement/_module.mjs | 2 +- .../advancement-migration-dialog.mjs | 54 +++++++++ module/applications/item/item-sheet.mjs | 12 ++ .../advancement-migration-dialog.hbs | 25 ++++ 8 files changed, 175 insertions(+), 74 deletions(-) create mode 100644 module/advancement/advancement-migration-dialog.mjs create mode 100644 templates/advancement/advancement-migration-dialog.hbs diff --git a/dnd5e.css b/dnd5e.css index 9bfac84493..16158adef2 100644 --- a/dnd5e.css +++ b/dnd5e.css @@ -1422,6 +1422,9 @@ .dnd5e.advancement.scale-value select option[value=""] { color: var(--color-text-light-6); } +.dnd5e.advancement-migration .items-list { + margin-block-end: 1em; +} /* ----------------------------------------- */ /* Advancement Flow */ /* ----------------------------------------- */ @@ -1518,9 +1521,6 @@ /* Item Actions */ /* ----------------------------------------- */ /* ----------------------------------------- */ - /* Item Advancement */ - /* ----------------------------------------- */ - /* ----------------------------------------- */ /* Loot Sheet (No Tabs) */ /* ----------------------------------------- */ } @@ -1698,37 +1698,43 @@ .dnd5e.sheet.item .weapon-properties label.checkbox { flex: 0 0 98px; } -.dnd5e.sheet.item .advancement .items-list { +.dnd5e.sheet.item .loot-header { + margin-bottom: 10px; +} +/* ----------------------------------------- */ +/* Item Advancement */ +/* ----------------------------------------- */ +.dnd5e .advancement .items-list { height: 100%; } -.dnd5e.sheet.item .advancement .items-list .main-controls .configuration-mode-control { +.dnd5e .advancement .items-list .main-controls .configuration-mode-control { flex: 1 0; margin-inline-start: 0.5em; } -.dnd5e.sheet.item .advancement .items-list .main-controls .configuration-mode-control a { +.dnd5e .advancement .items-list .main-controls .configuration-mode-control a { text-align: start; } -.dnd5e.sheet.item .advancement .items-list .main-controls .item-add { +.dnd5e .advancement .items-list .main-controls .item-add { padding-block-start: 0.2em; } -.dnd5e.sheet.item .advancement .items-list .items-header .item-checkmark { +.dnd5e .advancement .items-list .items-header .item-checkmark { flex: 0 0 44px; color: #119111; text-shadow: white 0 0 1px; } -.dnd5e.sheet.item .advancement .items-list .items-header .item-warning { +.dnd5e .advancement .items-list .items-header .item-warning { flex: 0 0 44px; color: #faff23; text-shadow: black 0 0 1px; } -.dnd5e.sheet.item .advancement .items-list .item-name { +.dnd5e .advancement .items-list .item-name { flex: 1 0 10em; } -.dnd5e.sheet.item .advancement .items-list .item-controls { +.dnd5e .advancement .items-list .item-controls { border-left: 1px solid #c9c7b8; flex: 0 0 44px; } -.dnd5e.sheet.item .advancement .items-list .item-summary { +.dnd5e .advancement .items-list .item-summary { flex: 0 0 100%; font-size: 12px; line-height: 16px; @@ -1736,16 +1742,13 @@ margin-top: -4px; color: #191813; } -.dnd5e.sheet.item .advancement .items-list .item-summary .item-list .item-name { +.dnd5e .advancement .items-list .item-summary .item-list .item-name { display: flex; } -.dnd5e.sheet.item .advancement .items-list .tag { +.dnd5e .advancement .items-list .tag { font-size: 0.7rem; padding: 0.1em 0.5em; } -.dnd5e.sheet.item .loot-header { - margin-bottom: 10px; -} /* ----------------------------------------- */ /* Chat Cards /* ----------------------------------------- */ diff --git a/lang/en.json b/lang/en.json index e861a522a2..a331ea2b17 100644 --- a/lang/en.json +++ b/lang/en.json @@ -139,6 +139,9 @@ "DND5E.AdvancementManagerRestartConfirmTitle": "Restart Advancement Choices", "DND5E.AdvancementManagerSteps": "Step {current} of {total}", "DND5E.AdvancementManagerTitle": "Advancement", +"DND5E.AdvancementMigrationConfirm": "Apply Migrations", +"DND5E.AdvancementMigrationHint": "Select which of the following advancements will be added to {name}.", +"DND5E.AdvancementMigrationTitle": "Migrate Advancement", "DND5E.AdvancementModifyChoices": "Modify Choices", "DND5E.AdvancementSaveButton": "Save Advancement", "DND5E.AdvancementScaleValueTitle": "Scale Value", diff --git a/less/advancement.less b/less/advancement.less index cc4f0ff27b..6269fd3dc3 100644 --- a/less/advancement.less +++ b/less/advancement.less @@ -86,6 +86,12 @@ } } +.dnd5e.advancement-migration { + .items-list { + margin-block-end: 1em; + } +} + /* ----------------------------------------- */ /* Advancement Flow */ /* ----------------------------------------- */ diff --git a/less/items.less b/less/items.less index 8ad68c424c..091b07a33c 100644 --- a/less/items.less +++ b/less/items.less @@ -240,69 +240,67 @@ } /* ----------------------------------------- */ - /* Item Advancement */ + /* Loot Sheet (No Tabs) */ /* ----------------------------------------- */ - .advancement { - .items-list { - height: 100%; + .loot-header { + margin-bottom: 10px; + } +} - .main-controls { - .configuration-mode-control { - flex: 1 0; - margin-inline-start: 0.5em; - a { text-align: start; } - } - .item-add { - padding-block-start: 0.2em; - } - } - .items-header { - .item-checkmark { - flex: 0 0 44px; - color: rgb(17 145 17); - text-shadow: white 0 0 1px; - } - .item-warning { - flex: 0 0 44px; - color: rgb(250 255 35); - text-shadow: black 0 0 1px; - } - } - .item-name { - flex: 1 0 10em; - } - .item-controls { - border-left: 1px solid @colorFaint; - flex: 0 0 44px; - } - .item-summary { - flex: 0 0 100%; - font-size: 12px; - line-height: 16px; - padding: 0 .5em .5em 34px; - margin-top: -4px; - color: @colorDark; - - .item-list { - .item-name { - display: flex; - } - } - } +/* ----------------------------------------- */ +/* Item Advancement */ +/* ----------------------------------------- */ - .tag { - font-size: 0.7rem; - padding: 0.1em 0.5em; +.dnd5e .advancement .items-list { + height: 100%; + + .main-controls { + .configuration-mode-control { + flex: 1 0; + margin-inline-start: 0.5em; + a { text-align: start; } + } + .item-add { + padding-block-start: 0.2em; + } + } + .items-header { + .item-checkmark { + flex: 0 0 44px; + color: rgb(17 145 17); + text-shadow: white 0 0 1px; + } + .item-warning { + flex: 0 0 44px; + color: rgb(250 255 35); + text-shadow: black 0 0 1px; + } + } + .item-name { + flex: 1 0 10em; + } + .item-controls { + border-left: 1px solid @colorFaint; + flex: 0 0 44px; + } + .item-summary { + flex: 0 0 100%; + font-size: 12px; + line-height: 16px; + padding: 0 .5em .5em 34px; + margin-top: -4px; + color: @colorDark; + + .item-list { + .item-name { + display: flex; } } } - /* ----------------------------------------- */ - /* Loot Sheet (No Tabs) */ - /* ----------------------------------------- */ - - .loot-header { - margin-bottom: 10px; + .tag { + font-size: 0.7rem; + padding: 0.1em 0.5em; } } diff --git a/module/advancement/_module.mjs b/module/advancement/_module.mjs index 6479608423..9ecb31512e 100644 --- a/module/advancement/_module.mjs +++ b/module/advancement/_module.mjs @@ -5,5 +5,5 @@ export {default as AdvancementConfig} from "./advancement-config.mjs"; export {default as AdvancementConfirmationDialog} from "./advancement-confirmation-dialog.mjs"; export {default as AdvancementFlow} from "./advancement-flow.mjs"; export {default as AdvancementManager} from "./advancement-manager.mjs"; +export {default as AdvancementMigrationDialog} from "./advancement-migration-dialog.mjs"; export {default as AdvancementSelection} from "./advancement-selection.mjs"; - diff --git a/module/advancement/advancement-migration-dialog.mjs b/module/advancement/advancement-migration-dialog.mjs new file mode 100644 index 0000000000..7a48e52be4 --- /dev/null +++ b/module/advancement/advancement-migration-dialog.mjs @@ -0,0 +1,54 @@ +/** + * Dialog to select which new advancements should be added to an item. + */ +export default class AdvancementMigrationDialog extends Dialog { + + /** @inheritdoc */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["dnd5e", "advancement-migration", "dialog"], + jQuery: false, + width: 500 + }); + } + + /* -------------------------------------------- */ + + /** + * A helper constructor function which displays the migration dialog. + * @param {Item5e} item Item to which the advancements are being added. + * @param {Advancement[]} advancements New advancements that should be displayed in the prompt. + * @returns {Promise} Resolves with the advancements that should be added, if any. + */ + static createDialog(item, advancements) { + const advancementContext = advancements.map(a => ({ + id: a.id, icon: a.icon, title: a.title, + summary: a.levels.length === 1 ? a.summaryForLevel(a.levels[0]) : "" + })); + return new Promise(async (resolve, reject) => { + const dialog = new this({ + title: `${game.i18n.localize("DND5E.AdvancementMigrationTitle")}: ${item.name}`, + content: await renderTemplate( + "systems/dnd5e/templates/advancement/advancement-migration-dialog.hbs", + { item, advancements: advancementContext } + ), + buttons: { + continue: { + icon: '', + label: game.i18n.localize("DND5E.AdvancementMigrationConfirm"), + callback: html => resolve(advancements.filter(a => html.querySelector(`[name="${a.id}"]`)?.checked)) + }, + cancel: { + icon: '', + label: game.i18n.localize("Cancel"), + callback: html => reject(null) + } + }, + default: "continue", + close: () => reject(null) + }); + dialog.render(true); + }); + } + +} diff --git a/module/applications/item/item-sheet.mjs b/module/applications/item/item-sheet.mjs index ffc2fb7072..3a311016be 100644 --- a/module/applications/item/item-sheet.mjs +++ b/module/applications/item/item-sheet.mjs @@ -1,4 +1,5 @@ import AdvancementManager from "../../advancement/advancement-manager.mjs"; +import AdvancementMigrationDialog from "../../advancement/advancement-migration-dialog.mjs"; import ProficiencySelector from "../proficiency-selector.mjs"; import TraitSelector from "../trait-selector.mjs"; import ActiveEffect5e from "../../documents/active-effect.mjs"; @@ -588,12 +589,14 @@ export default class ItemSheet5e extends ItemSheet { */ async _onDropAdvancement(event, data) { let advancements; + let showDialog = false; if ( data.type === "Advancement" ) { advancements = [await fromUuid(data.uuid)]; } else if ( data.type === "Item" ) { const item = await Item.implementation.fromDropData(data); if ( !item ) return false; advancements = Object.values(item.advancement.byId); + showDialog = true; } else { return false; } @@ -603,6 +606,15 @@ export default class ItemSheet5e extends ItemSheet { && a.constructor.availableForItem(this.item); }); + // Display dialog prompting for which advancements to add + if ( showDialog ) { + try { + advancements = await AdvancementMigrationDialog.createDialog(this.item, advancements); + } catch(err) { + return false; + } + } + if ( !advancements.length ) return false; if ( this.item.isEmbedded && !game.settings.get("dnd5e", "disableAdvancements") ) { const manager = AdvancementManager.forNewAdvancement(this.item.actor, this.item.id, advancements); diff --git a/templates/advancement/advancement-migration-dialog.hbs b/templates/advancement/advancement-migration-dialog.hbs new file mode 100644 index 0000000000..2588dcf6ea --- /dev/null +++ b/templates/advancement/advancement-migration-dialog.hbs @@ -0,0 +1,25 @@ +
+

{{localize "DND5E.AdvancementMigrationHint" name=item.name}}

+
    +
  1. +

    {{localize "DND5E.AdvancementTitle"}}

    +
    {{localize "DND5E.Add"}}
    +
  2. + {{#each advancements}} +
  3. +
    +
    +

    {{this.title}}

    +
    +
    + +
    + {{#if this.summary}} +
    + {{{this.summary}}} +
    + {{/if}} +
  4. + {{/each}} +
+