Skip to content

Commit

Permalink
[#893] Add summoning to item activation workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
arbron committed Mar 1, 2024
1 parent d5044ba commit 6989d1e
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 38 deletions.
11 changes: 9 additions & 2 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1384,7 +1384,9 @@
"Action": {
"Add": "Add Profile",
"Configure": "Configure Summons",
"Remove": "Remove Profile"
"Place": "Place Summons",
"Remove": "Remove Profile",
"Summon": "Summon"
},
"ArmorClass": {
"Label": "Bonus Armor Class",
Expand Down Expand Up @@ -1420,8 +1422,13 @@
}
},
"Profile": {
"Label": "Summons Profiles",
"Label": "Summons Profile",
"LabelPl": "Summons Profiles",
"Empty": "Click above to add a profile or drop an creature to summon here."
},
"Prompt": {
"Label": "Summon Prompt",
"Hint": "Disable the summoing prompt when item is used. Players will still be able to summon from the chat card."
}
},
"DND5E.Supply": "Supply",
Expand Down
48 changes: 38 additions & 10 deletions module/applications/item/ability-use-dialog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,33 @@ export default class AbilityUseDialog extends Dialog {
/* Rendering */
/* -------------------------------------------- */

/**
* Configuration options for displaying the ability use dialog.
*
* @typedef {object} AbilityUseDialogOptions
* @property {object} [button]
* @property {string} [button.icon] Icon used for the activation button.
* @property {string} [button.label] Label used for the activation button.
*/

/**
* A constructor function which displays the Spell Cast Dialog app for a given Actor and Item.
* Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
* @param {Item5e} item Item being used.
* @param {ItemUseConfiguration} config The ability use configuration's values.
* @returns {Promise} Promise that is resolved when the use dialog is acted upon.
* @param {Item5e} item Item being used.
* @param {ItemUseConfiguration} config The ability use configuration's values.
* @param {AbilityUseDialogOptions} [options={}] Additional options for displaying the dialog.
* @returns {Promise} Promise that is resolved when the use dialog is acted upon.
*/
static async create(item, config) {
static async create(item, config, options={}) {
if ( !item.isOwned ) throw new Error("You cannot display an ability usage dialog for an unowned item");
config ??= item._getUsageConfig();
const slotOptions = config.consumeSpellSlot ? this._createSpellSlotOptions(item.actor, item.system.level) : [];
const resourceOptions = this._createResourceOptions(item);

const data = {
item,
...config,
slotOptions,
resourceOptions,
slotOptions: config.consumeSpellSlot ? this._createSpellSlotOptions(item.actor, item.system.level) : [],
summoningOptions: this._createSummoningOptions(item),
resourceOptions: this._createResourceOptions(item),
scaling: item.usageScaling,
note: this._getAbilityUseNote(item, config),
title: game.i18n.format("DND5E.AbilityUseHint", {
Expand All @@ -68,8 +77,8 @@ export default class AbilityUseDialog extends Dialog {
content: html,
buttons: {
use: {
icon: `<i class="fas ${isSpell ? "fa-magic" : "fa-fist-raised"}"></i>`,
label: label,
icon: options.button?.icon ?? `<i class="fas ${isSpell ? "fa-magic" : "fa-fist-raised"}"></i>`,
label: options.button?.label ?? label,
callback: html => {
const fd = new FormDataExtended(html[0].querySelector("form"));
resolve(fd.object);
Expand Down Expand Up @@ -136,6 +145,25 @@ export default class AbilityUseDialog extends Dialog {

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

/**
* Create an array of summoning profiles.
* @param {Item5e} item The item.
* @returns {object|null} Array of select options.
*/
static _createSummoningOptions(item) {
const profiles = item.system.summons.profiles;
if ( profiles.length <= 1 ) return null;
const options = {};
for ( const profile of profiles ) {
const doc = profile.uuid ? fromUuidSync(profile.uuid) : null;
if ( profile.uuid && !doc ) continue;
options[profile._id] = profile.name ?? doc?.name ?? "—";
}
return options;
}

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

/**
* Configure resource consumption options for a select.
* @param {Item5e} item The item.
Expand Down
18 changes: 16 additions & 2 deletions module/data/item/templates/action.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ItemDataModel } from "../../abstract.mjs";
import { FormulaField } from "../../fields.mjs";

const {
ArrayField, BooleanField, DocumentIdField, IntegerSortField, NumberField, SchemaField, StringField
ArrayField, BooleanField, DocumentIdField, NumberField, SchemaField, StringField
} = foundry.data.fields;

/**
Expand Down Expand Up @@ -41,6 +41,7 @@ const {
* @property {boolean} summons.match.proficiency Match proficiency on summoned actor to the summoner.
* @property {boolean} summons.match.saves Match the save DC on summoned actor's abilities to the summoner.
* @property {SummonsProfile[]} summons.profiles Information on creatures that can be summoned.
* @property {boolean} summons.prompt Should the player be prompted to place the summons?
* @mixin
*/
export default class ActionTemplate extends ItemDataModel {
Expand Down Expand Up @@ -86,7 +87,10 @@ export default class ActionTemplate extends ItemDataModel {
count: new NumberField({integer: true, min: 1}),
name: new StringField(),
uuid: new StringField()
}))
})),
prompt: new BooleanField({
initial: true, label: "DND5E.Summoning.Prompt.Label", hint: "DND5E.Summoning.Prompt.Hint"
})
})
};
}
Expand Down Expand Up @@ -271,6 +275,16 @@ export default class ActionTemplate extends ItemDataModel {

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

/**
* Does this Item implement summoning as part of its usage?
* @type {boolean}
*/
get hasSummoning() {
return (this.actionType === "summ") && this.summons.profiles.length;
}

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

/**
* Does the Item provide an amount of healing instead of conventional damage?
* @type {boolean}
Expand Down
1 change: 1 addition & 0 deletions module/data/item/templates/activated-effect.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { FormulaField } from "../../fields.mjs";
* @property {number} target.width Width of line when line type is selected.
* @property {string} target.units Units used for value and width as defined in `DND5E.distanceUnits`.
* @property {string} target.type Targeting mode as defined in `DND5E.targetTypes`.
* @property {boolean} target.prompt Should the player be prompted to place the template?
* @property {object} range Effect's range.
* @property {number} range.value Regular targeting distance for item's effect.
* @property {number} range.long Maximum targeting distance for features that have a separate long range.
Expand Down
97 changes: 79 additions & 18 deletions module/documents/item.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -879,10 +879,12 @@ export default class Item5e extends SystemDocumentMixin(Item) {
*
* @typedef {object} ItemUseConfiguration
* @property {boolean} createMeasuredTemplate Should this item create a template?
* @property {boolean} createSummons Should this item create a summoned creature?
* @property {boolean} consumeResource Should this item consume a (non-ammo) resource?
* @property {boolean} consumeSpellSlot Should this item (a spell) consume a spell slot?
* @property {boolean} consumeUsage Should this item consume its limited uses or recharge?
* @property {string|number|null} slotLevel The spell slot type or level to consume by default.
* @property {string|null} summonsProfile ID of the summoning profile to use.
* @property {number|null} resourceAmount The amount to consume by default when scaling with consumption.
*/

Expand Down Expand Up @@ -969,6 +971,11 @@ export default class Item5e extends SystemDocumentMixin(Item) {
}
if ( item.type === "spell" ) foundry.utils.mergeObject(options.flags, {"dnd5e.use.spellLevel": item.system.level});

// Store selected summons type in flag
if ( config.createSummons && config.summonsProfile ) {
foundry.utils.setProperty(options.flags, "dnd5e.use.summonsProfile", config.summonsProfile);
}

/**
* A hook event that fires before an item's resource consumption has been calculated.
* @function dnd5e.preItemUsageConsumption
Expand Down Expand Up @@ -1025,6 +1032,12 @@ export default class Item5e extends SystemDocumentMixin(Item) {
}
}

// Initiate summons creation
let summoned;
if ( config.createSummons ) {
console.log("TODO: Create Summons", config.summonsProfile);
}

/**
* A hook event that fires when an item is used, after the measured template has been created if one is needed.
* @function dnd5e.useItem
Expand All @@ -1033,8 +1046,9 @@ export default class Item5e extends SystemDocumentMixin(Item) {
* @param {ItemUseConfiguration} config Configuration data for the roll.
* @param {ItemUseOptions} options Additional options for configuring item usage.
* @param {MeasuredTemplateDocument[]|null} templates The measured templates if they were created.
* @param {TokenDocument5e[]|null} summoned Summoned tokens if they were created.
*/
Hooks.callAll("dnd5e.useItem", item, config, options, templates ?? null);
Hooks.callAll("dnd5e.useItem", item, config, options, templates ?? null, summoned ?? null);

return cardData;
}
Expand All @@ -1044,15 +1058,17 @@ export default class Item5e extends SystemDocumentMixin(Item) {
* @returns {ItemUseConfiguration} Configuration data for the roll.
*/
_getUsageConfig() {
const { consume, uses, target, level, preparation } = this.system;
const { consume, uses, summons, target, level, preparation } = this.system;

const config = {
consumeSpellSlot: null,
slotLevel: null,
consumeUsage: null,
consumeResource: null,
resourceAmount: null,
createMeasuredTemplate: null
createMeasuredTemplate: null,
createSummons: null,
summonsProfile: null
};

const scaling = this.usageScaling;
Expand All @@ -1069,6 +1085,10 @@ export default class Item5e extends SystemDocumentMixin(Item) {
if ( consume.target === this.id ) config.consumeUsage = null;
}
if ( game.user.can("TEMPLATE_CREATE") && this.hasAreaTarget ) config.createMeasuredTemplate = target.prompt;
if ( this.system.hasSummoning ) {
config.createSummons = summons.prompt;
config.summonsProfile = this.system.summons.profiles[0]._id;
}

return config;
}
Expand Down Expand Up @@ -1293,7 +1313,7 @@ export default class Item5e extends SystemDocumentMixin(Item) {
// Render the chat card template
const token = this.actor.token;
const hasButtons = this.hasAttack || this.hasDamage || this.isVersatile || this.hasSave || this.system.formula
|| this.hasAreaTarget || (this.type === "tool") || this.hasAbilityCheck;
|| this.hasAreaTarget || (this.type === "tool") || this.hasAbilityCheck || this.system.hasSummoning;
const templateData = {
hasButtons,
actor: this.actor,
Expand Down Expand Up @@ -1909,6 +1929,13 @@ export default class Item5e extends SystemDocumentMixin(Item) {
// Handle different actions
let targets;
switch ( action ) {
case "abilityCheck":
targets = this._getChatCardTargets(card);
for ( let token of targets ) {
const speaker = ChatMessage.getSpeaker({scene: canvas.scene, token: token.document});
await token.actor.rollAbilityTest(button.dataset.ability, { event, speaker });
}
break;
case "applyEffect":
const effect = await fromUuid(button.closest("[data-uuid]")?.dataset.uuid);
let warn = false;
Expand All @@ -1932,19 +1959,8 @@ export default class Item5e extends SystemDocumentMixin(Item) {
});
break;
case "formula":
await item.rollFormula({event, spellLevel}); break;
case "save":
targets = this._getChatCardTargets(card);
for ( let token of targets ) {
const dc = parseInt(button.dataset.dc);
const speaker = ChatMessage.getSpeaker({scene: canvas.scene, token: token.document});
await token.actor.rollAbilitySave(button.dataset.ability, {
event, speaker, targetValue: Number.isFinite(dc) ? dc : undefined
});
}
await item.rollFormula({event, spellLevel});
break;
case "toolCheck":
await item.rollToolCheck({event}); break;
case "placeTemplate":
try {
await dnd5e.canvas.AbilityTemplate.fromItem(item, {"flags.dnd5e.spellLevel": spellLevel})?.drawPreview();
Expand All @@ -1956,13 +1972,22 @@ export default class Item5e extends SystemDocumentMixin(Item) {
});
}
break;
case "abilityCheck":
case "save":
targets = this._getChatCardTargets(card);
for ( let token of targets ) {
const dc = parseInt(button.dataset.dc);
const speaker = ChatMessage.getSpeaker({scene: canvas.scene, token: token.document});
await token.actor.rollAbilityTest(button.dataset.ability, { event, speaker });
await token.actor.rollAbilitySave(button.dataset.ability, {
event, speaker, targetValue: Number.isFinite(dc) ? dc : undefined
});
}
break;
case "summon":
await this._onChatCardSummon(message, item);
break;
case "toolCheck":
await item.rollToolCheck({event});
break;
}

} catch(err) {
Expand Down Expand Up @@ -2005,6 +2030,42 @@ export default class Item5e extends SystemDocumentMixin(Item) {

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

/**
* Handle summoning from a chat card.
* @param {ChatMessage5e} message The message that was clicked.
* @param {Item5e} item The item from which to summon.
*/
static async _onChatCardSummon(message, item) {
let summonsProfile = message.getFlag("dnd5e", "use.summonsProfile");

// No profile specified and only one profile on item, use that one
if ( !summonsProfile && (item.system.summons.profiles.length === 1) ) {
summonsProfile = item.system.summons.profiles[0]._id;
}

// Otherwise show the item use dialog to get the profile
else if ( !summonsProfile ) {
const config = await AbilityUseDialog.create(item, {
consumeResource: null,
consumeSpellSlot: null,
consumeUsage: null,
createMeasuredTemplate: null,
createSummons: true
}, {
button: {
icon: '<i class="fa-solid fa-spaghetti-monster-flying"></i>',
label: game.i18n.localize("DND5E.Summoning.Action.Summon")
}
});
if ( !config?.summonsProfile ) return;
summonsProfile = config.summonsProfile;
}

console.log("TODO: Create Summons", summonsProfile);
}

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

/**
* Handle toggling the visibility of chat card content when the name is clicked
* @param {Event} event The originating click event
Expand Down
Loading

0 comments on commit 6989d1e

Please sign in to comment.