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