import CthulhuEternalUtils from "../utils.mjs" export default class CthulhuEternalRoll extends Roll { /** * The HTML template path used to render dice checks of this type * @type {string} */ static CHAT_TEMPLATE = "systems/fvtt-cthulhu-eternal/templates/chat-message.hbs" get type() { return this.options.type } get isDamage() { return this.type === ROLL_TYPE.DAMAGE } 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 introText() { return this.options.introText } get introTextTooltip() { return this.options.introTextTooltip } get aide() { return this.options.aide } get gene() { return this.options.gene } get modificateur() { return this.options.modificateur } get avantages() { return this.options.avantages } 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 rollAdvantage() { return this.options.rollAdvantage } /** * Generates introductory text based on the roll type. * * @returns {string} The formatted introductory text for the roll. */ _createIntroText() { let text switch (this.type) { case ROLL_TYPE.SAVE: const saveLabel = game.i18n.localize(`TENEBRIS.Character.FIELDS.caracteristiques.${this.target}.valeur.label`) text = game.i18n.format("TENEBRIS.Roll.save", { save: saveLabel }) text = text.concat("
").concat(`Seuil : ${this.treshold}`) break case ROLL_TYPE.RESOURCE: const resourceLabel = game.i18n.localize(`TENEBRIS.Character.FIELDS.ressources.${this.target}.valeur.label`) text = game.i18n.format("TENEBRIS.Roll.resource", { resource: resourceLabel }) break case ROLL_TYPE.DAMAGE: const damageLabel = this.target text = game.i18n.format("TENEBRIS.Roll.damage", { item: damageLabel }) break case ROLL_TYPE.ATTACK: const attackLabel = this.target text = game.i18n.format("TENEBRIS.Roll.attack", { item: attackLabel }) break } return text } /** * Generates an introductory text tooltip with characteristics and modifiers. * * @returns {string} A formatted string containing the value, help, hindrance, and modifier. */ _createIntroTextTooltip() { let tooltip = game.i18n.format("TENEBRIS.Tooltip.saveIntroTextTooltip", { value: this.value, aide: this.aide, gene: this.gene, modificateur: this.modificateur }) if (this.hasTarget) { tooltip = tooltip.concat(`
Cible : ${this.targetName}`) } return tooltip } /** * 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 formula = options.rollValue // Formula for a resource roll if (options.rollType === ROLL_TYPE.RESOURCE) { let ressource = game.i18n.localize(`TENEBRIS.Character.FIELDS.ressources.${options.rollTarget}.valeur.label`) if (formula === "0" || formula === "") { ui.notifications.warn(game.i18n.format("TENEBRIS.Warning.plusDeRessource", { ressource: ressource })) return null } } 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 choiceAide = foundry.utils.mergeObject({ 0: "0" }, options.rollValue <= 10 ? { 1: "1" } : { 1: "1", 2: "2" }) const choiceGene = { 0: "0", "-1": "-1", "-2": "-2", "-3": "-3", "-4": "-4", "-5": "-5", "-6": "-6", "-7": "-7", "-8": "-8", "-9": "-9", "-10": "-10", } const choiceAvantage = { normal: "Normal", avantage: "Avantage", desavantage: "Désavantage", doubleAvantage: "Double avantage", doubleDesavantage: "Double désavantage" } const choiceModificateur = { 0: "0", "-1": "-1", "-2": "-2", "-3": "-3", "-4": "-4", "-5": "-5", "-6": "-6", "-7": "-7", "-8": "-8", "-9": "-9", "-10": "-10", } let damageDice let damageDiceMax let damageDiceFinal let damageDiceLowered // Damage roll : check the roll is not above the maximum damage if (options.rollType === ROLL_TYPE.DAMAGE) { damageDice = options.rollValue damageDiceMax = game.actors.get(options.actorId).system.dmax.valeur damageDiceFinal = CthulhuEternalUtils.maxDamage(damageDice, damageDiceMax) damageDiceLowered = damageDiceFinal !== damageDice // Récupération du nom de l'objet si c'est un jet depuis la fiche de l'acteur // Si c'est via une macro le nom est connu options.rollTarget = game.actors.get(options.actorId).items.get(options.rollTarget).name } if (options.rollType === ROLL_TYPE.ATTACK) { damageDice = options.rollValue } let malus = "0" let targetMalus = "0" let targetName let targetArmor const displayOpponentMalus = game.settings.get("tenebris", "displayOpponentMalus") if (options.rollType === ROLL_TYPE.SAVE && options.hasTarget && options.target.document.actor.type === "opponent") { targetName = options.target.document.actor.name if (displayOpponentMalus) malus = options.target.document.actor.system.malus.toString() else targetMalus = options.target.document.actor.system.malus.toString() } if (options.rollType === ROLL_TYPE.DAMAGE && options.hasTarget && options.target.document.actor.type === "opponent") { targetName = options.target.document.actor.name targetArmor = options.target.document.actor.system.armure.toString() } let dialogContext = { isSave: options.rollType === ROLL_TYPE.SAVE, isResource: options.rollType === ROLL_TYPE.RESOURCE, isDamage: options.rollType === ROLL_TYPE.DAMAGE, isAttack: options.rollType === ROLL_TYPE.ATTACK, rollModes, fieldRollMode, choiceAide, choiceGene, choiceAvantage, choiceModificateur, damageDice, damageDiceMax, damageDiceFinal, damageDiceLowered, formula, hasTarget: options.hasTarget, malus, targetName, targetArmor, rollAdvantage: this._convertAvantages(options.rollAdvantage), rangeAdvantage: this._convertRollAdvantageToRange(options.rollAdvantage), } const content = await renderTemplate("systems/fvtt-cthulhu-eternal/templates/roll-dialog.hbs", dialogContext) const title = CthulhuEternalRoll.createTitle(options.rollType, options.rollTarget) const label = game.i18n.localize("TENEBRIS.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 }, {}) // Avantages switch (output.avantages) { case "1": output.avantages = "doubleDesavantage" break case "2": output.avantages = "desavantage" break case "3": output.avantages = "normal" break case "4": output.avantages = "avantage" break case "5": output.avantages = "doubleAvantage" break } return output }, }, ], rejectClose: false, // Click on Close button will not launch an error render: (event, dialog) => { const rangeInput = dialog.querySelector('input[name="avantages"]') if (rangeInput) { rangeInput.addEventListener("change", (event) => { event.preventDefault() event.stopPropagation() const readOnly = dialog.querySelector('input[name="selectAvantages"]') readOnly.value = this._convertAvantages(event.target.value) }) } }, }) // If the user cancels the dialog, exit if (rollContext === null) return let treshold if (options.rollType === ROLL_TYPE.SAVE) { const aide = rollContext.aide === "" ? 0 : parseInt(rollContext.aide, 10) const gene = rollContext.gene === "" ? 0 : parseInt(rollContext.gene, 10) const modificateur = rollContext.modificateur === "" ? 0 : parseInt(rollContext.modificateur, 10) if (options.rollType === ROLL_TYPE.SAVE) { let dice = "1d20" switch (rollContext.avantages) { case "avantage": dice = "2d20kl" break case "desavantage": dice = "2d20kh" break case "doubleAvantage": dice = "3d20kl" break case "doubleDesavantage": dice = "3d20kh" break } formula = `${dice}` } treshold = options.rollValue + aide + gene + modificateur } // Formula for a damage roll if (options.rollType === ROLL_TYPE.DAMAGE) { formula = damageDiceFinal } // Formula for an attack roll if (options.rollType === ROLL_TYPE.ATTACK) { formula = damageDice } const rollData = { type: options.rollType, target: options.rollTarget, value: options.rollValue, treshold: treshold, actorId: options.actorId, actorName: options.actorName, actorImage: options.actorImage, rollMode: rollContext.visibility, hasTarget: options.hasTarget, targetName, targetArmor, targetMalus, ...rollContext, } /** * A hook event that fires before the roll is made. * @function tenebris.preRoll * @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("tenebris.preRoll", options, rollData) === false) return const roll = new this(formula, options.data, rollData) await roll.evaluate() let resultType if (options.rollType === ROLL_TYPE.SAVE) { resultType = roll.total <= treshold ? "success" : "failure" } else if (options.rollType === ROLL_TYPE.RESOURCE) { resultType = roll.total === 1 || roll.total === 2 ? "failure" : "success" } let realDamage if (options.rollType === ROLL_TYPE.DAMAGE) { realDamage = Math.max(0, roll.total - parseInt(targetArmor, 10)) } roll.options.resultType = resultType roll.options.treshold = treshold roll.options.introText = roll._createIntroText() roll.options.introTextTooltip = roll._createIntroTextTooltip() roll.options.realDamage = realDamage /** * A hook event that fires after the roll has been made. * @function tenebris.Roll * @memberof hookEvents * @param {Object} options Options for the roll. * @param {Object} rollData All data related to the roll. @param {CthulhuEternalRoll} roll The resulting roll. * @returns {boolean} Explicitly return `false` to prevent roll to be made. */ if (Hooks.call("tenebris.Roll", options, rollData, roll) === false) return return roll } /** * 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 ROLL_TYPE.SAVE: return `${game.i18n.localize("TENEBRIS.Dialog.titleSave")} : ${game.i18n.localize(`TENEBRIS.Manager.${target}`)}` case ROLL_TYPE.RESOURCE: return `${game.i18n.localize("TENEBRIS.Dialog.titleResource")} : ${game.i18n.localize(`TENEBRIS.Manager.${target}`)}` case ROLL_TYPE.DAMAGE: return `${game.i18n.localize("TENEBRIS.Dialog.titleDamage")} : ${target}` case ROLL_TYPE.ATTACK: return `${game.i18n.localize("TENEBRIS.Dialog.titleAttack")} : ${target}` default: return game.i18n.localize("TENEBRIS.Dialog.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} isResource - Indicates if the roll is related to a resource. * @property {boolean} isDamage - Indicates if the roll is for damage. * @property {boolean} isFailure - Indicates if the roll is a failure. * @property {Array} avantages - Advantages associated with the roll. * @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} introText - Introductory text for the roll. * @property {string} introTextTooltip - Tooltip for the introductory text. * @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 {number} realDamage - The real damage dealt. * @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, total: this.total, isSave: this.isSave, isResource: this.isResource, isDamage: this.isDamage, isFailure: this.isFailure, avantages: this.avantages, actorId: this.actorId, actingCharName: this.actorName, actingCharImg: this.actorImage, introText: this.introText, introTextTooltip: this.introTextTooltip, resultType: this.resultType, hasTarget: this.hasTarget, targetName: this.targetName, targetArmor: this.targetArmor, realDamage: this.realDamage, 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, isResource: this.isResource, isDamage: this.isDamage, isFailure: this.resultType === "failure", avantages: this.avantages, introText: this.introText, introTextTooltip: this.introTextTooltip, actingCharName: this.actorName, actingCharImg: this.actorImage, hasTarget: this.hasTarget, targetName: this.targetName, targetArmor: this.targetArmor, targetMalus: this.targetMalus, realDamage: this.realDamage, ...messageData, }, { rollMode: rollMode }, ) } // Used in the avantages select and with the rollAdvantage parameter: convert the selected value to the corresponding string static _convertAvantages(value) { switch (value) { case "1": return game.i18n.localize("TENEBRIS.Roll.doubleDesavantage") case "2": return game.i18n.localize("TENEBRIS.Roll.desavantage") case "3": return game.i18n.localize("TENEBRIS.Roll.normal") case "4": return game.i18n.localize("TENEBRIS.Roll.avantage") case "5": return game.i18n.localize("TENEBRIS.Roll.doubleAvantage") case "--": return game.i18n.localize("TENEBRIS.Roll.doubleDesavantage") case "-": return game.i18n.localize("TENEBRIS.Roll.desavantage") case "=": return game.i18n.localize("TENEBRIS.Roll.normal") case "+": return game.i18n.localize("TENEBRIS.Roll.avantage") case "++": return game.i18n.localize("TENEBRIS.Roll.doubleAvantage") } } // Used in the rollAdvantage parameter: convert the selected value to the corresponding range value static _convertRollAdvantageToRange(value) { switch (value) { case "--": return 1 case "-": return 2 case "=": return 3 case "+": return 4 case "++": return 5 } } }