Skip to content

Commit

Permalink
[foundryvtt#1401] Add initial version of ItemChoiceFlow
Browse files Browse the repository at this point in the history
  • Loading branch information
arbron committed Aug 22, 2022
1 parent f6571e4 commit 390eb95
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 3 deletions.
3 changes: 2 additions & 1 deletion lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@
"DND5E.AdvancementItemChoiceHint": "Present the player with a choice of items (such as equipment, features, or spells) that they can choose for their character at one or more levels.",
"DND5E.AdvancementItemChoiceAllowDrops": "Allow Drops",
"DND5E.AdvancementItemChoiceAllowDropsHint": "Should players be able to drop their own choices into this advancement?",
"DND5E.AdvancementItemChoiceDropHint": "Drop Items here to add them to the pool from which a player can choose.",
"DND5E.AdvancementItemChoiceConfigDropHint": "Drop Items here to add them to the pool from which a player can choose.",
"DND5E.AdvancementItemChoiceFlowDropHint": "Drop an Item here to choose it.",
"DND5E.AdvancementItemChoiceLevelsHint": "Specify how many choices are allowed at each level.",
"DND5E.AdvancementItemGrantTitle": "Grant Items",
"DND5E.AdvancementItemGrantHint": "Grant the character items (such as equipment, features, or spells) when they reach a certain level.",
Expand Down
164 changes: 163 additions & 1 deletion module/advancement/types/item-choice.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Advancement from "../advancement.mjs";
import AdvancementConfig from "../advancement-config.mjs";
import AdvancementFlow from "../advancement-flow.mjs";

/**
* Advancement that presents the player with a choice of multiple items that they can take. Keeps track of which
Expand All @@ -25,7 +26,8 @@ export class ItemChoiceAdvancement extends Advancement {
hint: game.i18n.localize("DND5E.AdvancementItemChoiceHint"),
multiLevel: true,
apps: {
config: ItemChoiceConfig
config: ItemChoiceConfig,
flow: ItemChoiceFlow
}
});
}
Expand Down Expand Up @@ -94,3 +96,163 @@ export class ItemChoiceConfig extends AdvancementConfig {
}

}


/**
* Inline application that presents the player with a choice of items.
*
* @extends {AdvancementFlow}
*/
export class ItemChoiceFlow extends AdvancementFlow {

/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
dragDrop: [{ dropSelector: ".drop-target" }],
template: "systems/dnd5e/templates/advancement/item-choice-flow.html"
});
}

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

/** @inheritdoc */
async getData() {
const maxChoices = this.advancement.data.configuration.choices[this.level];
const added = this.retainedData?.items.map(i => foundry.utils.getProperty(i, "flags.dnd5e.sourceId"))
?? this.advancement.data.value[this.level];
const checked = new Set(Object.values(added ?? {}));

const allUuids = new Set([...this.advancement.data.configuration.pool, ...checked]);
const items = await Promise.all(Array.from(allUuids).map(async uuid => {
const item = await fromUuid(uuid);
item.checked = checked.has(uuid);
return item;
}));

return foundry.utils.mergeObject(super.getData(), {
choices: {
max: maxChoices,
current: checked.size,
full: checked.size >= maxChoices
},
items
});
}

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

/** @inheritdoc */
activateListeners(html) {
super.activateListeners(html);
html.find("a[data-uuid]").click(this._onClickFeature.bind(this));
}

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

/** @inheritdoc */
_onChangeInput(event) {
this._calculateCurrent();
}

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

/**
* Adjust the currently selected number to reflect for and enable/disable inputs as necessary.
*/
_calculateCurrent() {
// Recalculate the currently selected number
const current = Array.from(this.form.querySelectorAll("input")).reduce((current, i) => {
return ( (i.type === "hidden") || i.checked ) ? current + 1 : current;
}, 0);
this.form.dataset.current = current;

// Enabled/disabled non-selected checkboxes
const checkboxes = this.form.querySelectorAll("input[type='checkbox']");
if ( current >= parseInt(this.form.dataset.max) ) {
checkboxes.forEach(c => c.disabled = !c.checked);
} else {
checkboxes.forEach(c => c.disabled = false);
}

// Update the current value listed in the UI
this.form.querySelector(".current").innerText = current;
}

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

/**
* Handle clicking on a feature during item grant to preview the feature.
* @param {MouseEvent} event The triggering event.
* @protected
*/
async _onClickFeature(event) {
event.preventDefault();
const uuid = event.currentTarget.dataset.uuid;
const item = await fromUuid(uuid);
item?.sheet.render(true);
}

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

/**
* 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.
* @private
*/
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: this.prepareConfigurationUpdate({
// [this.options.dropKeyPath]: items.filter(uuid => uuid !== uuidToDelete)
// }) };
// await this.advancement.update(updates);
// this.render();
}

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

/** @inheritdoc */
async _onDrop(event) {
if ( parseInt(this.form.dataset.current) >= parseInt(this.form.dataset.max) ) return false;

// 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 = Array.from(this.form.querySelectorAll("input")).map(i => i.name);

// Check to ensure the dropped item doesn't already exist on the actor

// If the item already exists, simply check its checkbox
if ( existingItems.includes(item.uuid) ) {
const existingInput = this.form.querySelector(`input[name='${item.uuid}']`);
existingInput.checked = true;
this._calculateCurrent();
return false;
}

// TODO: Add a new input
}

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

/** @inheritdoc */
async _updateObject(event, formData) {
const retainedData = this.retainedData?.items.reduce((obj, i) => {
obj[foundry.utils.getProperty(i, "flags.dnd5e.sourceId")] = i;
return obj;
}, {});
await this.advancement.apply(this.level, formData, retainedData);
}

}
2 changes: 1 addition & 1 deletion templates/advancement/item-choice-config.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
</ol>
</ol>

<p class="hint centered">{{localize "DND5E.AdvancementItemChoiceDropHint"}}</p>
<p class="hint centered">{{localize "DND5E.AdvancementItemChoiceConfigDropHint"}}</p>
</div>
</div>

Expand Down
29 changes: 29 additions & 0 deletions templates/advancement/item-choice-flow.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<form id="{{appId}}" data-level="{{level}}" data-id="{{advancement.id}}" data-type="{{type}}"
data-current="{{choices.current}}" data-max="{{choices.max}}">
<h3>{{{title}}}</h3>

<p>Chosen: <span class="current">{{choices.current}}</span> of {{choices.max}}</p>

<div class="drop-target">
{{#each items}}
<div class="item-name flexrow">
<div class="item-image" style="background-image: url({{this.img}});"></div>
<label class="flexrow">
<h4>
<a data-uuid="{{uuid}}">{{this.name}}</a>
</h4>
{{#if this.dropped}}
<a class="item-control item-delete" title="{{localize DND5E.ItemDelete}}"><i class="fas fa-trash"></i></a>
<input type="hidden" name="{{this.uuid}}" value="true" data-dtype="Boolean">
{{else}}
<input type="checkbox" name="{{this.uuid}}" {{checked this.checked}} {{disabled @root.choices.full}}>
{{/if}}
</label>
</div>
{{/each}}

{{#if advancement.data.configuration.allowDrops}}
<p class="hint centered">{{localize "DND5E.AdvancementItemChoiceFlowDropHint"}}</p>
{{/if}}
</div>
</form>

0 comments on commit 390eb95

Please sign in to comment.