Add files via upload

This commit is contained in:
CK 2020-06-24 14:23:02 -04:00 committed by GitHub
parent 848158b9e8
commit 28ab1fb404
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 606 additions and 0 deletions

View file

@ -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: '<i class="fas fa-fist-raised"></i>',
label: "Use Ability",
callback: html => formData = new FormData(html[0].querySelector("#ability-use-form"))
}
},
default: "use",
close: () => resolve(formData)
});
dlg.render(true);
});
}
}

112
module/apps/actor-flags.js Normal file
View file

@ -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});
}
}

102
module/apps/cast-dialog.js Normal file
View file

@ -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: '<i class="fas fa-magic"></i>',
label: "Cast",
callback: html => resolve(new FormData(html[0].querySelector("#cast-config-form")))
}
},
default: "cast",
close: reject
});
dlg.render(true);
});
}
}

View file

@ -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: '<i class="fas fa-magic"></i>',
label: "Cast",
callback: html => resolve(new FormData(html[0].querySelector("#power-config-form")))
}
},
default: "cast",
close: reject
});
dlg.render(true);
});
}
}

138
module/apps/short-rest.js Normal file
View file

@ -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: '<i class="fas fa-bed"></i>',
label: "Rest",
callback: () => resolve(true)
},
cancel: {
icon: '<i class="fas fa-times"></i>',
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 = `<p>Take a long rest?</p><p>On a long rest you will recover hit points, half your maximum hit dice,
class resources, limited use item charges, and power slots.</p>`;
return new Promise((resolve, reject) => {
new Dialog({
title: "Long Rest",
content: content,
buttons: {
rest: {
icon: '<i class="fas fa-bed"></i>',
label: "Rest",
callback: resolve
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel",
callback: reject
},
},
default: 'rest',
close: reject
}, {classes: ["sw5e", "dialog"]}).render(true);
});
}
}

View file

@ -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);
}
}