forked from GitHub-Mirrors/foundry-sw5e
186 lines
7.8 KiB
JavaScript
186 lines
7.8 KiB
JavaScript
/**
|
|
* A type of Roll specific to a damage (or healing) 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 DamageRoll
|
|
* @param {number} [options.criticalBonusDice=0] A number of bonus damage dice that are added for critical hits
|
|
* @param {number} [options.criticalMultiplier=2] A critical hit multiplier which is applied to critical hits
|
|
* @param {boolean} [options.multiplyNumeric=false] Multiply numeric terms by the critical multiplier
|
|
* @param {boolean} [options.powerfulCritical=false] Apply the "powerful criticals" house rule to critical hits
|
|
*
|
|
*/
|
|
export default class DamageRoll extends Roll {
|
|
constructor(formula, data, options) {
|
|
super(formula, data, options);
|
|
// For backwards compatibility, skip rolls which do not have the "critical" option defined
|
|
if (this.options.critical !== undefined) this.configureDamage();
|
|
}
|
|
|
|
/**
|
|
* 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 DamageRoll is a critical hit
|
|
* @type {boolean}
|
|
*/
|
|
get isCritical() {
|
|
return this.options.critical;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
/* Damage Roll Methods */
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Apply optional modifiers which customize the behavior of the d20term
|
|
* @private
|
|
*/
|
|
configureDamage() {
|
|
let flatBonus = 0;
|
|
for (let [i, term] of this.terms.entries()) {
|
|
// Multiply dice terms
|
|
if (term instanceof DiceTerm) {
|
|
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
|
|
term.number = term.options.baseNumber;
|
|
if (this.isCritical) {
|
|
let cm = this.options.criticalMultiplier ?? 2;
|
|
|
|
// Powerful critical - maximize damage and reduce the multiplier by 1
|
|
if (this.options.powerfulCritical) {
|
|
flatBonus += term.number * term.faces;
|
|
cm = Math.max(1, cm - 1);
|
|
}
|
|
|
|
// Alter the damage term
|
|
let cb = this.options.criticalBonusDice && i === 0 ? this.options.criticalBonusDice : 0;
|
|
term.alter(cm, cb);
|
|
term.options.critical = true;
|
|
}
|
|
}
|
|
|
|
// Multiply numeric terms
|
|
else if (this.options.multiplyNumeric && term instanceof NumericTerm) {
|
|
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
|
|
term.number = term.options.baseNumber;
|
|
if (this.isCritical) {
|
|
term.number *= this.options.criticalMultiplier ?? 2;
|
|
term.options.critical = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add powerful critical bonus
|
|
if (this.options.powerfulCritical && flatBonus > 0) {
|
|
this.terms.push(new OperatorTerm({operator: "+"}));
|
|
this.terms.push(
|
|
new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")})
|
|
);
|
|
}
|
|
|
|
// Re-compile the underlying formula
|
|
this._formula = this.constructor.getFormula(this.terms);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritdoc */
|
|
toMessage(messageData = {}, options = {}) {
|
|
messageData.flavor = messageData.flavor || this.options.flavor;
|
|
if (this.isCritical) {
|
|
const label = game.i18n.localize("SW5E.CriticalHit");
|
|
messageData.flavor = messageData.flavor ? `${messageData.flavor} (${label})` : label;
|
|
}
|
|
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 {string} [data.defaultCritical] Should critical be selected as default
|
|
* @param {string} [data.template] A custom path to an HTML template to use instead of the default
|
|
* @param {boolean} [data.allowCritical=true] Allow critical hit to be chosen as a possible damage mode
|
|
* @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, defaultCritical = false, template, allowCritical = true} = {},
|
|
options = {}
|
|
) {
|
|
// Render the Dialog inner HTML
|
|
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
|
|
formula: `${this.formula} + @bonus`,
|
|
defaultRollMode,
|
|
rollModes: CONFIG.Dice.rollModes
|
|
});
|
|
|
|
// Create the Dialog window and await submission of the form
|
|
return new Promise((resolve) => {
|
|
new Dialog(
|
|
{
|
|
title,
|
|
content,
|
|
buttons: {
|
|
critical: {
|
|
condition: allowCritical,
|
|
label: game.i18n.localize("SW5E.CriticalHit"),
|
|
callback: (html) => resolve(this._onDialogSubmit(html, true))
|
|
},
|
|
normal: {
|
|
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
|
|
callback: (html) => resolve(this._onDialogSubmit(html, false))
|
|
}
|
|
},
|
|
default: defaultCritical ? "critical" : "normal",
|
|
close: () => resolve(null)
|
|
},
|
|
options
|
|
).render(true);
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* Handle submission of the Roll evaluation configuration Dialog
|
|
* @param {jQuery} html The submitted dialog content
|
|
* @param {boolean} isCritical Is the damage a critical hit?
|
|
* @private
|
|
*/
|
|
_onDialogSubmit(html, isCritical) {
|
|
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);
|
|
}
|
|
|
|
// Apply advantage or disadvantage
|
|
this.options.critical = isCritical;
|
|
this.options.rollMode = form.rollMode.value;
|
|
this.configureDamage();
|
|
return this;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritdoc */
|
|
static fromData(data) {
|
|
const roll = super.fromData(data);
|
|
roll._formula = this.getFormula(roll.terms);
|
|
return roll;
|
|
}
|
|
}
|