Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…& type restriction to ItemGrant advancement
  • Loading branch information
arbron committed Aug 7, 2022
1 parent c00db1d commit 4cd1b46
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 62 deletions.
2 changes: 1 addition & 1 deletion dnd5e.css
Original file line number Diff line number Diff line change
Expand Up @@ -1646,7 +1646,7 @@
margin-inline-start: 0.5em;
}
.dnd5e.sheet.item .advancement .items-list .main-controls .configuration-mode-control a {
text-align: inline-start;
text-align: start;
}
.dnd5e.sheet.item .advancement .items-list .main-controls .item-add {
padding-block-start: 0.2em;
Expand Down
1 change: 1 addition & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"DND5E.AdvancementItemGrantRecursiveWarning": "You cannot grant an item in its own advancement.",
"DND5E.AdvancementItemGrantOptional": "Optional",
"DND5E.AdvancementItemGrantOptionalHint": "If optional, players will be given the option to opt out of any of the following items, otherwise all of them are granted.",
"DND5E.AdvancementItemTypeInvalidWarning": "{type} items cannot be added with this advancement type.",
"DND5E.AdvancementLevelHeader": "Level {level}",
"DND5E.AdvancementLevelAnyHeader": "Any Level",
"DND5E.AdvancementLevelNoneHeader": "No Level",
Expand Down
2 changes: 1 addition & 1 deletion less/items.less
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@
.configuration-mode-control {
flex: 1 0;
margin-inline-start: 0.5em;
a { text-align: inline-start; }
a { text-align: start; }
}
.item-add {
padding-block-start: 0.2em;
Expand Down
101 changes: 98 additions & 3 deletions module/advancement/advancement-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
* Base configuration application for advancements that can be extended by other types to implement custom
* editing interfaces.
*
* @param {Advancement} advancement The advancement item being edited.
* @param {object} [options={}] Additional options passed to FormApplication.
* @property {Advancement} advancement The advancement item being edited.
* @param {object} [options={}] Additional options passed to FormApplication.
* @param {string} [options.dropKeyPath=null] Path within advancement configuration where dropped items are stored.
* If populated, will enable default drop & delete behavior.
*/
export default class AdvancementConfig extends FormApplication {
constructor(advancement, options={}) {
Expand Down Expand Up @@ -32,7 +34,8 @@ export default class AdvancementConfig extends FormApplication {
width: 400,
height: "auto",
submitOnChange: true,
closeOnSubmit: false
closeOnSubmit: false,
dropKeyPath: null
});
}

Expand All @@ -52,6 +55,7 @@ export default class AdvancementConfig extends FormApplication {
if ( ["class", "subclass"].includes(this.item.type) ) delete levels[0];
else levels[0] = game.i18n.localize("DND5E.AdvancementLevelAnyHeader");
return {
CONFIG: CONFIG.DND5E,
data: this.advancement.data,
default: {
title: this.advancement.constructor.metadata.title,
Expand All @@ -76,6 +80,15 @@ export default class AdvancementConfig extends FormApplication {

/* -------------------------------------------- */

activateListeners(html) {
super.activateListeners(html);

// Remove an item from the list
if ( this.options.dropKeyPath ) html.on("click", ".item-delete", this._onItemDelete.bind(this));
}

/* -------------------------------------------- */

/** @inheritdoc */
async _updateObject(event, formData) {
let updates = foundry.utils.expandObject(formData).data;
Expand All @@ -101,4 +114,86 @@ export default class AdvancementConfig extends FormApplication {
}, {});
}

/* -------------------------------------------- */
/* Drag & Drop for Item Pools */
/* -------------------------------------------- */

/**
* Handle deleting an existing Item entry from the Advancement.
* @param {Event} event The originating click event.
* @returns {Promise<Item5e>} The updated parent Item after the application re-renders.
* @protected
*/
async _onItemDelete(event) {
event.preventDefault();
const uuidToDelete = event.currentTarget.closest("[data-item-uuid]")?.dataset.itemUuid;
if ( !uuidToDelete ) return;
const items = foundry.utils.getProperty(this.advancement.data.configuration, this.options.dropKeyPath);
const updates = { configuration: await this.prepareConfigurationUpdate({
[this.options.dropKeyPath]: items.filter(uuid => uuid !== uuidToDelete)
}) };
console.log(updates);
await this.advancement.update(updates);
this.render();
}

/* -------------------------------------------- */

/** @inheritdoc */
_canDragDrop() {
return this.isEditable;
}

/* -------------------------------------------- */

/** @inheritdoc */
async _onDrop(event) {
if ( !this.options.dropKeyPath ) return console.error(
"AdvancementConfig#options.dropKeyPath must be configured or #_onDrop must be overridden to support"
+ " drag and drop on advancement config items."
);

// Try to extract the data
let data;
try {
data = JSON.parse(event.dataTransfer.getData("text/plain"));
} catch(err) {
return false;
}

if ( data.type !== "Item" ) return false;
const item = await Item.implementation.fromDropData(data);

const verified = this._verifyDroppedItem(event, item);
if ( !verified ) return false;

const existingItems = foundry.utils.getProperty(this.advancement.data.configuration, this.options.dropKeyPath);

// Abort if this uuid is the parent item
if ( item.uuid === this.item.uuid ) {
return ui.notifications.warn(game.i18n.localize("DND5E.AdvancementItemGrantRecursiveWarning"));
}

// Abort if this uuid exists already
if ( existingItems.includes(item.uuid) ) {
return ui.notifications.warn(game.i18n.localize("DND5E.AdvancementItemGrantDuplicateWarning"));
}

await this.advancement.update({[`configuration.${this.options.dropKeyPath}`]: [...existingItems, item.uuid]});
this.render();
}

/* -------------------------------------------- */

/**
* Called when an item is dropped to verify the Item before it is saved.
* @param {Event} event Triggering drop event.
* @param {Item5e} item The materialized Item that was dropped.
* @returns {boolean} Is the dropped Item valid?
* @protected
*/
_verifyDroppedItem(event, item) {
return true;
}

}
27 changes: 27 additions & 0 deletions module/advancement/advancement.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -308,4 +308,31 @@ export default class Advancement {
* @abstract
*/
async reverse(level) { }

/* -------------------------------------------- */

/**
* Helper method to prepare spell customizations.
* @param {object} spell Spell configuration object.
* @returns {object} Object of updates to apply to item.
* @protected
*/
_prepareSpellChanges(spell) {
const updates = {};
if ( spell.ability ) updates["system.ability"] = spell.ability;
if ( spell.preparation ) updates["system.preparation.mode"] = spell.preparation;
if ( spell.uses?.max ) {
updates["system.uses.max"] = spell.uses.max;
if ( Number.isNumeric(spell.uses.max) ) updates["system.uses.value"] = parseInt(spell.uses.max);
else {
try {
const rollData = this.actor.getRollData({ deterministic: true });
const formula = Roll.replaceFormulaData(spell.uses.max, rollData, {missing: 0});
updates["system.uses.value"] = Roll.safeEval(formula);
} catch(e) { }
}
}
if ( spell.uses?.per ) updates["system.uses.per"] = spell.uses.per;
return updates;
}
}
81 changes: 26 additions & 55 deletions module/advancement/types/item-grant.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ export class ItemGrantAdvancement extends Advancement {
static get metadata() {
return foundry.utils.mergeObject(super.metadata, {
defaults: {
configuration: { items: [], optional: false }
configuration: {
items: [],
optional: false,
spell: null
}
},
order: 40,
icon: "systems/dnd5e/icons/svg/item-grant.svg",
Expand All @@ -25,6 +29,14 @@ export class ItemGrantAdvancement extends Advancement {
});
}

/* -------------------------------------------- */

/**
* The item types that are supported in Item Grant.
* @type {Set<string>}
*/
static VALID_TYPES = new Set(["feat", "spell", "consumable", "backpack", "equipment", "loot", "tool", "weapon"]);

/* -------------------------------------------- */
/* Display Methods */
/* -------------------------------------------- */
Expand Down Expand Up @@ -66,6 +78,7 @@ export class ItemGrantAdvancement extends Advancement {
async apply(level, data, retainedData={}) {
const items = [];
const updates = {};
const spellChanges = this.data.configuration.spell ? this._prepareSpellChanges(this.data.configuration.spell) : {};
for ( const [uuid, selected] of Object.entries(data) ) {
if ( !selected ) continue;

Expand All @@ -79,6 +92,7 @@ export class ItemGrantAdvancement extends Advancement {
"flags.dnd5e.advancementOrigin": `${this.item.id}.${this.id}`
}, {keepId: true}).toObject();
}
foundry.utils.mergeObject(itemData, spellChanges);

items.push(itemData);
// TODO: Trigger any additional advancement steps for added items
Expand Down Expand Up @@ -129,72 +143,29 @@ export class ItemGrantConfig extends AdvancementConfig {
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
dragDrop: [{ dropSelector: ".drop-target" }],
dropKeyPath: "items",
template: "systems/dnd5e/templates/advancement/item-grant-config.hbs"
});
}

/* -------------------------------------------- */

activateListeners(html) {
super.activateListeners(html);

// Remove an item from the list
html.on("click", ".item-delete", this._onItemDelete.bind(this));
}

/* -------------------------------------------- */

/**
* Handle deleting an existing Item entry from the Advancement.
* @param {Event} event The originating click event.
* @returns {Promise<Item5e>} The promise for the updated parent Item which resolves after the application re-renders
* @private
*/
async _onItemDelete(event) {
event.preventDefault();
const uuidToDelete = event.currentTarget.closest("[data-item-uuid]")?.dataset.itemUuid;
if ( !uuidToDelete ) return;
const items = this.advancement.data.configuration.items.filter(uuid => uuid !== uuidToDelete);
const updates = { configuration: this.prepareConfigurationUpdate({ items }) };
await this.advancement.update(updates);
this.render();
}

/* -------------------------------------------- */

/** @inheritdoc */
_canDragDrop() {
return this.isEditable;
async getData() {
const data = super.getData();
data.items = data.data.configuration.items.map(fromUuidSync);
data.showSpellConfig = data.items.some(i => i.type === "spell");
return data;
}

/* -------------------------------------------- */

/** @inheritdoc */
async _onDrop(event) {
// Try to extract the data
let data;
try {
data = JSON.parse(event.dataTransfer.getData("text/plain"));
} catch(err) {
return false;
}

if ( data.type !== "Item" ) return false;
const item = await Item.implementation.fromDropData(data);
const existingItems = this.advancement.data.configuration.items;

// Abort if this uuid is the parent item
if ( item.uuid === this.item.uuid ) {
return ui.notifications.warn(game.i18n.localize("DND5E.AdvancementItemGrantRecursiveWarning"));
}

// Abort if this uuid exists already
if ( existingItems.includes(item.uuid) ) {
return ui.notifications.warn(game.i18n.localize("DND5E.AdvancementItemGrantDuplicateWarning"));
}

await this.advancement.update({"configuration.items": [...existingItems, item.uuid]});
this.render();
_verifyDroppedItem(event, item) {
if ( this.advancement.constructor.VALID_TYPES.has(item.type) ) return true;
const type = game.i18n.localize(`ITEM.Type${item.type.capitalize()}`);
ui.notifications.warn(game.i18n.format("DND5E.AdvancementItemTypeInvalidWarning", { type }));
return false;
}
}

Expand Down
3 changes: 2 additions & 1 deletion module/utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ export async function preloadHandlebarsTemplates() {
"systems/dnd5e/templates/items/parts/item-spellcasting.hbs",

// Advancement Partials
"systems/dnd5e/templates/advancement/parts/advancement-controls.hbs"
"systems/dnd5e/templates/advancement/parts/advancement-controls.hbs",
"systems/dnd5e/templates/advancement/parts/advancement-spell-config.hbs"
];

const paths = {};
Expand Down
8 changes: 7 additions & 1 deletion templates/advancement/item-grant-config.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<p class="hint">{{localize "DND5E.AdvancementItemGrantOptionalHint"}}</p>
</div>

{{#if showSpellConfig}}
{{> "dnd5e.advancement-spell-config"}}
{{/if}}

<div class="drop-target">
<ol class="items-list">
<li class="items-header flexrow"><h3 class="item-name">{{localize "DOCUMENT.Items"}}</h3></li>
Expand All @@ -16,7 +20,9 @@
<li class="item flexrow" data-item-uuid="{{this}}">
<div class="item-name">{{{dnd5e-linkForUuid this}}}</div>
<div class="item-controls flexrow">
<a class="item-control item-delete" title="Delete Item"><i class="fas fa-trash"></i></a>
<a class="item-control item-delete" title="{{localize 'DND5E.ItemDelete'}}">
<i class="fas fa-trash"></i>
</a>
</div>
</li>
{{/each}}
Expand Down
28 changes: 28 additions & 0 deletions templates/advancement/parts/advancement-spell-config.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<div class="form-group">
{{log this}}
<label>{{localize "DND5E.AbilityModifier"}}</label>
<div class="form-fields">
<select name="data.configuration.spell.ability">
{{selectOptions CONFIG.abilities selected=data.configuration.spell.ability blank="&mdash;"}}
</select>
</div>
</div>

<div class="form-group">
<label>{{localize "DND5E.SpellPreparationMode"}}</label>
<div class="form-fields">
<select name="data.configuration.spell.preparation">
{{selectOptions CONFIG.spellPreparationModes selected=data.configuration.spell.preparation blank="&mdash;"}}
</select>
</div>
</div>

<div class="form-group">
<label>{{localize "DND5E.LimitedUses"}}</label>
<div class="form-fields">
<input type="text" name="data.configuration.spell.uses.max" value="{{data.configuration.spell.uses.max}}">
<select name="data.configuration.spell.uses.per">
{{selectOptions CONFIG.limitedUsePeriods selected=data.configuration.spell.uses.per blank="&mdash;"}}
</select>
</div>
</div>

0 comments on commit 4cd1b46

Please sign in to comment.