refactor: extract inline HTML to templates, split oversized files, fix bugs
- Extract all inline HTML from JS into 21 Handlebars templates (chat/, dialogs/, ui/) - Split utils.mjs (1507) into barrel + helpers.mjs, combat.mjs, d30.mjs - Split roll.mjs (1632) into barrel + roll-base.mjs, roll-prompt.mjs, roll-combat.mjs, roll-damage.mjs - Split lethal-fantasy.mjs (1426) into bootstrap + chat-reaction.mjs - Fix: missing async on injectDiceTray (free-roll.mjs:29 SyntaxError) - Fix: weapon._id fallback for deserialized chat-message weapon objects - Fix: missing await on rollModifier.evaluate() calls in roll-combat.mjs - Fix: choices→choicesList ReferenceError in utils.mjs - Fix: add 12 missing i18n keys (chooseWeapon, chooseSave, attackRoll, etc.) - Fix: restore sideLabel in bonus-die-select.hbs - Clean: remove dead messageContent param, console.log→log() - Style: barrel files preserve existing import paths
This commit is contained in:
@@ -0,0 +1,598 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
import D30Roll from "./d30-roll.mjs"
|
||||
|
||||
/**
|
||||
* Prompt the user with a dialog to configure and execute a roll.
|
||||
*
|
||||
* @param {Object} options Configuration options for the roll.
|
||||
* @param {string} options.rollType The type of roll being performed (e.g., RESOURCE, DAMAGE, ATTACK, SAVE).
|
||||
* @param {string} options.rollValue The initial value or formula for the roll.
|
||||
* @param {string} options.rollTarget The target of the roll.
|
||||
* @param {"="|"+"|"++"|"-"|"--"} options.rollAdvantage If there is an avantage (+), a disadvantage (-), a double advantage (++), a double disadvantage (--) or a normal roll (=).
|
||||
* @param {string} options.actorId The ID of the actor performing the roll.
|
||||
* @param {string} options.actorName The name of the actor performing the roll.
|
||||
* @param {string} options.actorImage The image of the actor performing the roll.
|
||||
* @param {boolean} options.hasTarget Whether the roll has a target.
|
||||
* @param {Object} options.target The target of the roll, if any.
|
||||
* @param {Object} options.data Additional data for the roll.
|
||||
*
|
||||
* @returns {Promise<Object|null>} The roll result or null if the dialog was cancelled.
|
||||
*/
|
||||
export async function prompt(options = {}) {
|
||||
try {
|
||||
let dice = "1D20"
|
||||
let maxValue = 20
|
||||
let baseFormula = "1D20"
|
||||
let modifierFormula = "1D0"
|
||||
let hasModifier = true
|
||||
let hasChangeDice = false
|
||||
let hasD30 = false
|
||||
let hasFavor = false
|
||||
let hasMaxValue = true
|
||||
let hasGrantedDice = false
|
||||
let pointBlank = false
|
||||
let letItFly = false
|
||||
let saveSpell = game.lethalFantasy?.spellDefense ?? false
|
||||
let beyondSkill = false
|
||||
let hasStaticModifier = false
|
||||
let hasExplode = true
|
||||
let actor = game.actors.get(options.actorId)
|
||||
|
||||
if (options.rollType === "challenge" || options.rollType === "save") {
|
||||
options.rollName = game.i18n.localize(`LETHALFANTASY.Label.${options.rollTarget.rollKey}`)
|
||||
hasD30 = options.rollType === "save"
|
||||
if (options.rollTarget.rollKey === "dying") {
|
||||
dice = options.rollTarget.value
|
||||
hasModifier = false
|
||||
hasChangeDice = true
|
||||
hasFavor = true
|
||||
} else {
|
||||
dice = "1D20"
|
||||
hasFavor = true
|
||||
}
|
||||
|
||||
} else if (options.rollType === "granted") {
|
||||
hasD30 = false
|
||||
options.rollName = `Granted ${options.rollTarget.rollKey}`
|
||||
dice = options.rollTarget.formula
|
||||
baseFormula = options.rollTarget.formula
|
||||
hasModifier = false
|
||||
hasMaxValue = false
|
||||
hasChangeDice = false
|
||||
hasFavor = false
|
||||
|
||||
} else if (options.rollType === "monster-attack" || options.rollType === "monster-defense") {
|
||||
hasD30 = true
|
||||
options.rollName = options.rollTarget.name
|
||||
dice = "1D20"
|
||||
baseFormula = "D20"
|
||||
hasModifier = true
|
||||
hasChangeDice = false
|
||||
hasFavor = true
|
||||
if (options.rollType === "monster-attack") {
|
||||
options.rollTarget.value = options.rollTarget.attackModifier
|
||||
options.rollTarget.charModifier = 0
|
||||
} else {
|
||||
options.rollTarget.value = options.rollTarget.defenseModifier
|
||||
options.rollTarget.charModifier = 0
|
||||
options.isRangedDefense = options.rollTarget.isRangedDefense ?? false
|
||||
}
|
||||
|
||||
} else if (options.rollType === "monster-skill") {
|
||||
options.rollName = game.i18n.localize(`LETHALFANTASY.Label.${options.rollTarget.rollKey}`)
|
||||
dice = "1D20"
|
||||
baseFormula = "D20"
|
||||
hasModifier = true
|
||||
hasFavor = true
|
||||
hasChangeDice = false
|
||||
|
||||
} else if (options.rollType === "skill") {
|
||||
options.rollName = options.rollTarget.name
|
||||
hasD30 = true
|
||||
dice = "1D20"
|
||||
baseFormula = "D20"
|
||||
hasModifier = true
|
||||
hasFavor = true
|
||||
hasChangeDice = false
|
||||
options.rollTarget.value = Math.floor(options.rollTarget.system.skillTotal / 10)
|
||||
|
||||
} else if (options.rollType === "weapon-attack" || options.rollType === "weapon-defense") {
|
||||
hasD30 = true
|
||||
options.rollName = options.rollTarget.name
|
||||
dice = "1D20"
|
||||
baseFormula = "D20"
|
||||
hasModifier = true
|
||||
hasChangeDice = false
|
||||
hasFavor = true
|
||||
if (options.rollType === "weapon-attack") {
|
||||
if (options.rollTarget.weapon.system.weaponType === "melee") {
|
||||
options.rollTarget.value = options.rollTarget.combat.attackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus
|
||||
options.rollTarget.charModifier = options.rollTarget.combat.attackModifier
|
||||
} else {
|
||||
options.rollTarget.value = options.rollTarget.combat.rangedAttackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus
|
||||
options.rollTarget.charModifier = options.rollTarget.combat.rangedAttackModifier
|
||||
}
|
||||
} else {
|
||||
// For defense, check if it's a ranged defense
|
||||
const defenseModifier = options.rollTarget.isRangedDefense
|
||||
? options.rollTarget.combat.rangedDefenseModifier
|
||||
: options.rollTarget.combat.defenseModifier
|
||||
options.rollTarget.value = defenseModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.defenseBonus + options.rollTarget.armorDefense
|
||||
options.rollTarget.charModifier = defenseModifier
|
||||
// Store isRanged flag for D30 lookup
|
||||
options.isRangedDefense = options.rollTarget.isRangedDefense
|
||||
}
|
||||
|
||||
} else if (options.rollType === "spell" || options.rollType === "spell-attack" || options.rollType === "spell-power") {
|
||||
hasD30 = true
|
||||
options.rollName = options.rollTarget.name
|
||||
dice = "1D20"
|
||||
baseFormula = "D20"
|
||||
hasModifier = true
|
||||
hasChangeDice = false
|
||||
hasFavor = true
|
||||
options.rollTarget.value = options.rollTarget.actorModifiers.levelSpellModifier + options.rollTarget.actorModifiers.intSpellModifier
|
||||
options.rollTarget.charModifier = options.rollTarget.actorModifiers.intSpellModifier
|
||||
hasStaticModifier = options.rollType === "spell-power"
|
||||
//hasModifier = options.rollType !== "spell-attack"
|
||||
if (hasStaticModifier) {
|
||||
options.rollTarget.staticModifier = options.rollTarget.actorLevel
|
||||
} else {
|
||||
options.rollTarget.staticModifier = 0
|
||||
}
|
||||
|
||||
} else if (options.rollType === "miracle" || options.rollType === "miracle-attack" || options.rollType === "miracle-power") {
|
||||
hasD30 = true
|
||||
options.rollName = options.rollTarget.name
|
||||
dice = "1D20"
|
||||
baseFormula = "D20"
|
||||
hasChangeDice = false
|
||||
hasFavor = true
|
||||
options.rollTarget.value = options.rollTarget.actorModifiers.levelMiracleModifier + options.rollTarget.actorModifiers.chaMiracleModifier
|
||||
options.rollTarget.charModifier = options.rollTarget.actorModifiers.chaMiracleModifier
|
||||
hasStaticModifier = options.rollType === "miracle-power"
|
||||
//hasModifier = options.rollType !== "miracle-attack"
|
||||
if (hasStaticModifier) {
|
||||
options.rollTarget.staticModifier = options.rollTarget.actorLevel
|
||||
} else {
|
||||
options.rollTarget.staticModifier = 0
|
||||
}
|
||||
|
||||
} else if (options.rollType === "shield-roll") {
|
||||
hasD30 = false
|
||||
options.rollName = "Shield Defense"
|
||||
dice = options.rollTarget.system.defense.toUpperCase()
|
||||
baseFormula = dice
|
||||
hasModifier = true
|
||||
hasChangeDice = false
|
||||
hasMaxValue = false
|
||||
hasExplode = false
|
||||
hasFavor = true
|
||||
options.rollTarget.value = 0
|
||||
|
||||
} else if (options.rollType.includes("weapon-damage")) {
|
||||
options.rollName = options.rollTarget.name
|
||||
options.isDamage = true
|
||||
hasModifier = true
|
||||
hasChangeDice = false
|
||||
let damageBonus = (options.rollTarget.weapon.system.applyStrengthDamageBonus) ? options.rollTarget.combat.damageModifier : 0
|
||||
options.rollTarget.value = damageBonus + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.damageBonus
|
||||
options.rollTarget.charModifier = damageBonus
|
||||
dice = options.rollTarget.weapon.system.damage.damageM
|
||||
if (/NE$/i.test(dice)) {
|
||||
hasMaxValue = false
|
||||
hasExplode = false
|
||||
}
|
||||
dice = dice.replace(/NE$/i, "").replace("E", "")
|
||||
baseFormula = dice
|
||||
|
||||
} else if (options.rollType.includes("monster-damage")) {
|
||||
options.rollName = options.rollTarget.name
|
||||
options.isDamage = true
|
||||
hasModifier = true
|
||||
hasChangeDice = false
|
||||
options.rollTarget.value = options.rollTarget.damageModifier
|
||||
options.rollTarget.charModifier = 0
|
||||
dice = options.rollTarget.damageDice
|
||||
dice = dice.replace("E", "")
|
||||
baseFormula = dice
|
||||
if (options.rollTarget.noExplode) {
|
||||
hasMaxValue = false
|
||||
hasExplode = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (options.rollType === "save" && (options.rollTarget.rollKey === "pain" || options.rollTarget.rollKey === "paincourage")) {
|
||||
dice = options.rollTarget.rollDice
|
||||
baseFormula = options.rollTarget.rollDice
|
||||
hasModifier = false
|
||||
}
|
||||
|
||||
const rollModes = foundry.utils.duplicate(CONFIG.ChatMessage.modes);
|
||||
|
||||
|
||||
const fieldRollMode = new foundry.data.fields.StringField({
|
||||
choices: rollModes,
|
||||
blank: false,
|
||||
default: "public",
|
||||
})
|
||||
|
||||
const choiceModifier = SYSTEM.CHOICE_MODIFIERS
|
||||
const choiceDice = SYSTEM.CHOICE_DICE
|
||||
const choiceFavor = SYSTEM.FAVOR_CHOICES
|
||||
|
||||
let modifier = "+0"
|
||||
let targetName
|
||||
|
||||
// True for any ranged attack: PC weapon (ranged type) or monster attack (ranged mode)
|
||||
const isRangedAttack = (options.rollType === "weapon-attack" && options.rollTarget?.weapon?.system?.weaponType === "ranged")
|
||||
|| (options.rollType === "monster-attack" && options.rollTarget?.attackMode === "ranged")
|
||||
|
||||
let dialogContext = {
|
||||
rollType: options.rollType,
|
||||
rollTarget: options.rollTarget,
|
||||
rollName: options.rollName,
|
||||
actorName: options.actorName,
|
||||
rollModes,
|
||||
hasModifier,
|
||||
hasFavor,
|
||||
hasChangeDice,
|
||||
pointBlank,
|
||||
baseValue: options.rollTarget.value,
|
||||
attackerAimChoices: SYSTEM.ATTACKER_AIM_CHOICES,
|
||||
attackerAim: "0",
|
||||
changeDice: `${dice}`,
|
||||
fieldRollMode,
|
||||
choiceModifier,
|
||||
choiceDice,
|
||||
choiceFavor,
|
||||
baseFormula,
|
||||
dice,
|
||||
hasTarget: options.hasTarget,
|
||||
modifier,
|
||||
saveSpell,
|
||||
favor: "none",
|
||||
targetName,
|
||||
isRangedAttack
|
||||
}
|
||||
let rollContext
|
||||
if (options.rollContext) {
|
||||
rollContext = foundry.utils.duplicate(options.rollContext)
|
||||
hasGrantedDice = !!rollContext.hasGrantedDice
|
||||
pointBlank = !!rollContext.pointBlank
|
||||
beyondSkill = !!rollContext.beyondSkill
|
||||
letItFly = !!rollContext.letItFly
|
||||
saveSpell = !!rollContext.saveSpell
|
||||
const _rawMode = rollContext.rollMode || game.settings.get("core", "rollMode")
|
||||
const _modeMap = { publicroll: "public", gmroll: "gm", blindroll: "blind", selfroll: "self" }
|
||||
rollContext.visibility ||= _modeMap[_rawMode] ?? _rawMode ?? "public"
|
||||
rollContext.modifier ||= modifier
|
||||
rollContext.favor ||= "none"
|
||||
rollContext.changeDice ||= `${dice}`
|
||||
rollContext.attackerAim ||= "0"
|
||||
} else {
|
||||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/roll-dialog.hbs", dialogContext)
|
||||
|
||||
let position = game.user.getFlag(SYSTEM.id, "roll-dialog-pos") || { top: -1, left: -1 }
|
||||
const label = game.i18n.localize("LETHALFANTASY.Roll.roll")
|
||||
rollContext = await foundry.applications.api.DialogV2.wait({
|
||||
window: { title: "Roll dialog" },
|
||||
classes: ["lethalfantasy"],
|
||||
content,
|
||||
position,
|
||||
buttons: [
|
||||
{
|
||||
action: "roll",
|
||||
type: "button",
|
||||
label: label,
|
||||
callback: (event, button, dialog) => {
|
||||
log("Roll context", event, button, dialog)
|
||||
let position = dialog?.position
|
||||
game.user.setFlag(SYSTEM.id, "roll-dialog-pos", foundry.utils.duplicate(position))
|
||||
const output = Array.from(button.form.elements).reduce((obj, input) => {
|
||||
if (input.name) obj[input.name] = input.value
|
||||
return obj
|
||||
}, {})
|
||||
return output
|
||||
},
|
||||
},
|
||||
],
|
||||
actions: {
|
||||
"selectGranted": (event, button) => {
|
||||
hasGrantedDice = event.target.checked
|
||||
},
|
||||
"selectBeyondSkill": (event, button) => {
|
||||
beyondSkill = button.checked
|
||||
},
|
||||
"selectPointBlank": (event, button) => {
|
||||
pointBlank = button.checked
|
||||
},
|
||||
"selectLetItFly": (event, button) => {
|
||||
letItFly = button.checked
|
||||
},
|
||||
"saveSpellCheck": (event, button) => {
|
||||
saveSpell = button.checked
|
||||
},
|
||||
"gotoToken": (event, button) => {
|
||||
let tokenId = $(button).data("tokenId")
|
||||
let token = canvas.tokens?.get(tokenId)
|
||||
if (token) {
|
||||
canvas.animatePan({ x: token.x, y: token.y, duration: 200 })
|
||||
canvas.tokens.releaseAll()
|
||||
token.control({ releaseOthers: true })
|
||||
}
|
||||
}
|
||||
},
|
||||
rejectClose: false // Click on Close button will not launch an error
|
||||
})
|
||||
}
|
||||
|
||||
// If the user cancels the dialog, exit
|
||||
if (rollContext === null) return
|
||||
log("rollContext", rollContext, hasGrantedDice)
|
||||
rollContext.saveSpell = saveSpell // Update fucking flag
|
||||
|
||||
let fullModifier = 0
|
||||
let titleFormula = ""
|
||||
dice = rollContext.changeDice || dice
|
||||
if (hasModifier) {
|
||||
let bonus = Number(options.rollTarget.value)
|
||||
fullModifier = rollContext.modifier === "" ? 0 : parseInt(rollContext.modifier, 10) + bonus
|
||||
fullModifier += (rollContext.saveSpell) ? (options.rollTarget.actorModifiers?.saveModifier ?? 0) : 0
|
||||
if (Number(rollContext.attackerAim) > 0) {
|
||||
fullModifier += Number(rollContext.attackerAim)
|
||||
}
|
||||
|
||||
if (fullModifier === 0) {
|
||||
modifierFormula = "0"
|
||||
} else {
|
||||
let modAbs = Math.abs(fullModifier)
|
||||
modifierFormula = `D${modAbs + 1} - 1`
|
||||
}
|
||||
if (hasStaticModifier) {
|
||||
modifierFormula += ` + ${options.rollTarget.staticModifier}`
|
||||
}
|
||||
let sign = fullModifier < 0 ? "-" : "+"
|
||||
if (hasExplode) {
|
||||
titleFormula = `${dice}E ${sign} ${modifierFormula}`
|
||||
} else {
|
||||
titleFormula = `${dice} ${sign} ${modifierFormula}`
|
||||
}
|
||||
} else {
|
||||
modifierFormula = "0"
|
||||
fullModifier = 0
|
||||
baseFormula = `${dice}`
|
||||
if (hasExplode) {
|
||||
titleFormula = `${dice}E`
|
||||
} else {
|
||||
titleFormula = `${dice}`
|
||||
}
|
||||
}
|
||||
|
||||
// Latest addition : favor choice at point blank range
|
||||
if (pointBlank) {
|
||||
rollContext.favor = "favor"
|
||||
}
|
||||
if (beyondSkill) {
|
||||
rollContext.favor = "disfavor"
|
||||
}
|
||||
|
||||
// Specific pain case
|
||||
if (options.rollType === "save" && options.rollTarget.rollKey === "pain" || options.rollTarget.rollKey === "paincourage") {
|
||||
baseFormula = options.rollTarget.rollDice
|
||||
titleFormula = `${dice}`
|
||||
modifierFormula = "0"
|
||||
fullModifier = 0
|
||||
}
|
||||
|
||||
// Specific pain/poison/contagion case
|
||||
if (options.rollType === "save" && (options.rollTarget.rollKey === "poison" || options.rollTarget.rollKey === "contagion")) {
|
||||
hasD30 = false
|
||||
hasStaticModifier = true
|
||||
modifierFormula = ` + ${Math.abs(fullModifier)}`
|
||||
titleFormula = `${dice}E + ${Math.abs(fullModifier)}`
|
||||
}
|
||||
|
||||
if (letItFly) {
|
||||
baseFormula = "1D20"
|
||||
titleFormula = `1D20E`
|
||||
modifierFormula = "0"
|
||||
fullModifier = 0
|
||||
hasFavor = false
|
||||
hasExplode = true
|
||||
rollContext.favor = "none"
|
||||
}
|
||||
|
||||
const maxMatch = baseFormula ? baseFormula.match(/\d+$/) : null
|
||||
maxValue = maxMatch ? Number(maxMatch[0]) : 0
|
||||
|
||||
const rollData = {
|
||||
type: options.rollType,
|
||||
rollType: options.rollType,
|
||||
target: options.rollTarget,
|
||||
rollName: options.rollName,
|
||||
actorId: options.actorId,
|
||||
actorName: options.actorName,
|
||||
actorImage: options.actorImage,
|
||||
rollMode: rollContext.visibility,
|
||||
hasTarget: options.hasTarget,
|
||||
isDamage: options.isDamage,
|
||||
pointBlank,
|
||||
beyondSkill,
|
||||
letItFly,
|
||||
hasGrantedDice,
|
||||
titleFormula,
|
||||
targetName,
|
||||
...rollContext,
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook event that fires before the roll is made.
|
||||
* @function
|
||||
* @memberof hookEvents
|
||||
* @param {Object} options Options for the roll.
|
||||
* @param {Object} rollData All data related to the roll.
|
||||
* @returns {boolean} Explicitly return `false` to prevent roll to be made.
|
||||
*/
|
||||
if (Hooks.call("fvtt-lethal-fantasy.preRoll", options, rollData) === false) return
|
||||
|
||||
let rollBase = new this(baseFormula, options.data, rollData)
|
||||
const rollModifier = new Roll(modifierFormula, options.data, rollData)
|
||||
await rollModifier.evaluate()
|
||||
await rollBase.evaluate()
|
||||
|
||||
let rollFavor
|
||||
let badResult
|
||||
if (rollContext.favor === "favor") {
|
||||
rollFavor = new this(baseFormula, options.data, rollData)
|
||||
await rollFavor.evaluate()
|
||||
log("Favor dice", {
|
||||
rollBaseTotal: rollBase.total,
|
||||
rollFavorTotal: rollFavor.total,
|
||||
rollBaseResults: rollBase.dice.map(d => d.results.map(r => r.result)),
|
||||
rollFavorResults: rollFavor.dice.map(d => d.results.map(r => r.result)),
|
||||
baseFormula
|
||||
})
|
||||
if (game?.dice3d) {
|
||||
game.dice3d.showForRoll(rollFavor, game.user, true)
|
||||
}
|
||||
if (Number(rollFavor.result) > Number(rollBase.result)) {
|
||||
badResult = rollBase.result
|
||||
rollBase = rollFavor
|
||||
} else {
|
||||
badResult = rollFavor.result
|
||||
}
|
||||
rollFavor = null
|
||||
}
|
||||
|
||||
if (rollContext.favor === "disfavor") {
|
||||
rollFavor = new this(baseFormula, options.data, rollData)
|
||||
await rollFavor.evaluate()
|
||||
log("Disfavor dice", {
|
||||
rollBaseTotal: rollBase.total,
|
||||
rollFavorTotal: rollFavor.total,
|
||||
rollBaseResults: rollBase.dice.map(d => d.results.map(r => r.result)),
|
||||
rollFavorResults: rollFavor.dice.map(d => d.results.map(r => r.result)),
|
||||
baseFormula
|
||||
})
|
||||
if (game?.dice3d) {
|
||||
game.dice3d.showForRoll(rollFavor, game.user, true)
|
||||
}
|
||||
if (Number(rollFavor.result) < Number(rollBase.result)) {
|
||||
badResult = rollBase.result
|
||||
rollBase = rollFavor
|
||||
} else {
|
||||
badResult = rollFavor.result
|
||||
}
|
||||
rollFavor = null
|
||||
}
|
||||
|
||||
if (options.forceNoD30) {
|
||||
hasD30 = false
|
||||
}
|
||||
|
||||
if (hasD30) {
|
||||
let rollD30 = await new Roll("1D30").evaluate()
|
||||
if (game?.dice3d) {
|
||||
game.dice3d.showForRoll(rollD30, game.user, true)
|
||||
}
|
||||
options.D30result = rollD30.total
|
||||
|
||||
// Compute isRanged for D30: covers defense (isRangedDefense), monster ranged attacks (attackMode),
|
||||
// and PC weapon attacks (isRangedAttack or weaponType)
|
||||
const isRangedForD30 = options.isRangedDefense
|
||||
|| options.rollTarget?.attackMode === "ranged"
|
||||
|| options.rollTarget?.isRangedAttack === true
|
||||
|| options.rollTarget?.weapon?.system?.weaponType === "ranged"
|
||||
const d30Message = D30Roll.getResult(
|
||||
rollD30.total,
|
||||
options.rollType,
|
||||
options.rollTarget?.weapon,
|
||||
{ isRanged: isRangedForD30, isSpellSave: saveSpell }
|
||||
)
|
||||
options.D30message = d30Message
|
||||
}
|
||||
|
||||
let rollTotal = 0
|
||||
let diceResults = []
|
||||
let resultType
|
||||
let diceSum = 0
|
||||
|
||||
let singleDice = `1D${maxValue}`
|
||||
for (let i = 0; i < rollBase.dice.length; i++) {
|
||||
const dieResults = rollBase.dice[i].results
|
||||
const resultCount = dieResults.length
|
||||
for (let j = 0; j < resultCount; j++) {
|
||||
let diceResult = dieResults[j].result
|
||||
diceResults.push({ dice: `${singleDice.toUpperCase()}`, value: diceResult })
|
||||
diceSum += diceResult
|
||||
if (hasMaxValue) {
|
||||
while (diceResult === maxValue) {
|
||||
let r = await new Roll(baseFormula).evaluate()
|
||||
diceResult = r.dice[0].results[0].result
|
||||
diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 })
|
||||
diceSum += (diceResult - 1)
|
||||
// Add to DieTerm results so DSN/Foundry display shows explosion dice
|
||||
dieResults.push({ result: diceResult, active: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasGrantedDice && options.rollTarget.grantedDice && options.rollTarget.grantedDice !== "") {
|
||||
titleFormula += ` + ${options.rollTarget.grantedDice.toUpperCase()}`
|
||||
let grantedRoll = new Roll(options.rollTarget.grantedDice)
|
||||
await grantedRoll.evaluate()
|
||||
if (game?.dice3d) {
|
||||
await game.dice3d.showForRoll(grantedRoll, game.user, true)
|
||||
}
|
||||
diceResults.push({ dice: `${options.rollTarget.grantedDice.toUpperCase()}`, value: grantedRoll.total })
|
||||
rollTotal += grantedRoll.total
|
||||
}
|
||||
|
||||
if (fullModifier !== 0) {
|
||||
diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total })
|
||||
if (fullModifier < 0) {
|
||||
rollTotal += Math.max(diceSum - rollModifier.total, 0)
|
||||
} else {
|
||||
rollTotal += diceSum + rollModifier.total
|
||||
}
|
||||
} else {
|
||||
rollTotal += diceSum
|
||||
}
|
||||
|
||||
rollBase.options.resultType = resultType
|
||||
rollBase.options.rollTotal = rollTotal
|
||||
rollBase.options.diceResults = diceResults
|
||||
rollBase.options.rollTarget = options.rollTarget
|
||||
rollBase.options.titleFormula = titleFormula
|
||||
rollBase.options.D30result = options.D30result
|
||||
rollBase.options.D30message = options.D30message
|
||||
rollBase.options.badResult = badResult
|
||||
rollBase.options.rollData = foundry.utils.duplicate(rollData)
|
||||
rollBase.options.defenderId = options.defenderId
|
||||
rollBase.options.defenderTokenId = options.defenderTokenId
|
||||
rollBase.options.extraShieldDr = options.extraShieldDr || 0
|
||||
rollBase.options.damageTier = options.damageTier || "standard"
|
||||
rollBase.options.d30Bleed = options.d30Bleed || false
|
||||
rollBase.options.d30DamageMultiplier = options.d30DamageMultiplier || 1
|
||||
rollBase.options.d30DrMultiplier = options.d30DrMultiplier || 1
|
||||
|
||||
/**
|
||||
* A hook event that fires after the roll has been made.
|
||||
* @function
|
||||
* @memberof hookEvents
|
||||
* @param {Object} options Options for the roll.
|
||||
* @param {Object} rollData All data related to the roll.
|
||||
* @param {LethalFantasyRoll} roll The resulting roll.
|
||||
* @returns {boolean} Explicitly return `false` to prevent roll to be made.
|
||||
*/
|
||||
if (Hooks.call("fvtt-lethal-fantasy.Roll", options, rollData, rollBase) === false) return
|
||||
|
||||
return rollBase
|
||||
} finally {
|
||||
// Clear one-shot flag so it doesn't leak to subsequent non-spell saves
|
||||
if (game.lethalFantasy) game.lethalFantasy.spellDefense = false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user