/** * 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} 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; } }