diff --git a/lang/en.json b/lang/en.json index 86002522d6..e3e7d080f2 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1480,6 +1480,10 @@ "Label": "Creature Changes", "Hint": "Changes that will be made to the creature being summoned. Any @ references used in the following formulas will be based on the summoner's stats." }, + "CreatureTypes": { + "Label": "Creature Types", + "Hint": "Summoned creature will be changed to this type. If more than one type is selected, then player will be able to choose from these types when summoning." + }, "DisplayName": "Display Name", "DropHint": "Drop creature here", "ItemChanges": { diff --git a/less/v1/items.less b/less/v1/items.less index 277a6477a1..b0ea1850d1 100644 --- a/less/v1/items.less +++ b/less/v1/items.less @@ -545,6 +545,8 @@ /* ----------------------------------------- */ .dnd5e.summoning-config { + max-block-size: 90vh; + .unbutton { width: unset; border: none; @@ -596,4 +598,8 @@ padding-inline: 4px; } } + + multi-select .tags .tag { + cursor: pointer; + } } diff --git a/module/applications/item/ability-use-dialog.mjs b/module/applications/item/ability-use-dialog.mjs index 000e6daf53..d6627c5844 100644 --- a/module/applications/item/ability-use-dialog.mjs +++ b/module/applications/item/ability-use-dialog.mjs @@ -177,17 +177,22 @@ export default class AbilityUseDialog extends Dialog { /** * Create an array of summoning profiles. * @param {Item5e} item The item. - * @returns {object|null} Array of select options. + * @returns {{ profiles: object, creatureTypes: object }|null} Array of select options. */ static _createSummoningOptions(item) { - const profiles = item.system.summons?.profiles ?? []; - if ( profiles.length <= 1 ) return null; + const summons = item.system.summons; + if ( !summons?.profiles.length ) return null; const options = {}; - for ( const profile of profiles ) { + if ( summons.profiles.length > 1 ) options.profiles = summons.profiles.reduce((obj, profile) => { const doc = profile.uuid ? fromUuidSync(profile.uuid) : null; - if ( profile.uuid && !doc ) continue; - options[profile._id] = profile.name ? profile.name : (doc?.name ?? "—"); - } + if ( !profile.uuid || doc ) obj[profile._id] = profile.name ? profile.name : (doc?.name ?? "—"); + return obj; + }, {}); + else options.profile = summons.profiles[0]._id; + if ( summons.creatureTypes.size > 1 ) options.creatureTypes = summons.creatureTypes.reduce((obj, k) => { + obj[k] = CONFIG.DND5E.creatureTypes[k].label; + return obj; + }, {}); return options; } diff --git a/module/applications/item/summoning-config.mjs b/module/applications/item/summoning-config.mjs index 8c554c30e5..1e15b8837a 100644 --- a/module/applications/item/summoning-config.mjs +++ b/module/applications/item/summoning-config.mjs @@ -44,6 +44,7 @@ export default class SummoningConfig extends DocumentSheet { /** @inheritDoc */ async getData(options={}) { const context = await super.getData(options); + context.CONFIG = CONFIG.DND5E; context.profiles = this.profiles.map(p => { const profile = { id: p._id, ...p }; if ( p.uuid ) profile.document = fromUuidSync(p.uuid); @@ -52,6 +53,10 @@ export default class SummoningConfig extends DocumentSheet { (lhs.name || lhs.document?.name || "").localeCompare(rhs.name || rhs.document?.name || "", game.i18n.lang) ); context.summons = this.document.system.summons; + context.creatureTypes = Object.entries(CONFIG.DND5E.creatureTypes).reduce((obj, [k, c]) => { + obj[k] = { label: c.label, selected: context.summons?.creatureTypes.has(k) ? "selected" : "" }; + return obj; + }, {}); return context; } diff --git a/module/data/item/fields/summons-field.mjs b/module/data/item/fields/summons-field.mjs index 8eee7c94e8..a1d0ad2fe0 100644 --- a/module/data/item/fields/summons-field.mjs +++ b/module/data/item/fields/summons-field.mjs @@ -2,7 +2,7 @@ import TokenPlacement from "../../../canvas/token-placement.mjs"; import { FormulaField } from "../../fields.mjs"; const { - ArrayField, BooleanField, DocumentIdField, NumberField, SchemaField, StringField + ArrayField, BooleanField, DocumentIdField, NumberField, SchemaField, SetField, StringField } = foundry.data.fields; /** @@ -35,6 +35,7 @@ export default class SummonsField extends foundry.data.fields.EmbeddedDataField * @property {string} bonuses.attackDamage Formula for bonus added to damage for attacks. * @property {string} bonuses.saveDamage Formula for bonus added to damage for saving throws. * @property {string} bonuses.healing Formula for bonus added to healing. + * @property {Set} creatureTypes Set of creature types that will be set on summoned creature. * @property {object} match * @property {boolean} match.attacks Match the to hit values on summoned actor's attack to the summoner. * @property {boolean} match.proficiency Match proficiency on summoned actor to the summoner. @@ -63,6 +64,9 @@ export class SummonsData extends foundry.abstract.DataModel { label: "DND5E.Summoning.Bonuses.Healing.Label", hint: "DND5E.Summoning.Bonuses.Healing.Hint" }) }), + creatureTypes: new SetField(new StringField(), { + label: "DND5E.Summoning.CreatureTypes.Label", hint: "DND5E.Summoning.CreatureTypes.Hint" + }), match: new SchemaField({ attacks: new BooleanField({ label: "DND5E.Summoning.Match.Attacks.Label", hint: "DND5E.Summoning.Match.Attacks.Hint" @@ -116,11 +120,19 @@ export class SummonsData extends foundry.abstract.DataModel { /* Summoning */ /* -------------------------------------------- */ + /** + * Additional options that might modify summoning behavior. + * + * @typedef {object} SummoningOptions + * @property {string} creatureType Selected creature type if multiple are available. + */ + /** * Process for summoning actor to the scene. - * @param {string} profileId ID of the summoning profile to use. + * @param {string} profileId ID of the summoning profile to use. + * @param {object} [options={}] Additional summoning options. */ - async summon(profileId) { + async summon(profileId, options={}) { if ( !this.canSummon || !canvas.scene ) return; const profile = this.profiles.find(p => p._id === profileId); @@ -132,11 +144,12 @@ export class SummonsData extends foundry.abstract.DataModel { * A hook event that fires before summoning is performed. * @function dnd5e.preSummon * @memberof hookEvents - * @param {Item5e} item The item that is performing the summoning. - * @param {SummonsProfile} profile Profile used for summoning. - * @returns {boolean} Explicitly return `false` to prevent summoning. + * @param {Item5e} item The item that is performing the summoning. + * @param {SummonsProfile} profile Profile used for summoning. + * @param {SummoningOptions} options Additional summoning options. + * @returns {boolean} Explicitly return `false` to prevent summoning. */ - if ( Hooks.call("dnd5e.preSummon", this.item, profile) === false ) return; + if ( Hooks.call("dnd5e.preSummon", this.item, profile, options) === false ) return; // Fetch the actor that will be summoned const actor = await this.fetchActor(profile.uuid); @@ -159,7 +172,7 @@ export class SummonsData extends foundry.abstract.DataModel { actor, placement, tokenUpdates: {}, - actorUpdates: await this.getChanges(actor, profile) + actorUpdates: await this.getChanges(actor, profile, options) }; /** @@ -167,12 +180,13 @@ export class SummonsData extends foundry.abstract.DataModel { * the final token data is constructed. * @function dnd5e.preSummonToken * @memberof hookEvents - * @param {Item5e} item The item that is performing the summoning. - * @param {SummonsProfile} profile Profile used for summoning. - * @param {TokenUpdateData} config Configuration for creating a modified token. - * @returns {boolean} Explicitly return `false` to prevent this token from being summoned. + * @param {Item5e} item The item that is performing the summoning. + * @param {SummonsProfile} profile Profile used for summoning. + * @param {TokenUpdateData} config Configuration for creating a modified token. + * @param {SummoningOptions} options Additional summoning options. + * @returns {boolean} Explicitly return `false` to prevent this token from being summoned. */ - if ( Hooks.call("dnd5e.preSummonToken", this.item, profile, tokenUpdateData) === false ) continue; + if ( Hooks.call("dnd5e.preSummonToken", this.item, profile, tokenUpdateData, options) === false ) continue; // Create a token document and apply updates const tokenData = await this.getTokenData(tokenUpdateData); @@ -181,11 +195,12 @@ export class SummonsData extends foundry.abstract.DataModel { * A hook event that fires after token creation data is prepared, but before summoning occurs. * @function dnd5e.summonToken * @memberof hookEvents - * @param {Item5e} item The item that is performing the summoning. - * @param {SummonsProfile} profile Profile used for summoning. - * @param {object} tokenData Data for creating a token. + * @param {Item5e} item The item that is performing the summoning. + * @param {SummonsProfile} profile Profile used for summoning. + * @param {object} tokenData Data for creating a token. + * @param {SummoningOptions} options Additional summoning options. */ - Hooks.callAll("dnd5e.summonToken", this.item, profile, tokenData); + Hooks.callAll("dnd5e.summonToken", this.item, profile, tokenData, options); tokensData.push(tokenData); } @@ -199,11 +214,12 @@ export class SummonsData extends foundry.abstract.DataModel { * A hook event that fires when summoning is complete. * @function dnd5e.postSummon * @memberof hookEvents - * @param {Item5e} item The item that is performing the summoning. - * @param {SummonsProfile} profile Profile used for summoning. - * @param {Token5e[]} tokens Tokens that have been created. + * @param {Item5e} item The item that is performing the summoning. + * @param {SummonsProfile} profile Profile used for summoning. + * @param {Token5e[]} tokens Tokens that have been created. + * @param {SummoningOptions} options Additional summoning options. */ - Hooks.callAll("dnd5e.postSummon", this.item, profile, createdTokens); + Hooks.callAll("dnd5e.postSummon", this.item, profile, createdTokens, options); } /* -------------------------------------------- */ @@ -237,11 +253,12 @@ export class SummonsData extends foundry.abstract.DataModel { /** * Prepare the updates to apply to the summoned actor. - * @param {Actor5e} actor Actor that will be modified. - * @param {SummonsProfile} profile Summoning profile used to summon the actor. - * @returns {object} Changes that will be applied to the actor & its items. + * @param {Actor5e} actor Actor that will be modified. + * @param {SummonsProfile} profile Summoning profile used to summon the actor. + * @param {SummoningOptions} options Additional summoning options. + * @returns {object} Changes that will be applied to the actor & its items. */ - async getChanges(actor, profile) { + async getChanges(actor, profile, options) { const updates = { effects: [], items: [] }; const rollData = this.item.getRollData(); const prof = rollData.attributes?.prof ?? 0; @@ -306,6 +323,16 @@ export class SummonsData extends foundry.abstract.DataModel { } } + // Change creature type + if ( this.creatureTypes.size ) { + const type = this.creatureTypes.has(options.creatureType) ? options.creatureType : this.creatureTypes.first(); + if ( actor.system.details?.race instanceof Item ) { + updates.items.push({ _id: actor.system.details.race.id, "system.type.value": type }); + } else { + updates["system.details.type.value"] = type; + } + } + const attackDamageBonus = Roll.replaceFormulaData(this.bonuses.attackDamage, rollData); const saveDamageBonus = Roll.replaceFormulaData(this.bonuses.saveDamage, rollData); const healingBonus = Roll.replaceFormulaData(this.bonuses.healing, rollData); diff --git a/module/documents/item.mjs b/module/documents/item.mjs index e5d49b8fe6..7817e9b34f 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -1057,7 +1057,7 @@ export default class Item5e extends SystemDocumentMixin(Item) { let summoned; if ( config.createSummons ) { try { - summoned = await item.system.summons.summon(config.summonsProfile); + summoned = await item.system.summons.summon(config.summonsProfile, config.summonsOptions); } catch(err) { Hooks.onError("Item5e#use", err, { log: "error", notify: "error" }); } @@ -2137,15 +2137,19 @@ export default class Item5e extends SystemDocumentMixin(Item) { */ static async _onChatCardSummon(message, item) { let summonsProfile; + let summonsOptions = {}; + let needsConfiguration = false; // No profile specified and only one profile on item, use that one - if ( item.system.summons.profiles.length === 1 ) { - summonsProfile = item.system.summons.profiles[0]._id; - } + if ( item.system.summons.profiles.length === 1 ) summonsProfile = item.system.summons.profiles[0]._id; + else needsConfiguration = true; - // Otherwise show the item use dialog to get the profile - else { - const config = await AbilityUseDialog.create(item, { + // More than one creature type requires configuration + if ( item.system.summons.creatureTypes.size > 1 ) needsConfiguration = true; + + // Show the item use dialog to get the profile and other options + if ( needsConfiguration ) { + let config = await AbilityUseDialog.create(item, { beginConcentrating: null, consumeResource: null, consumeSpellSlot: null, @@ -2160,11 +2164,13 @@ export default class Item5e extends SystemDocumentMixin(Item) { disableScaling: true }); if ( !config?.summonsProfile ) return; + config = foundry.utils.expandObject(config); summonsProfile = config.summonsProfile; + summonsOptions = config.summonsOptions; } try { - await item.system.summons.summon(summonsProfile); + await item.system.summons.summon(summonsProfile, summonsOptions); } catch(err) { Hooks.onError("Item5e#_onChatCardSummon", err, { log: "error", notify: "error" }); } diff --git a/templates/apps/ability-use.hbs b/templates/apps/ability-use.hbs index 05fa63ef8c..b98398a34c 100644 --- a/templates/apps/ability-use.hbs +++ b/templates/apps/ability-use.hbs @@ -91,15 +91,26 @@ {{ localize "DND5E.Summoning.Action.Place" }} - {{#if summoningOptions}} + {{#if summoningOptions.profiles}}
{{else}} - + {{/if}} + + {{#if summoningOptions.creatureTypes}} +
+ +
+ +
+
+ {{/if}} {{/if}} diff --git a/templates/apps/summoning-config.hbs b/templates/apps/summoning-config.hbs index fc7037d353..a29e48695d 100644 --- a/templates/apps/summoning-config.hbs +++ b/templates/apps/summoning-config.hbs @@ -51,6 +51,15 @@

{{ localize "DND5E.Summoning.Bonuses.HitPoints.Hint" }}

+
+ + + {{#each creatureTypes}} + + {{/each}} + +

{{ localize "DND5E.Summoning.CreatureTypes.Hint" }}

+

{{ localize "DND5E.Summoning.ItemChanges.Label" }}

{{ localize "DND5E.Summoning.ItemChanges.Hint" }}