/** * A specialized Dialog subclass for ability usage * @type {Dialog} */ export default class AbilityUseDialog extends Dialog { constructor(item, dialogData={}, options={}) { super(dialogData, options); this.options.classes = ["sw5e", "dialog"]; /** * Store a reference to the Item entity being used * @type {Item5e} */ this.item = item; } /* -------------------------------------------- */ /* Rendering */ /* -------------------------------------------- */ /** * A constructor function which displays the Power 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 * @return {Promise} */ static async create(item) { if ( !item.isOwned ) throw new Error("You cannot display an ability usage dialog for an unowned item"); // Prepare data const actorData = item.actor.data.data; const itemData = item.data.data; const uses = itemData.uses || {}; const quantity = itemData.quantity || 0; const recharge = itemData.recharge || {}; const recharges = !!recharge.value; const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0; // Prepare dialog form data const data = { item: item.data, title: game.i18n.format("SW5E.AbilityUseHint", {type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), name: item.name}), note: this._getAbilityUseNote(item.data, uses, recharge), consumePowerSlot: false, consumeRecharge: recharges, consumeResource: !!itemData.consume.target, consumeUses: uses.per && (uses.max > 0), canUse: recharges ? recharge.charged : sufficientUses, createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget, errors: [] }; if ( item.data.type === "power" ) this._getPowerData(actorData, itemData, data); // Render the ability usage template const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data); // Create the Dialog and return data as a Promise const icon = data.isPower ? "fa-magic" : "fa-fist-raised"; const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use")); return new Promise((resolve) => { const dlg = new this(item, { title: `${item.name}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`, content: html, buttons: { use: { icon: ``, label: label, callback: html => { const fd = new FormDataExtended(html[0].querySelector("form")); resolve(fd.toObject()); } } }, default: "use", close: () => resolve(null) }); dlg.render(true); }); } /* -------------------------------------------- */ /* Helpers */ /* -------------------------------------------- */ /** * Get dialog data related to limited power slots * @private */ static _getPowerData(actorData, itemData, data) { // Determine whether the power may be up-cast const lvl = itemData.level; const consumePowerSlot = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode); // If can't upcast, return early and don't bother calculating available power slots if (!consumePowerSlot) { mergeObject(data, { isPower: true, consumePowerSlot }); return; } // Determine the levels which are feasible let lmax = 0; let points; let powerType; switch (itemData.school){ case "lgt": case "uni": case "drk": { powerType = "force" points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp; break; } case "tec": { powerType = "tech" points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp; break; } } // eliminate point usage for innate casters if (actorData.attributes.powercasting === 'innate') points = 999; let powerLevels if (powerType === "force"){ powerLevels = Array.fromRange(10).reduce((arr, i) => { if ( i < lvl ) return arr; const label = CONFIG.SW5E.powerLevels[i]; const l = actorData.powers["power"+i] || {fmax: 0, foverride: null}; let max = parseInt(l.foverride || l.fmax || 0); let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max); if ( max > 0 ) lmax = i; if ((max > 0) && (slots > 0) && (points > i)){ arr.push({ level: i, label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label, canCast: max > 0, hasSlots: slots > 0 }); } return arr; }, []).filter(sl => sl.level <= lmax); }else if (powerType === "tech"){ powerLevels = Array.fromRange(10).reduce((arr, i) => { if ( i < lvl ) return arr; const label = CONFIG.SW5E.powerLevels[i]; const l = actorData.powers["power"+i] || {tmax: 0, toverride: null}; let max = parseInt(l.override || l.tmax || 0); let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max); if ( max > 0 ) lmax = i; if ((max > 0) && (slots > 0) && (points > i)){ arr.push({ level: i, label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label, canCast: max > 0, hasSlots: slots > 0 }); } return arr; }, []).filter(sl => sl.level <= lmax); } const canCast = powerLevels.some(l => l.hasSlots); if ( !canCast ) data.errors.push(game.i18n.format("SW5E.PowerCastNoSlots", { level: CONFIG.SW5E.powerLevels[lvl], name: data.item.name })); // Merge power casting data return foundry.utils.mergeObject(data, { isPower: true, consumePowerSlot, powerLevels }); } /* -------------------------------------------- */ /** * Get the ability usage note that is displayed * @private */ static _getAbilityUseNote(item, uses, recharge) { // Zero quantity const quantity = item.data.quantity; if ( quantity <= 0 ) return game.i18n.localize("SW5E.AbilityUseUnavailableHint"); // Abilities which use Recharge if ( !!recharge.value ) { return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", { type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), }) } // Does not use any resource if ( !uses.per || !uses.max ) return ""; // Consumables if ( item.type === "consumable" ) { let str = "SW5E.AbilityUseNormalHint"; if ( uses.value > 1 ) str = "SW5E.AbilityUseConsumableChargeHint"; else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint"; else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint"; return game.i18n.format(str, { type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`), value: uses.value, quantity: item.data.quantity, max: uses.max, per: CONFIG.SW5E.limitedUsePeriods[uses.per] }); } // Other Items else { return game.i18n.format("SW5E.AbilityUseNormalHint", { type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), value: uses.value, max: uses.max, per: CONFIG.SW5E.limitedUsePeriods[uses.per] }); } } }