import { SYSTEM } from "../config/system.mjs" import LethalFantasyUtils from "../utils.mjs" export default class LethalFantasyRoll extends Roll { /** * The HTML template path used to render dice checks of this type * @type {string} */ static CHAT_TEMPLATE = "systems/fvtt-lethal-fantasy/templates/chat-message.hbs" get type() { return this.options.type } get titleFormula() { return this.options.titleFormula } get rollName() { return this.options.rollName } get target() { return this.options.target } get value() { return this.options.value } get treshold() { return this.options.treshold } get actorId() { return this.options.actorId } get actorName() { return this.options.actorName } get actorImage() { return this.options.actorImage } get modifier() { return this.options.modifier } get resultType() { return this.options.resultType } get isFailure() { return this.resultType === "failure" } get hasTarget() { return this.options.hasTarget } get targetName() { return this.options.targetName } get targetArmor() { return this.options.targetArmor } get targetMalus() { return this.options.targetMalus } get realDamage() { return this.options.realDamage } get rollTotal() { return this.options.rollTotal } get diceResults() { return this.options.diceResults } get rollTarget() { return this.options.rollTarget } get D30result() { return this.options.D30result } /** * 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} The roll result or null if the dialog was cancelled. */ static async prompt(options = {}) { let dice = "1D20" let maxValue = 20 let baseFormula = "1D20" let modifierFormula = "1d0" let hasModifier = true let hasChangeDice = false let hasD30 = false if (options.rollType === "challenge" || options.rollType === "save") { options.rollName = options.rollTarget.rollKey hasD30 = options.rollType === "save" if (options.rollTarget.rollKey === "dying") { dice = options.rollTarget.value maxValue = Number(options.rollTarget.value.match(/\d+/)[0]) hasModifier = false hasChangeDice = true } else { dice = "1D20" maxValue = 20 } } else if (options.rollType === "skill") { options.rollName = options.rollTarget.name dice = "1D100" baseFormula = "D100" maxValue = 100 hasModifier = true hasChangeDice = false options.rollTarget.value = options.rollTarget.system.skillTotal } else if (options.rollType === "weapon-attack" || options.rollType === "weapon-defense") { hasD30 = true options.rollName = options.rollTarget.name dice = "1D20" baseFormula = "D20" maxValue = 20 hasModifier = true hasChangeDice = false if (options.rollType === "weapon-attack") { options.rollTarget.value = options.rollTarget.combat.attackModifier + options.rollTarget.weaponSkillModifier options.rollTarget.charModifier = options.rollTarget.combat.attackModifier } else { options.rollTarget.value = options.rollTarget.combat.defenseModifier + options.rollTarget.weaponSkillModifier options.rollTarget.charModifier = options.rollTarget.combat.defenseModifier } } else if (options.rollType === "spell") { hasD30 = true options.rollName = options.rollTarget.name dice = "1D20" baseFormula = "D20" maxValue = 20 hasModifier = true hasChangeDice = false options.rollTarget.value = options.rollTarget.actorModifiers.levelSpellModifier + options.rollTarget.actorModifiers.intSpellModifier options.rollTarget.charModifier = options.rollTarget.actorModifiers.intSpellModifier } else if (options.rollType === "miracle") { hasD30 = true options.rollName = options.rollTarget.name dice = "1D20" baseFormula = "D20" maxValue = 20 hasModifier = true hasChangeDice = false options.rollTarget.value = options.rollTarget.actorModifiers.levelMiracleModifier + options.rollTarget.actorModifiers.chaMiracleModifier options.rollTarget.charModifier = options.rollTarget.actorModifiers.chaMiracleModifier } else if (options.rollType.includes("weapon-damage")) { options.rollName = options.rollTarget.name hasModifier = true hasChangeDice = false options.rollTarget.value = options.rollTarget.combat.damageModifier + options.rollTarget.weaponSkillModifier options.rollTarget.charModifier = options.rollTarget.combat.damageModifier if (options.rollType.includes("small")) { dice = options.rollTarget.weapon.system.damage.damageS } else { dice = options.rollTarget.weapon.system.damage.damageM } dice = dice.replace("E", "") baseFormula = dice maxValue = 20 } if (options.rollType === "save" && options.rollTarget.rollKey === "pain") { dice = options.rollTarget.rollDice baseFormula = options.rollTarget.rollDice hasModifier = false } const rollModes = Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)])) const fieldRollMode = new foundry.data.fields.StringField({ choices: rollModes, blank: false, default: "public", }) const choiceModifier = SYSTEM.CHOICE_MODIFIERS const choiceDice = SYSTEM.CHOICE_DICE let modifier = "+0" let targetName let dialogContext = { rollType: options.rollType, rollTarget: options.rollTarget, rollName: options.rollName, rollModes, hasModifier, hasChangeDice, baseValue: options.rollTarget.value, changeDice: `${dice}`, fieldRollMode, choiceModifier, choiceDice, baseFormula, dice, hasTarget: options.hasTarget, modifier, saveSpell: false, targetName } console.log("dialogContext", dialogContext) const content = await renderTemplate("systems/fvtt-lethal-fantasy/templates/roll-dialog.hbs", dialogContext) const title = LethalFantasyRoll.createTitle(options.rollType, options.rollTarget) const label = game.i18n.localize("LETHALFANTASY.Roll.roll") const rollContext = await foundry.applications.api.DialogV2.wait({ window: { title: title }, classes: ["lethalfantasy"], content, buttons: [ { label: label, callback: (event, button, dialog) => { const output = Array.from(button.form.elements).reduce((obj, input) => { if (input.name) obj[input.name] = input.value return obj }, {}) return output }, }, ], rejectClose: false // Click on Close button will not launch an error }) // If the user cancels the dialog, exit if (rollContext === null) return 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 if (fullModifier === 0) { modifierFormula = "0" } else { let modAbs = Math.abs(fullModifier) modifierFormula = `d${modAbs + 1} - 1` } let sign = fullModifier < 0 ? "-" : "+" titleFormula = `${dice}E ${sign} ${modifierFormula}` } else { modifierFormula = "0" fullModifier = 0 baseFormula = `${dice}` titleFormula = `${dice}E` } // Specific pain case if (options.rollType === "save" && options.rollTarget.rollKey === "pain") { baseFormula = options.rollTarget.rollDice titleFormula = `${dice}` modifierFormula = "0" fullModifier = 0 } // Specific pain/poison/contagion case if (options.rollType === "save" && options.rollTarget.rollKey.includes(["pain", "poison", "contagion"])) { hasD30 = false } maxValue = Number(baseFormula.match(/\d+$/)[0]) // Update the max value agains 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, 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 const rollBase = new this(baseFormula, options.data, rollData) const rollModifier = new Roll(modifierFormula, options.data, rollData) rollModifier.evaluate() await rollBase.evaluate() if (hasD30) { let rollD30 = await new Roll("1D30").evaluate() options.D30result = rollD30.total } let rollTotal = -1 let diceResults = [] let resultType let diceSum = 0 let singleDice = `1D${maxValue}` for (let i = 0; i < rollBase.dice.length; i++) { for (let j = 0; j < rollBase.dice[i].results.length; j++) { let diceResult = rollBase.dice[i].results[j].result diceResults.push({ dice: `${singleDice}`, value: diceResult }) diceSum += diceResult while (diceResult === maxValue) { let r = await new Roll(baseFormula).evaluate() diceResult = r.dice[0].results[0].result diceResults.push({ dice: `${singleDice}-1`, value: diceResult - 1 }) diceSum += (diceResult - 1) } } } if (fullModifier !== 0) { diceResults.push({ dice: `${rollModifier.formula}`, 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 /** * 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 } static async promptRangedDefense(rollTarget) { const rollModes = Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)])) const fieldRollMode = new foundry.data.fields.StringField({ choices: rollModes, blank: false, default: "public", }) let dialogContext = { movementChoices : SYSTEM.MOVEMENT_CHOICES, moveDirectionChoices : SYSTEM.MOVE_DIRECTION_CHOICES, sizeChoices : SYSTEM.SIZE_CHOICES, rangeChoices : SYSTEM.RANGE_CHOICES, attackerAimChoices : SYSTEM.ATTACKER_AIM_CHOICES, movement: "none", moveDirection: "none", size: "medium", range: "short", attackerAim: "simple", fieldRollMode, rollModes } console.log("CTX", dialogContext) const content = await renderTemplate("systems/fvtt-lethal-fantasy/templates/range-defense-dialog.hbs", dialogContext) const label = game.i18n.localize("LETHALFANTASY.Label.rangeDefenseRoll") const rollContext = await foundry.applications.api.DialogV2.wait({ window: { title: "Range Defense" }, classes: ["lethalfantasy"], content, buttons: [ { label: label, callback: (event, button, dialog) => { const output = Array.from(button.form.elements).reduce((obj, input) => { if (input.name) obj[input.name] = input.value return obj }, {}) return output }, }, ], rejectClose: false // Click on Close button will not launch an error }) console.log("RollContext", rollContext) // Build the final modifier let fullModifier = Number(rollContext.moveDirection) + Number(rollContext.size) + Number(rollContext.range) + Number(rollContext.attackerAim) console.log("Modifier", fullModifier) let modifierFormula if (fullModifier === 0) { modifierFormula = "0" } else { let modAbs = Math.abs(fullModifier) modifierFormula = `d${modAbs + 1} - 1` } // If the user cancels the dialog, exit if (rollContext === null) return let rollData = {...rollContext} let options = {...rollContext} options.rollName = "Ranged Defense" const rollBase = new this(rollContext.movement, options.data, rollData) const rollModifier = new Roll(modifierFormula, options.data, rollData) rollModifier.evaluate() await rollBase.evaluate() let rollD30 = await new Roll("1D30").evaluate() options.D30result = rollD30.total let dice = rollContext.movement let maxValue = Number(dice.match(/\d+$/)[0]) // Update the max value agains let rollTotal = -1 let diceResults = [] let resultType let diceResult = rollBase.dice[0].results[0].result diceResults.push({ dice: `${dice}`, value: diceResult }) let diceSum = diceResult while (diceResult === maxValue) { let r = await new Roll(baseFormula).evaluate() diceResult = r.dice[0].results[0].result diceResults.push({ dice: `${dice}-1`, value: diceResult - 1 }) diceSum += (diceResult - 1) } if (fullModifier !== 0) { diceResults.push({ dice: `${rollModifier.formula}`, 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 = `${dice}E + ${modifierFormula}` rollBase.options.D30result = options.D30result rollBase.options.rollName = "Ranged Defense" /** * 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. */ return rollBase } /** * Creates a title based on the given type. * * @param {string} type The type of the roll. * @param {string} target The target of the roll. * @returns {string} The generated title. */ static createTitle(type, target) { switch (type) { case "challenge": return `${game.i18n.localize("LETHALFANTASY.Label.titleChallenge")}` case "save": return `${game.i18n.localize("LETHALFANTASY.Label.titleSave")}` case "skill": return `${game.i18n.localize("LETHALFANTASY.Label.titleSkill")}` case "weapon-attack": return `${game.i18n.localize("LETHALFANTASY.Label.weapon-attack")}` case "weapon-defense": return `${game.i18n.localize("LETHALFANTASY.Label.weapon-defense")}` case "weapon-damage-small": return `${game.i18n.localize("LETHALFANTASY.Label.weapon-damage-small")}` case "weapon-damage-medium": return `${game.i18n.localize("LETHALFANTASY.Label.weapon-damage-medium")}` case "spell": return `${game.i18n.localize("LETHALFANTASY.Label.spell")}` default: return game.i18n.localize("LETHALFANTASY.Label.titleStandard") } } /** @override */ async render(chatOptions = {}) { let chatData = await this._getChatCardData(chatOptions.isPrivate) return await renderTemplate(this.constructor.CHAT_TEMPLATE, chatData) } /** * Generates the data required for rendering a roll chat card. * * @param {boolean} isPrivate Indicates if the chat card is private. * @returns {Promise} A promise that resolves to an object containing the chat card data. * @property {Array} css - CSS classes for the chat card. * @property {Object} data - The data associated with the roll. * @property {number} diceTotal - The total value of the dice rolled. * @property {boolean} isGM - Indicates if the user is a Game Master. * @property {string} formula - The formula used for the roll. * @property {number} total - The total result of the roll. * @property {boolean} isSave - Indicates if the roll is a saving throw. * @property {boolean} isDamage - Indicates if the roll is for damage. * @property {boolean} isFailure - Indicates if the roll is a failure. * @property {string} actorId - The ID of the actor performing the roll. * @property {string} actingCharName - The name of the character performing the roll. * @property {string} actingCharImg - The image of the character performing the roll. * @property {string} resultType - The type of result (e.g., success, failure). * @property {boolean} hasTarget - Indicates if the roll has a target. * @property {string} targetName - The name of the target. * @property {number} targetArmor - The armor value of the target. * @property {boolean} isPrivate - Indicates if the chat card is private. * @property {string} cssClass - The combined CSS classes as a single string. * @property {string} tooltip - The tooltip text for the chat card. */ async _getChatCardData(isPrivate) { const cardData = { css: [SYSTEM.id, "dice-roll"], data: this.data, diceTotal: this.dice.reduce((t, d) => t + d.total, 0), isGM: game.user.isGM, formula: this.formula, titleFormula: this.titleFormula, rollName: this.rollName, rollType: this.type, rollTarget: this.rollTarget, total: this.rollTotal, isFailure: this.isFailure, actorId: this.actorId, diceResults: this.diceResults, actingCharName: this.actorName, actingCharImg: this.actorImage, resultType: this.resultType, hasTarget: this.hasTarget, targetName: this.targetName, targetArmor: this.targetArmor, D30result: this.D30result, isPrivate: isPrivate } cardData.cssClass = cardData.css.join(" ") cardData.tooltip = isPrivate ? "" : await this.getTooltip() return cardData } /** * Converts the roll result to a chat message. * * @param {Object} [messageData={}] Additional data to include in the message. * @param {Object} options Options for message creation. * @param {string} options.rollMode The mode of the roll (e.g., public, private). * @param {boolean} [options.create=true] Whether to create the message. * @returns {Promise} - A promise that resolves when the message is created. */ async toMessage(messageData = {}, { rollMode, create = true } = {}) { super.toMessage( { isSave: this.isSave, isChallenge: this.isChallenge, isFailure: this.resultType === "failure", rollType: this.type, rollTarget: this.rollTarget, actingCharName: this.actorName, actingCharImg: this.actorImage, hasTarget: this.hasTarget, targetName: this.targetName, targetArmor: this.targetArmor, targetMalus: this.targetMalus, realDamage: this.realDamage, ...messageData, }, { rollMode: rollMode }, ) } }