From 28ab1fb4041e6b6e49a374b44eb0566f3d9a9418 Mon Sep 17 00:00:00 2001 From: CK <31608392+unrealkakeman89@users.noreply.github.com> Date: Wed, 24 Jun 2020 14:23:02 -0400 Subject: [PATCH] Add files via upload --- module/apps/ability-use-dialog.js | 64 ++++++++++++++ module/apps/actor-flags.js | 112 ++++++++++++++++++++++++ module/apps/cast-dialog.js | 102 ++++++++++++++++++++++ module/apps/power-cast-dialog.js | 102 ++++++++++++++++++++++ module/apps/short-rest.js | 138 ++++++++++++++++++++++++++++++ module/apps/trait-selector.js | 88 +++++++++++++++++++ 6 files changed, 606 insertions(+) create mode 100644 module/apps/ability-use-dialog.js create mode 100644 module/apps/actor-flags.js create mode 100644 module/apps/cast-dialog.js create mode 100644 module/apps/power-cast-dialog.js create mode 100644 module/apps/short-rest.js create mode 100644 module/apps/trait-selector.js diff --git a/module/apps/ability-use-dialog.js b/module/apps/ability-use-dialog.js new file mode 100644 index 00000000..ec3510c8 --- /dev/null +++ b/module/apps/ability-use-dialog.js @@ -0,0 +1,64 @@ +/** + * A specialized Dialog subclass for ability usage + * @type {Dialog} + */ +export 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) { + + const uses = item.data.data.uses; + const recharge = item.data.data.recharge; + const recharges = !!recharge.value; + + // Render the ability usage template + const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", { + item: item.data, + canUse: recharges ? recharge.charged : uses.value > 0, + consume: true, + uses: uses, + recharges: !!recharge.value, + isCharged: recharge.charged, + hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget, + perLabel: CONFIG.SW5E.limitedUsePeriods[uses.per] + }); + + // Create the Dialog and return as a Promise + return new Promise((resolve) => { + let formData = null; + const dlg = new this(item, { + title: `${item.name}: Ability Configuration`, + content: html, + buttons: { + use: { + icon: '', + label: "Use Ability", + callback: html => formData = new FormData(html[0].querySelector("#ability-use-form")) + } + }, + default: "use", + close: () => resolve(formData) + }); + dlg.render(true); + }); + } +} diff --git a/module/apps/actor-flags.js b/module/apps/actor-flags.js new file mode 100644 index 00000000..1e35d0e7 --- /dev/null +++ b/module/apps/actor-flags.js @@ -0,0 +1,112 @@ +export class ActorSheetFlags extends BaseEntitySheet { + static get defaultOptions() { + const options = super.defaultOptions; + return mergeObject(options, { + id: "actor-flags", + classes: ["sw5e"], + template: "systems/sw5e/templates/apps/actor-flags.html", + width: 500, + closeOnSubmit: true + }); + } + + /* -------------------------------------------- */ + + /** + * Configure the title of the special traits selection window to include the Actor name + * @type {String} + */ + get title() { + return `${game.i18n.localize('SW5E.FlagsTitle')}: ${this.object.name}`; + } + + /* -------------------------------------------- */ + + /** + * Prepare data used to render the special Actor traits selection UI + * @return {Object} + */ + getData() { + const data = super.getData(); + data.actor = this.object; + data.flags = this._getFlags(); + data.bonuses = this._getBonuses(); + return data; + } + + /* -------------------------------------------- */ + + /** + * Prepare an object of flags data which groups flags by section + * Add some additional data for rendering + * @return {Object} + */ + _getFlags() { + const flags = {}; + for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) { + if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {}; + let flag = duplicate(v); + flag.type = v.type.name; + flag.isCheckbox = v.type === Boolean; + flag.isSelect = v.hasOwnProperty('choices'); + flag.value = this.entity.getFlag("sw5e", k); + flags[v.section][`flags.sw5e.${k}`] = flag; + } + return flags; + } + + /* -------------------------------------------- */ + + /** + * Get the bonuses fields and their localization strings + * @return {Array} + * @private + */ + _getBonuses() { + const bonuses = [ + {name: "data.bonuses.mwak.attack", label: "SW5E.BonusMWAttack"}, + {name: "data.bonuses.mwak.damage", label: "SW5E.BonusMWDamage"}, + {name: "data.bonuses.rwak.attack", label: "SW5E.BonusRWAttack"}, + {name: "data.bonuses.rwak.damage", label: "SW5E.BonusRWDamage"}, + {name: "data.bonuses.msak.attack", label: "SW5E.BonusMSAttack"}, + {name: "data.bonuses.msak.damage", label: "SW5E.BonusMSDamage"}, + {name: "data.bonuses.rsak.attack", label: "SW5E.BonusRSAttack"}, + {name: "data.bonuses.rsak.damage", label: "SW5E.BonusRSDamage"}, + {name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"}, + {name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"}, + {name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"}, + {name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"} + ]; + for ( let b of bonuses ) { + b.value = getProperty(this.object.data, b.name) || ""; + } + return bonuses; + } + + /* -------------------------------------------- */ + + /** + * Update the Actor using the configured flags + * Remove/unset any flags which are no longer configured + */ + async _updateObject(event, formData) { + const actor = this.object; + const updateData = expandObject(formData); + + // Unset any flags which are "false" + let unset = false; + const flags = updateData.flags.sw5e; + for ( let [k, v] of Object.entries(flags) ) { + if ( [undefined, null, "", false, 0].includes(v) ) { + delete flags[k]; + if ( hasProperty(actor.data.flags, `sw5e.${k}`) ) { + unset = true; + flags[`-=${k}`] = null; + } + } + } + + // Apply the changes + await actor.update(updateData, {diff: false}); + } +} diff --git a/module/apps/cast-dialog.js b/module/apps/cast-dialog.js new file mode 100644 index 00000000..e31bfb72 --- /dev/null +++ b/module/apps/cast-dialog.js @@ -0,0 +1,102 @@ +/** + * A specialized Dialog subclass for casting a cast item at a certain level + * @type {Dialog} + */ +export class CastDialog extends Dialog { + constructor(actor, item, dialogData={}, options={}) { + super(dialogData, options); + this.options.classes = ["sw5e", "dialog"]; + + /** + * Store a reference to the Actor entity which is casting the cast + * @type {Actor5e} + */ + this.actor = actor; + + /** + * Store a reference to the Item entity which is the cast being cast + * @type {Item5e} + */ + this.item = item; + } + + /* -------------------------------------------- */ + /* Rendering */ + /* -------------------------------------------- */ + + /** + * A constructor function which displays the Cast 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 {Actor5e} actor + * @param {Item5e} item + * @return {Promise} + */ + static async create(actor, item) { + const ad = actor.data.data; + const id = item.data.data; + + // Determine whether the cast may be upcast + const lvl = id.level; + const canUpcast = (lvl > 0) && CONFIG.SW5E.castUpcastModes.includes(id.preparation.mode); + + // Determine the levels which are feasible + let lmax = 0; + const castLevels = Array.fromRange(10).reduce((arr, i) => { + if ( i < lvl ) return arr; + const l = ad.casts["cast"+i] || {max: 0, override: null}; + let max = parseInt(l.override || l.max || 0); + let slots = Math.clamped(parseInt(l.value || 0), 0, max); + if ( max > 0 ) lmax = i; + arr.push({ + level: i, + label: i > 0 ? `${CONFIG.SW5E.castLevels[i]} (${slots} Slots)` : CONFIG.SW5E.castLevels[i], + canCast: canUpcast && (max > 0), + hasSlots: slots > 0 + }); + return arr; + }, []).filter(sl => sl.level <= lmax); + + const pact = ad.casts.pact; + if (pact.level >= lvl) { + // If this character has pact slots, present them as an option for + // casting the cast. + castLevels.push({ + level: 'pact', + label: game.i18n.localize('SW5E.CastLevelPact') + + ` (${game.i18n.localize('SW5E.Level')} ${pact.level}) ` + + `(${pact.value} ${game.i18n.localize('SW5E.Slots')})`, + canCast: canUpcast, + hasSlots: pact.value > 0 + }); + } + + const canCast = castLevels.some(l => l.hasSlots); + + // Render the Cast casting template + const html = await renderTemplate("systems/sw5e/templates/apps/cast-cast.html", { + item: item.data, + canCast: canCast, + canUpcast: canUpcast, + castLevels, + hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget + }); + + // Create the Dialog and return as a Promise + return new Promise((resolve, reject) => { + const dlg = new this(actor, item, { + title: `${item.name}: Cast Configuration`, + content: html, + buttons: { + cast: { + icon: '', + label: "Cast", + callback: html => resolve(new FormData(html[0].querySelector("#cast-config-form"))) + } + }, + default: "cast", + close: reject + }); + dlg.render(true); + }); + } +} diff --git a/module/apps/power-cast-dialog.js b/module/apps/power-cast-dialog.js new file mode 100644 index 00000000..9aeafad6 --- /dev/null +++ b/module/apps/power-cast-dialog.js @@ -0,0 +1,102 @@ +/** + * A specialized Dialog subclass for casting a power item at a certain level + * @type {Dialog} + */ +export class PowerCastDialog extends Dialog { + constructor(actor, item, dialogData={}, options={}) { + super(dialogData, options); + this.options.classes = ["sw5e", "dialog"]; + + /** + * Store a reference to the Actor entity which is casting the power + * @type {Actor5e} + */ + this.actor = actor; + + /** + * Store a reference to the Item entity which is the power being cast + * @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 {Actor5e} actor + * @param {Item5e} item + * @return {Promise} + */ + static async create(actor, item) { + const ad = actor.data.data; + const id = item.data.data; + + // Determine whether the power may be upcast + const lvl = id.level; + const canUpcast = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode); + + // Determine the levels which are feasible + let lmax = 0; + const powerLevels = Array.fromRange(10).reduce((arr, i) => { + if ( i < lvl ) return arr; + const l = ad.powers["power"+i] || {max: 0, override: null}; + let max = parseInt(l.override || l.max || 0); + let slots = Math.clamped(parseInt(l.value || 0), 0, max); + if ( max > 0 ) lmax = i; + arr.push({ + level: i, + label: i > 0 ? `${CONFIG.SW5E.powerLevels[i]} (${slots} Slots)` : CONFIG.SW5E.powerLevels[i], + canCast: canUpcast && (max > 0), + hasSlots: slots > 0 + }); + return arr; + }, []).filter(sl => sl.level <= lmax); + + const pact = ad.powers.pact; + if (pact.level >= lvl) { + // If this character has pact slots, present them as an option for + // casting the power. + powerLevels.push({ + level: 'pact', + label: game.i18n.localize('SW5E.PowerLevelPact') + + ` (${game.i18n.localize('SW5E.Level')} ${pact.level}) ` + + `(${pact.value} ${game.i18n.localize('SW5E.Slots')})`, + canCast: canUpcast, + hasSlots: pact.value > 0 + }); + } + + const canCast = powerLevels.some(l => l.hasSlots); + + // Render the Power casting template + const html = await renderTemplate("systems/sw5e/templates/apps/power-cast.html", { + item: item.data, + canCast: canCast, + canUpcast: canUpcast, + powerLevels, + hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget + }); + + // Create the Dialog and return as a Promise + return new Promise((resolve, reject) => { + const dlg = new this(actor, item, { + title: `${item.name}: Power Configuration`, + content: html, + buttons: { + cast: { + icon: '', + label: "Cast", + callback: html => resolve(new FormData(html[0].querySelector("#power-config-form"))) + } + }, + default: "cast", + close: reject + }); + dlg.render(true); + }); + } +} diff --git a/module/apps/short-rest.js b/module/apps/short-rest.js new file mode 100644 index 00000000..8d28ebbb --- /dev/null +++ b/module/apps/short-rest.js @@ -0,0 +1,138 @@ +/** + * A helper Dialog subclass for rolling Hit Dice on short rest + * @type {Dialog} + */ +export class ShortRestDialog extends Dialog { + constructor(actor, dialogData={}, options={}) { + super(dialogData, options); + + /** + * Store a reference to the Actor entity which is resting + * @type {Actor} + */ + this.actor = actor; + + /** + * Track the most recently used HD denomination for re-rendering the form + * @type {string} + */ + this._denom = null; + } + + /* -------------------------------------------- */ + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + template: "systems/sw5e/templates/apps/short-rest.html", + classes: ["sw5e", "dialog"] + }); + } + + /* -------------------------------------------- */ + + /** @override */ + getData() { + const data = super.getData(); + data.availableHD = this.actor.data.items.reduce((hd, item) => { + if ( item.type === "class" ) { + const d = item.data; + const denom = d.hitDice || "d6"; + const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0); + hd[denom] = denom in hd ? hd[denom] + available : available; + } + return hd; + }, {}); + data.canRoll = this.actor.data.data.attributes.hd > 0; + data.denomination = this._denom; + return data; + } + + /* -------------------------------------------- */ + + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + let btn = html.find("#roll-hd"); + btn.click(this._onRollHitDie.bind(this)); + super.activateListeners(html); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling a Hit Die as part of a Short Rest action + * @param {Event} event The triggering click event + * @private + */ + async _onRollHitDie(event) { + event.preventDefault(); + const btn = event.currentTarget; + this._denom = btn.form.hd.value; + await this.actor.rollHitDie(this._denom); + this.render(); + } + + /* -------------------------------------------- */ + + /** + * A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has + * been resolved. + * @param {Actor5e} actor + * @return {Promise} + */ + static async shortRestDialog({actor}={}) { + return new Promise(resolve => { + const dlg = new this(actor, { + title: "Short Rest", + buttons: { + rest: { + icon: '', + label: "Rest", + callback: () => resolve(true) + }, + cancel: { + icon: '', + label: "Cancel", + callback: () => resolve(false) + } + } + }); + dlg.render(true); + }); + } + + /* -------------------------------------------- */ + + /** + * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's + * workflow has been resolved. + * @param {Actor5e} actor + * @return {Promise} + */ + static async longRestDialog({actor}={}) { + const content = `
Take a long rest?
On a long rest you will recover hit points, half your maximum hit dice, + class resources, limited use item charges, and power slots.
`; + return new Promise((resolve, reject) => { + new Dialog({ + title: "Long Rest", + content: content, + buttons: { + rest: { + icon: '', + label: "Rest", + callback: resolve + }, + cancel: { + icon: '', + label: "Cancel", + callback: reject + }, + }, + default: 'rest', + close: reject + }, {classes: ["sw5e", "dialog"]}).render(true); + }); + } +} diff --git a/module/apps/trait-selector.js b/module/apps/trait-selector.js new file mode 100644 index 00000000..8a947004 --- /dev/null +++ b/module/apps/trait-selector.js @@ -0,0 +1,88 @@ +/** + * A specialized form used to select from a checklist of attributes, traits, or properties + * @extends {FormApplication} + */ +export class TraitSelector extends FormApplication { + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + id: "trait-selector", + classes: ["sw5e"], + title: "Actor Trait Selection", + template: "systems/sw5e/templates/apps/trait-selector.html", + width: 320, + height: "auto", + choices: {}, + allowCustom: true, + minimum: 0, + maximum: null + }); + } + + /* -------------------------------------------- */ + + /** + * Return a reference to the target attribute + * @type {String} + */ + get attribute() { + return this.options.name; + } + + /* -------------------------------------------- */ + + /** @override */ + getData() { + + // Get current values + let attr = getProperty(this.object.data, this.attribute) || {}; + attr.value = attr.value || []; + + // Populate choices + const choices = duplicate(this.options.choices); + for ( let [k, v] of Object.entries(choices) ) { + choices[k] = { + label: v, + chosen: attr ? attr.value.includes(k) : false + } + } + + // Return data + return { + allowCustom: this.options.allowCustom, + choices: choices, + custom: attr ? attr.custom : "" + } + } + + /* -------------------------------------------- */ + + /** @override */ + _updateObject(event, formData) { + const updateData = {}; + + // Obtain choices + const chosen = []; + for ( let [k, v] of Object.entries(formData) ) { + if ( (k !== "custom") && v ) chosen.push(k); + } + updateData[`${this.attribute}.value`] = chosen; + + // Validate the number chosen + if ( this.options.minimum && (chosen.length < this.options.minimum) ) { + return ui.notifications.error(`You must choose at least ${this.options.minimum} options`); + } + if ( this.options.maximum && (chosen.length > this.options.maximum) ) { + return ui.notifications.error(`You may choose no more than ${this.options.maximum} options`); + } + + // Include custom + if ( this.options.allowCustom ) { + updateData[`${this.attribute}.custom`] = formData.custom; + } + + // Update the object + this.object.update(updateData); + } +}