forked from GitHub-Mirrors/foundry-sw5e
230 lines
9.6 KiB
JavaScript
230 lines
9.6 KiB
JavaScript
/**
|
|
* A type of Roll specific to a d20-based check, save, or attack roll in the 5e system.
|
|
* @param {string} formula The string formula to parse
|
|
* @param {object} data The data object against which to parse attributes within the formula
|
|
* @param {object} [options={}] Extra optional arguments which describe or modify the D20Roll
|
|
* @param {number} [options.advantageMode] What advantage modifier to apply to the roll (none, advantage, disadvantage)
|
|
* @param {number} [options.critical] The value of d20 result which represents a critical success
|
|
* @param {number} [options.fumble] The value of d20 result which represents a critical failure
|
|
* @param {(number)} [options.targetValue] Assign a target value against which the result of this roll should be compared
|
|
* @param {boolean} [options.elvenAccuracy=false] Allow Elven Accuracy to modify this roll?
|
|
* @param {boolean} [options.halflingLucky=false] Allow Halfling Luck to modify this roll?
|
|
* @param {boolean} [options.reliableTalent=false] Allow Reliable Talent to modify this roll?
|
|
*/
|
|
// TODO: Check elven accuracy, halfling lucky, and reliable talent are required
|
|
// Elven Accuracy is Supreme accuracy feat, Reliable Talent is operative's Reliable Talent Class Feat
|
|
export default class D20Roll extends Roll {
|
|
constructor(formula, data, options) {
|
|
super(formula, data, options);
|
|
if (!(this.terms[0] instanceof Die && this.terms[0].faces === 20)) {
|
|
throw new Error(`Invalid D20Roll formula provided ${this._formula}`);
|
|
}
|
|
this.configureModifiers();
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Advantage mode of a 5e d20 roll
|
|
* @enum {number}
|
|
*/
|
|
static ADV_MODE = {
|
|
NORMAL: 0,
|
|
ADVANTAGE: 1,
|
|
DISADVANTAGE: -1
|
|
};
|
|
|
|
/**
|
|
* The HTML template path used to configure evaluation of this Roll
|
|
* @type {string}
|
|
*/
|
|
static EVALUATION_TEMPLATE = "systems/sw5e/templates/chat/roll-dialog.html";
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* A convenience reference for whether this D20Roll has advantage
|
|
* @type {boolean}
|
|
*/
|
|
get hasAdvantage() {
|
|
return this.options.advantageMode === D20Roll.ADV_MODE.ADVANTAGE;
|
|
}
|
|
|
|
/**
|
|
* A convenience reference for whether this D20Roll has disadvantage
|
|
* @type {boolean}
|
|
*/
|
|
get hasDisadvantage() {
|
|
return this.options.advantageMode === D20Roll.ADV_MODE.DISADVANTAGE;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
/* D20 Roll Methods */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Apply optional modifiers which customize the behavior of the d20term
|
|
* @private
|
|
*/
|
|
configureModifiers() {
|
|
const d20 = this.terms[0];
|
|
d20.modifiers = [];
|
|
|
|
// Halfling Lucky
|
|
if (this.options.halflingLucky) d20.modifiers.push("r1=1");
|
|
|
|
// Reliable Talent
|
|
if (this.options.reliableTalent) d20.modifiers.push("min10");
|
|
|
|
// Handle Advantage or Disadvantage
|
|
if (this.hasAdvantage) {
|
|
d20.number = this.options.elvenAccuracy ? 3 : 2;
|
|
d20.modifiers.push("kh");
|
|
d20.options.advantage = true;
|
|
} else if (this.hasDisadvantage) {
|
|
d20.number = 2;
|
|
d20.modifiers.push("kl");
|
|
d20.options.disadvantage = true;
|
|
} else d20.number = 1;
|
|
|
|
// Assign critical and fumble thresholds
|
|
if (this.options.critical) d20.options.critical = this.options.critical;
|
|
if (this.options.fumble) d20.options.fumble = this.options.fumble;
|
|
if (this.options.targetValue) d20.options.target = this.options.targetValue;
|
|
|
|
// Re-compile the underlying formula
|
|
this._formula = this.constructor.getFormula(this.terms);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritdoc */
|
|
async toMessage(messageData = {}, options = {}) {
|
|
// Evaluate the roll now so we have the results available to determine whether reliable talent came into play
|
|
if (!this._evaluated) await this.evaluate({async: true});
|
|
|
|
// Add appropriate advantage mode message flavor and sw5e roll flags
|
|
messageData.flavor = messageData.flavor || this.options.flavor;
|
|
if (this.hasAdvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
|
|
else if (this.hasDisadvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
|
|
|
|
// Add reliable talent to the d20-term flavor text if it applied
|
|
if (this.options.reliableTalent) {
|
|
const d20 = this.dice[0];
|
|
const isRT = d20.results.every((r) => !r.active || r.result < 10);
|
|
const label = `(${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
|
|
if (isRT) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label;
|
|
}
|
|
|
|
// Record the preferred rollMode
|
|
options.rollMode = options.rollMode ?? this.options.rollMode;
|
|
return super.toMessage(messageData, options);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
/* Configuration Dialog */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Create a Dialog prompt used to configure evaluation of an existing D20Roll instance.
|
|
* @param {object} data Dialog configuration data
|
|
* @param {string} [data.title] The title of the shown dialog window
|
|
* @param {number} [data.defaultRollMode] The roll mode that the roll mode select element should default to
|
|
* @param {number} [data.defaultAction] The button marked as default
|
|
* @param {boolean} [data.chooseModifier] Choose which ability modifier should be applied to the roll?
|
|
* @param {string} [data.defaultAbility] For tool rolls, the default ability modifier applied to the roll
|
|
* @param {string} [data.template] A custom path to an HTML template to use instead of the default
|
|
* @param {object} options Additional Dialog customization options
|
|
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
|
|
*/
|
|
async configureDialog(
|
|
{
|
|
title,
|
|
defaultRollMode,
|
|
defaultAction = D20Roll.ADV_MODE.NORMAL,
|
|
chooseModifier = false,
|
|
defaultAbility,
|
|
template
|
|
} = {},
|
|
options = {}
|
|
) {
|
|
// Render the Dialog inner HTML
|
|
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
|
|
formula: `${this.formula} + @bonus`,
|
|
defaultRollMode,
|
|
rollModes: CONFIG.Dice.rollModes,
|
|
chooseModifier,
|
|
defaultAbility,
|
|
abilities: CONFIG.SW5E.abilities
|
|
});
|
|
|
|
let defaultButton = "normal";
|
|
switch (defaultAction) {
|
|
case D20Roll.ADV_MODE.ADVANTAGE:
|
|
defaultButton = "advantage";
|
|
break;
|
|
case D20Roll.ADV_MODE.DISADVANTAGE:
|
|
defaultButton = "disadvantage";
|
|
break;
|
|
}
|
|
|
|
// Create the Dialog window and await submission of the form
|
|
return new Promise((resolve) => {
|
|
new Dialog(
|
|
{
|
|
title,
|
|
content,
|
|
buttons: {
|
|
advantage: {
|
|
label: game.i18n.localize("SW5E.Advantage"),
|
|
callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE))
|
|
},
|
|
normal: {
|
|
label: game.i18n.localize("SW5E.Normal"),
|
|
callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL))
|
|
},
|
|
disadvantage: {
|
|
label: game.i18n.localize("SW5E.Disadvantage"),
|
|
callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE))
|
|
}
|
|
},
|
|
default: defaultButton,
|
|
close: () => resolve(null)
|
|
},
|
|
options
|
|
).render(true);
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Handle submission of the Roll evaluation configuration Dialog
|
|
* @param {jQuery} html The submitted dialog content
|
|
* @param {number} advantageMode The chosen advantage mode
|
|
* @private
|
|
*/
|
|
_onDialogSubmit(html, advantageMode) {
|
|
const form = html[0].querySelector("form");
|
|
|
|
// Append a situational bonus term
|
|
if (form.bonus.value) {
|
|
const bonus = new Roll(form.bonus.value, this.data);
|
|
if (!(bonus.terms[0] instanceof OperatorTerm)) this.terms.push(new OperatorTerm({operator: "+"}));
|
|
this.terms = this.terms.concat(bonus.terms);
|
|
}
|
|
|
|
// Customize the modifier
|
|
if (form.ability?.value) {
|
|
const abl = this.data.abilities[form.ability.value];
|
|
this.terms.findSplice((t) => t.term === "@mod", new NumericTerm({number: abl.mod}));
|
|
this.options.flavor += ` (${CONFIG.SW5E.abilities[form.ability.value]})`;
|
|
}
|
|
|
|
// Apply advantage or disadvantage
|
|
this.options.advantageMode = advantageMode;
|
|
this.options.rollMode = form.rollMode.value;
|
|
this.configureModifiers();
|
|
return this;
|
|
}
|
|
}
|