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 } get badResult() { return this.options.badResult } /** * 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 let hasFavor = false let hasMaxValue = true let hasGrantedDice = false let hasStaticModifier = false let hasExplode = true 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 maxValue = Number(options.rollTarget.value.match(/\d+/)[0]) hasModifier = false hasChangeDice = true hasFavor = true } else { dice = "1D20" maxValue = 20 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 maxValue = 100 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" maxValue = 20 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 } } else if (options.rollType === "monster-skill") { options.rollName = game.i18n.localize(`LETHALFANTASY.Label.${options.rollTarget.rollKey}`) dice = "1D100" baseFormula = "D100" maxValue = 100 hasModifier = true hasFavor = true hasChangeDice = false } else if (options.rollType === "skill") { options.rollName = options.rollTarget.name dice = "1D100" baseFormula = "D100" maxValue = 100 hasModifier = true hasFavor = 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 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 { options.rollTarget.value = options.rollTarget.combat.defenseModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.defenseBonus options.rollTarget.charModifier = options.rollTarget.combat.defenseModifier } } else if (options.rollType === "spell" || options.rollType === "spell-attack" || options.rollType === "spell-power") { 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 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" maxValue = 20 hasChangeDice = false 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 options.rollTarget.value = 0 } else if (options.rollType.includes("weapon-damage")) { options.rollName = options.rollTarget.name 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 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 } else if (options.rollType.includes("monster-damage")) { options.rollName = options.rollTarget.name hasModifier = true hasChangeDice = false options.rollTarget.value = options.rollTarget.damageModifier options.rollTarget.charModifier = 0 dice = options.rollTarget.damageDice dice = dice.replace("E", "") baseFormula = dice maxValue = 20 } 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.Dice.rollModes); // v12 : Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)])) console.log("Roll mode", rollModes) 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 let dialogContext = { rollType: options.rollType, rollTarget: options.rollTarget, rollName: options.rollName, actorName: options.actorName, rollModes, hasModifier, hasFavor, hasChangeDice, baseValue: options.rollTarget.value, changeDice: `${dice}`, fieldRollMode, choiceModifier, choiceDice, choiceFavor, baseFormula, dice, hasTarget: options.hasTarget, modifier, saveSpell: false, favor: "none", targetName } 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") const rollContext = await foundry.applications.api.DialogV2.wait({ window: { title: "Roll dialog" }, classes: ["lethalfantasy"], content, position, buttons: [ { label: label, callback: (event, button, dialog) => { console.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, dialog) => { hasGrantedDice = true }, "gotoToken" : (event, button, dialog) => { 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 console.log("rollContext", rollContext, hasGrantedDice) 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 { if (options.rollType === "skill" || options.rollType === "monster-skill") { modifierFormula = `${fullModifier}` } else { let modAbs = Math.abs(fullModifier) modifierFormula = `D${modAbs + 1} - 1` } } if (hasStaticModifier) { modifierFormula += ` + ${options.rollTarget.staticModifier}` } // modifierFormula += ` + ${options.rollTarget.charModifier}` 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}` } } // 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 === "pain" || options.rollTarget.rollKey === "paincourage" || options.rollTarget.rollKey === "poison" || options.rollTarget.rollKey === "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, 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() if (game?.dice3d) { game.dice3d.showForRoll(rollFavor, game.user, true) } if (rollFavor.result > rollBase.result) { badResult = rollBase.result rollBase = rollFavor } else { badResult = rollFavor.result } } if (rollContext.favor === "disfavor") { rollFavor = new this(baseFormula, options.data, rollData) await rollFavor.evaluate() if (game?.dice3d) { game.dice3d.showForRoll(rollFavor, game.user, true) } if (rollFavor.result < rollBase.result) { badResult = rollBase.result rollBase = rollFavor } else { badResult = rollFavor.result } } if (hasD30) { let rollD30 = await new Roll("1D30").evaluate() if (game?.dice3d) { game.dice3d.showForRoll(rollD30, game.user, true) } 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.toUpperCase()}`, value: diceResult }) diceSum += diceResult if (hasMaxValue) { while (diceResult === maxValue) { let r = await new Roll(baseFormula).evaluate() if (game?.dice3d) { await game.dice3d.showForRoll(r, game.user, true) } diceResult = r.dice[0].results[0].result diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 }) diceSum += (diceResult - 1) } } } } if (hasGrantedDice) { 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.badResult = badResult /** * 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 promptInitiative(options = {}) { const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); // v12 : 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", }) if (SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass]) { options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass] } else { options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS["untrained"] } let dialogContext = { actorClass: options.actorClass, initiativeDiceChoice: options.initiativeDiceChoice, initiativeDice: "1D20", maxInit: options.maxInit, fieldRollMode, rollModes } console.log("CTX", dialogContext) const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/roll-initiative-dialog.hbs", dialogContext) const label = game.i18n.localize("LETHALFANTASY.Label.initiative") const rollContext = await foundry.applications.api.DialogV2.wait({ window: { title: "Initiative Roll" }, 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 }) let initRoll = new Roll(`min(${rollContext.initiativeDice}, ${options.maxInit})`, options.data, rollContext) await initRoll.evaluate() let msg = await initRoll.toMessage({ flavor: `Initiative for ${options.actorName}` }, { rollMode: rollContext.visibility }) if (game?.dice3d) { await game.dice3d.waitFor3DAnimationByMessageID(msg.id) } if (options.combatId && options.combatantId) { let combat = game.combats.get(options.combatId) combat.updateEmbeddedDocuments("Combatant", [{ _id: options.combatantId, initiative: initRoll.total, 'system.progressionCount': 0 }]); } } /* ***********************************************************/ static async promptCombatAction(options = {}) { const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); // v12 : 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 combatant = game.combats.get(options.combatId)?.combatants?.get(options.combatantId) if (!combatant) { console.error("No combatant found for this combat") return } let currentAction = combatant.getFlag(SYSTEM.id, "currentAction") let position = game.user.getFlag(SYSTEM.id, "combat-action-dialog-pos") || { top: -1, left: -1 } let dialogContext = { progressionDiceId: "", fieldRollMode, rollModes, currentAction, ...options } const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/combat-action-dialog.hbs", dialogContext) let buttons = [] if (currentAction) { if (currentAction.type === "weapon") { buttons.push({ action: "roll", label: "Roll progression dice", callback: (event, button, dialog) => { let pos = $('#combat-action-dialog').position() game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) return "rollProgressionDice" }, }) } else if (currentAction.type === "spell" || currentAction.type === "miracle") { let label = "" if (currentAction.spellStatus === "castingTime") { let pos = $('#combat-action-dialog').position() game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) label = "Wait casting time" } if (currentAction.spellStatus === "toBeCasted") { let pos = $('#combat-action-dialog').position() game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) label = "Cast spell/miracle" } if (currentAction.spellStatus === "lethargy") { let pos = $('#combat-action-dialog').position() game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) label = "Roll lethargy dice" } buttons.push({ action: "roll", label: label, callback: (event, button, dialog) => { let pos = $('#combat-action-dialog').position() game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) return "rollLethargyDice" }, }) } } else { buttons.push({ action: "roll", label: "Select action", callback: (event, button, dialog) => { let pos = $('#combat-action-dialog').position() game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) const output = Array.from(button.form.elements).reduce((obj, input) => { if (input.name) obj[input.name] = input.value return obj }, {}) return output }, }, ) } buttons.push({ action: "cancel", label: "Other action, not listed here", callback: (event, button, dialog) => { let pos = $('#combat-action-dialog').position() game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) return null; } }) let rollContext = await foundry.applications.api.DialogV2.wait({ window: { title: "Combat Action Dialog" }, id: "combat-action-dialog", classes: ["lethalfantasy"], position, content, buttons, rejectClose: false // Click on Close button will not launch an error }) console.log("RollContext", dialogContext, rollContext) // If action is cancelled, exit if (rollContext === null || rollContext === "cancel") { await combatant.setFlag(SYSTEM.id, "currentAction", "") let message = `${combatant.name} : Other action, progression reset` ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) return } // Setup the current action if (!currentAction || currentAction === "") { // Get the item from the returned selectedChoice value let selectedChoice = rollContext.selectedChoice let rangedMode if (selectedChoice.match("simpleAim")) { selectedChoice = selectedChoice.replace("simpleAim", "") rangedMode = "simpleAim" } if (selectedChoice.match("carefulAim")) { selectedChoice = selectedChoice.replace("carefulAim", "") rangedMode = "carefulAim" } if (selectedChoice.match("focusedAim")) { selectedChoice = selectedChoice.replace("focusedAim", "") rangedMode = "focusedAim" } let selectedItem = combatant.actor.items.find(i => i.id === selectedChoice) // Setup flag for combat action usage let actionItem = foundry.utils.duplicate(selectedItem) actionItem.progressionCount = 1 actionItem.rangedMode = rangedMode actionItem.castingTime = 1 actionItem.spellStatus = "castingTime" // Set the flag on the combatant await combatant.setFlag(SYSTEM.id, "currentAction", actionItem) let message = `${combatant.name} action : ${selectedItem.name}, start rolling progression dice or casting time` ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) rollContext = (actionItem.type == "weapon") ? "rollProgressionDice" : "rollLethargyDice" // Set the roll context to rollProgressionDice currentAction = actionItem } if (currentAction) { if (rollContext === "rollLethargyDice") { if (currentAction.spellStatus === "castingTime") { let time = currentAction.type === "spell" ? currentAction.system.castingTime : currentAction.system.prayerTime if (currentAction.castingTime < time) { let message = `Casting time : ${currentAction.name}, count : ${currentAction.castingTime}/${time}` ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) currentAction.castingTime += 1 await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) return } else { let message = `Spell/Miracle ${currentAction.name} ready to be cast on next second !` ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) currentAction.castingTime = 1 currentAction.spellStatus = "toBeCasted" await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) return } } if (currentAction.spellStatus === "toBeCasted") { combatant.actor.prepareRoll((currentAction.type === "spell") ? "spell-attack" : "miracle-attack", currentAction._id) if (currentAction.type === "spell") { currentAction.spellStatus = "lethargy" await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) } else { // No lethargy for miracle await combatant.setFlag(SYSTEM.id, "currentAction", "") } return } if (currentAction.spellStatus === "lethargy") { // Roll lethargy dice let dice = LethalFantasyUtils.getLethargyDice(currentAction.system.level) let roll = new Roll(dice) await roll.evaluate() if (game?.dice3d) { await game.dice3d.showForRoll(roll) } let max = roll.dice[0].faces - 1 let toCompare = Math.min(currentAction.progressionCount, max) if (roll.total <= toCompare) { // Notify that the player can act now with a chat message let message = game.i18n.format("LETHALFANTASY.Notifications.messageLethargyOK", { name: combatant.actor.name, spellName: currentAction.name, roll: roll.total }) ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) // Update the combatant progression count await combatant.setFlag(SYSTEM.id, "currentAction", "") // Display the action selection window again combatant.actor.system.rollProgressionDice(options.combatId, options.combatantId) } else { // Notify that the player cannot act now with a chat message currentAction.progressionCount += 1 await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) let message = game.i18n.format("LETHALFANTASY.Notifications.messageLethargyKO", { name: combatant.actor.name, spellName: currentAction.name, roll: roll.total }) ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) } } } if (rollContext === "rollProgressionDice") { let formula = currentAction.system.combatProgressionDice if (currentAction?.rangedMode) { let toSplit = currentAction.system.speed[currentAction.rangedMode] let split = toSplit.split("+") currentAction.rangedLoad = Number(split[0]) || 0 formula = split[1] console.log("Ranged Mode", currentAction.rangedMode, currentAction.rangedLoad, formula) } // Range weapon loading if (!currentAction.weaponLoaded && currentAction.rangedLoad) { if (currentAction.progressionCount <= currentAction.rangedLoad) { let message = `Ranged weapon ${currentAction.name} is loading, loading count : ${currentAction.progressionCount}/${currentAction.rangedLoad}` ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) currentAction.progressionCount += 1 await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) } else { let message = `Ranged weapon ${currentAction.name} is loaded !` ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) currentAction.weaponLoaded = true currentAction.progressionCount = 1 await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) } return } // Melee mode let isMonster = combatant.actor.type === "monster" // Get the dice and roll it if let roll = new Roll(formula) await roll.evaluate() let max = roll.dice[0].faces - 1 max = Math.min(currentAction.progressionCount, max) let msg = await roll.toMessage({ flavor: `Progression Roll for ${currentAction.name}, progression count : ${currentAction.progressionCount}/${max}` }, { rollMode: rollContext.visibility }) if (game?.dice3d) { await game.dice3d.waitFor3DAnimationByMessageID(msg.id) } if (roll.total <= max) { // Notify that the player can act now with a chat message let message = game.i18n.format("LETHALFANTASY.Notifications.messageProgressionOK", { isMonster, name: combatant.actor.name, weapon: currentAction.name, roll: roll.total }) ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) await combatant.setFlag(SYSTEM.id, "currentAction", "") combatant.actor.prepareRoll(currentAction.type === "weapon" ? "weapon-attack" : "spell-attack", currentAction._id) } else { // Notify that the player cannot act now with a chat message currentAction.progressionCount += 1 combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) let message = game.i18n.format("LETHALFANTASY.Notifications.messageProgressionKO", { isMonster, name: combatant.actor.name, weapon: currentAction.name, roll: roll.total }) ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) } } } } /* ***********************************************************/ static async promptRangedDefense(rollTarget) { const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); // v12 : 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 = `${modAbs}` } // 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.toUpperCase()}`, value: diceResult }) let diceSum = diceResult while (diceResult === maxValue) { let r = await new Roll(dice).evaluate() diceResult = r.dice[0].results[0].result diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 }) diceSum += (diceResult - 1) } 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 = `${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 "monster-skill": 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": case "spell-attack": case "spell-power": return `${game.i18n.localize("LETHALFANTASY.Label.spell")}` case "miracle": case "miracle-attack": case "miracle-power": return `${game.i18n.localize("LETHALFANTASY.Label.miracle")}` default: return game.i18n.localize("LETHALFANTASY.Label.titleStandard") } } /** @override */ async render(chatOptions = {}) { let chatData = await this._getChatCardData(chatOptions.isPrivate) console.log("ChatData", chatData) return await foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, chatData) } /* * Generates the data required for rendering a roll 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, badResult: this.badResult, 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 }, ) } }