foundry-sw5e/module/dice.js
supervj b56a074697 DND5e Core 1.3.5
DND5e Core 1.3.5 modded to SW5e System

Combining with DND5e Core 1.3.2 to see one big commit since last core update

DND5e Core 1.3.2 modded to SW5e System
2021-06-22 22:01:19 -04:00

267 lines
13 KiB
JavaScript

export {default as D20Roll} from "./dice/d20-roll.js";
export {default as DamageRoll} from "./dice/damage-roll.js";
/**
* A standardized helper function for simplifying the constant parts of a multipart roll formula
*
* @param {string} formula The original Roll formula
* @param {Object} data Actor or item data against which to parse the roll
* @param {Object} options Formatting options
* @param {boolean} options.constantFirst Puts the constants before the dice terms in the resulting formula
*
* @return {string} The resulting simplified formula
*/
export function simplifyRollFormula(formula, data, {constantFirst = false} = {}) {
const roll = new Roll(formula, data); // Parses the formula and replaces any @properties
const terms = roll.terms;
// Some terms are "too complicated" for this algorithm to simplify
// In this case, the original formula is returned.
if (terms.some(_isUnsupportedTerm)) return roll.formula;
const rollableTerms = []; // Terms that are non-constant, and their associated operators
const constantTerms = []; // Terms that are constant, and their associated operators
let operators = []; // Temporary storage for operators before they are moved to one of the above
for (let term of terms) { // For each term
if (term instanceof OperatorTerm) operators.push(term); // If the term is an addition/subtraction operator, push the term into the operators array
else { // Otherwise the term is not an operator
if (term instanceof DiceTerm) { // If the term is something rollable
rollableTerms.push(...operators); // Place all the operators into the rollableTerms array
rollableTerms.push(term); // Then place this rollable term into it as well
} //
else { // Otherwise, this must be a constant
constantTerms.push(...operators); // Place the operators into the constantTerms array
constantTerms.push(term); // Then also add this constant term to that array.
} //
operators = []; // Finally, the operators have now all been assigend to one of the arrays, so empty this before the next iteration.
}
}
const constantFormula = Roll.getFormula(constantTerms); // Cleans up the constant terms and produces a new formula string
const rollableFormula = Roll.getFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string
// Mathematically evaluate the constant formula to produce a single constant term
let constantPart = undefined;
if ( constantFormula ) {
try {
constantPart = Roll.safeEval(constantFormula)
} catch (err) {
console.warn(`Unable to evaluate constant term ${constantFormula} in simplifyRollFormula`);
}
}
// Order the rollable and constant terms, either constant first or second depending on the optional argument
const parts = constantFirst ? [constantPart, rollableFormula] : [rollableFormula, constantPart];
// Join the parts with a + sign, pass them to `Roll` once again to clean up the formula
return new Roll(parts.filterJoin(" + ")).formula;
}
/* -------------------------------------------- */
/**
* Only some terms are supported by simplifyRollFormula, this method returns true when the term is not supported.
* @param {*} term - A single Dice term to check support on
* @return {Boolean} True when unsupported, false if supported
*/
function _isUnsupportedTerm(term) {
const diceTerm = term instanceof DiceTerm;
const operator = term instanceof OperatorTerm && ["+", "-"].includes(term.operator);
const number = term instanceof NumericTerm;
return !(diceTerm || operator || number);
}
/* -------------------------------------------- */
/* D20 Roll */
/* -------------------------------------------- */
/**
* A standardized helper function for managing core 5e d20 rolls.
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
* This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively
*
* @param {string[]} parts The dice roll component parts, excluding the initial d20
* @param {object} data Actor or item data against which to parse the roll
*
* @param {boolean} [advantage] Apply advantage to the roll (unless otherwise specified)
* @param {boolean} [disadvantage] Apply disadvantage to the roll (unless otherwise specified)
* @param {number} [critical] The value of d20 result which represents a critical success
* @param {number} [fumble] The value of d20 result which represents a critical failure
* @param {number} [targetValue] Assign a target value against which the result of this roll should be compared
* @param {boolean} [elvenAccuracy] Allow Elven Accuracy to modify this roll?
* @param {boolean} [halflingLucky] Allow Halfling Luck to modify this roll?
* @param {boolean} [reliableTalent] Allow Reliable Talent to modify this roll?
* @param {boolean} [chooseModifier=false] Choose the ability modifier that should be used when the roll is made
* @param {boolean} [fastForward=false] Allow fast-forward advantage selection
* @param {Event} [event] The triggering event which initiated the roll
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
* @param {string} [template] The HTML template used to render the roll dialog
* @param {string} [title] The dialog window title
* @param {Object} [dialogOptions] Modal dialog options
*
* @param {boolean} [chatMessage=true] Automatically create a Chat Message for the result of this roll
* @param {object} [messageData={}] Additional data which is applied to the created Chat Message, if any
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
* @param {object} [speaker] The ChatMessage speaker to pass when creating the chat
* @param {string} [flavor] Flavor text to use in the posted chat message
*
* @return {Promise<D20Roll|null>} The evaluated D20Roll, or null if the workflow was cancelled
*/
export async function d20Roll({
parts=[], data={}, // Roll creation
advantage, disadvantage, fumble=1, critical=20, targetValue, elvenAccuracy, halflingLucky, reliableTalent, // Roll customization
chooseModifier=false, fastForward=false, event, template, title, dialogOptions, // Dialog configuration
chatMessage=true, messageData={}, rollMode, speaker, flavor // Chat Message customization
}={}) {
// Handle input arguments
const formula = ["1d20"].concat(parts).join(" + ");
const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event});
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
if ( chooseModifier && !isFF ) data["mod"] = "@mod";
// Construct the D20Roll instance
const roll = new CONFIG.Dice.D20Roll(formula, data, {
flavor: flavor || title,
advantageMode,
defaultRollMode,
critical,
fumble,
targetValue,
elvenAccuracy,
halflingLucky,
reliableTalent
});
// Prompt a Dialog to further configure the D20Roll
if ( !isFF ) {
const configured = await roll.configureDialog({
title,
chooseModifier,
defaultRollMode: defaultRollMode,
defaultAction: advantageMode,
defaultAbility: data?.item?.ability,
template
}, dialogOptions);
if ( configured === null ) return null;
}
// Evaluate the configured roll
await roll.evaluate({async: true});
// Create a Chat Message
if ( speaker ) {
console.warn(`You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`);
messageData.speaker = speaker;
}
if ( roll && chatMessage ) await roll.toMessage(messageData);
return roll;
}
/* -------------------------------------------- */
/**
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
* @returns {{isFF: boolean, advantageMode: number}} Whether the roll is fast-forward, and its advantage mode
*/
function _determineAdvantageMode({event, advantage=false, disadvantage=false, fastForward=false}={}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL;
if ( advantage || event?.altKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE;
else if ( disadvantage || event?.ctrlKey || event?.metaKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE;
return {isFF, advantageMode};
}
/* -------------------------------------------- */
/* Damage Roll */
/* -------------------------------------------- */
/**
* A standardized helper function for managing core 5e damage rolls.
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
* This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively
*
* @param {string[]} parts The dice roll component parts, excluding the initial d20
* @param {object} [data] Actor or item data against which to parse the roll
*
* @param {boolean} [critical=false] Flag this roll as a critical hit for the purposes of fast-forward or default dialog action
* @param {number} [criticalBonusDice=0] A number of bonus damage dice that are added for critical hits
* @param {number} [criticalMultiplier=2] A critical hit multiplier which is applied to critical hits
* @param {boolean} [multiplyNumeric=false] Multiply numeric terms by the critical multiplier
* @param {boolean} [powerfulCritical=false] Apply the "powerful criticals" house rule to critical hits
* @param {boolean} [fastForward=false] Allow fast-forward advantage selection
* @param {Event}[event] The triggering event which initiated the roll
* @param {boolean} [allowCritical=true] Allow the opportunity for a critical hit to be rolled
* @param {string} [template] The HTML template used to render the roll dialog
* @param {string} [title] The dice roll UI window title
* @param {object} [dialogOptions] Configuration dialog options
*
* @param {boolean} [chatMessage=true] Automatically create a Chat Message for the result of this roll
* @param {object} [messageData={}] Additional data which is applied to the created Chat Message, if any
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
* @param {object} [speaker] The ChatMessage speaker to pass when creating the chat
* @param {string} [flavor] Flavor text to use in the posted chat message
*
* @return {Promise<DamageRoll|null>} The evaluated DamageRoll, or null if the workflow was canceled
*/
export async function damageRoll({
parts=[], data, // Roll creation
critical=false, criticalBonusDice, criticalMultiplier, multiplyNumeric, powerfulCritical, // Damage customization
fastForward=false, event, allowCritical=true, template, title, dialogOptions, // Dialog configuration
chatMessage=true, messageData={}, rollMode, speaker, flavor, // Chat Message customization
}={}) {
// Handle input arguments
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
// Construct the DamageRoll instance
const formula = parts.join(" + ");
const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event});
const roll = new CONFIG.Dice.DamageRoll(formula, data, {
flavor: flavor || title,
critical: isCritical,
criticalBonusDice,
criticalMultiplier,
multiplyNumeric,
powerfulCritical
});
// Prompt a Dialog to further configure the DamageRoll
if ( !isFF ) {
const configured = await roll.configureDialog({
title,
defaultRollMode: defaultRollMode,
defaultCritical: isCritical,
template,
allowCritical
}, dialogOptions);
if ( configured === null ) return null;
}
// Evaluate the configured roll
await roll.evaluate({async: true});
// Create a Chat Message
if ( speaker ) {
console.warn(`You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`);
messageData.speaker = speaker;
}
if ( roll && chatMessage ) await roll.toMessage(messageData);
return roll;
}
/* -------------------------------------------- */
/**
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
* @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit
*/
function _determineCriticalMode({event, critical=false, fastForward=false}={}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
if ( event?.altKey ) critical = true;
return {isFF, isCritical: critical};
}