forked from GitHub-Mirrors/foundry-sw5e
313 lines
14 KiB
JavaScript
313 lines
14 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};
|
|
}
|