import { SYSTEM } from "../config/system.mjs" /** * D&D 5e style Roll system for Prism RPG * Simple 1d20 + modifier, with advantage/disadvantage */ export default class PrismRPGRoll extends Roll { static CHAT_TEMPLATE = "systems/fvtt-prism-rpg/templates/chat-message.hbs" /** * Calculate D&D 5e style ability modifier from ability score * @param {number} abilityScore The ability score value (3-18+) * @returns {number} The ability modifier */ static getAbilityModifier(abilityScore) { return Math.floor((abilityScore - 10) / 2) } // Getters for roll data 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 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 rollTotal() { return this.options.rollTotal } get rollTarget() { return this.options.rollTarget } get rollData() { return this.options.rollData } /** * D&D 5e style dice roll prompt * Formula: 1d20 + modifier, or damage dice + modifier * Advantage: 2d20kh, Disadvantage: 2d20kl * @param {Object} options Roll options * @returns {Promise} The roll result or null if cancelled */ static async prompt(options = {}) { let dice = "1d20" let hasModifier = true let hasAdvantage = true let isDamageRoll = false // Determine roll type and modifiers switch (options.rollType) { case "characteristic": options.rollName = options.rollTarget.name // Calculate D&D 5e modifier from characteristic value options.rollTarget.value = this.getAbilityModifier(options.rollTarget.value) break case "sub-attribute": options.rollName = options.rollTarget.name // Sub-attribute value is already a modifier (calculated in prepareDerivedData) break case "challenge": options.rollName = game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`) break case "save": options.rollName = `${game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`)} Save` // Calculate D&D 5e saving throw: ability modifier + proficiency bonus // Get the characteristic value from rollTarget const charValue = options.rollTarget.characteristicValue const abilityMod = this.getAbilityModifier(charValue) const saveBonus = options.rollTarget.saveBonus || 0 // Store separate values for display options.rollTarget.abilityModifier = abilityMod options.rollTarget.saveProficiency = saveBonus // Add the save bonus (proficiency) stored in saves options.rollTarget.value = abilityMod + saveBonus break case "skill": options.rollName = options.rollTarget.name // D&D 5e style: ability modifier + proficiency bonus const skillCharValue = options.rollTarget.characteristicValue const skillAbilityMod = this.getAbilityModifier(skillCharValue) const proficiency = options.rollTarget.proficiencyBonus || 0 options.rollTarget.value = skillAbilityMod + proficiency break case "weapon-attack": options.rollName = options.rollTarget.name if (options.rollTarget.weapon.system.weaponType === "melee") { options.rollTarget.value = options.rollTarget.combat.attackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus } else { options.rollTarget.value = options.rollTarget.combat.rangedAttackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus } break case "weapon-defense": options.rollName = options.rollTarget.name options.rollTarget.value = options.rollTarget.combat.defenseModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.defenseBonus break case "spell": case "spell-attack": case "spell-power": options.rollName = options.rollTarget.name options.rollTarget.value = options.rollTarget.actorModifiers.levelSpellModifier + options.rollTarget.actorModifiers.intSpellModifier break case "miracle": case "miracle-attack": case "miracle-power": options.rollName = options.rollTarget.name options.rollTarget.value = options.rollTarget.actorModifiers.levelMiracleModifier + options.rollTarget.actorModifiers.chaMiracleModifier break case "monster-attack": options.rollName = options.rollTarget.name options.rollTarget.value = options.rollTarget.attackModifier break case "monster-defense": options.rollName = options.rollTarget.name options.rollTarget.value = options.rollTarget.defenseModifier break case "monster-skill": options.rollName = game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`) break default: if (options.rollType.includes("weapon-damage")) { isDamageRoll = true hasAdvantage = false options.rollName = options.rollTarget.name let damageBonus = options.rollTarget.combat.damageModifier options.rollTarget.value = damageBonus + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.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/gi, "") } else if (options.rollType.includes("monster-damage")) { isDamageRoll = true hasAdvantage = false options.rollName = options.rollTarget.name options.rollTarget.value = options.rollTarget.damageModifier dice = options.rollTarget.damageDice.replace(/E/gi, "") } else if (options.rollType === "granted") { hasModifier = false hasAdvantage = false options.rollName = `Granted ${options.rollTarget.rollKey}` dice = options.rollTarget.formula } } // Setup dialog const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes) const choiceModifier = SYSTEM.CHOICE_MODIFIERS const choiceAdvantage = SYSTEM.ADVANTAGE_CHOICES let dialogContext = { rollType: options.rollType, rollTarget: options.rollTarget, rollName: options.rollName, actorName: options.actorName, rollModes, hasModifier, hasAdvantage, isDamageRoll, baseValue: options.rollTarget.value || 0, dice, choiceModifier, choiceAdvantage, hasTarget: options.hasTarget, modifier: "+0", advantage: "none" } const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-prism-rpg/templates/roll-dialog.hbs", dialogContext ) let position = game.user.getFlag(SYSTEM.id, "roll-dialog-pos") || { top: -1, left: -1 } const label = game.i18n.localize("PRISMRPG.Roll.roll") const rollContext = await foundry.applications.api.DialogV2.wait({ window: { title: "Roll dialog" }, classes: ["prismrpg"], content, position, buttons: [{ label: label, callback: (event, button, dialog) => { game.user.setFlag(SYSTEM.id, "roll-dialog-pos", foundry.utils.duplicate(dialog.position)) const output = Array.from(button.form.elements).reduce((obj, input) => { if (input.name) obj[input.name] = input.value return obj }, {}) return output }, }], rejectClose: false }) if (rollContext === null) return // Build D&D 5e formula: 1d20 + modifier let finalFormula = dice let totalModifier = 0 if (hasModifier) { let bonus = Number(options.rollTarget.value) || 0 let extraModifier = rollContext.modifier === "" ? 0 : parseInt(rollContext.modifier, 10) totalModifier = bonus + extraModifier if (totalModifier !== 0) { finalFormula = totalModifier > 0 ? `${dice} + ${totalModifier}` : `${dice} - ${Math.abs(totalModifier)}` } } // Apply advantage/disadvantage if (rollContext.advantage === "advantage" && !isDamageRoll) { finalFormula = finalFormula.replace(dice, `2${dice}kh`) } else if (rollContext.advantage === "disadvantage" && !isDamageRoll) { finalFormula = finalFormula.replace(dice, `2${dice}kl`) } 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: finalFormula, ...rollContext, } if (Hooks.call("fvtt-prism-rpg.preRoll", options, rollData) === false) return // Execute the roll let roll = new this(finalFormula, options.data, rollData) await roll.evaluate() // Store results roll.options.resultType = "success" roll.options.rollTotal = roll.total roll.options.rollTarget = options.rollTarget roll.options.titleFormula = finalFormula roll.options.rollData = foundry.utils.duplicate(rollData) if (Hooks.call("fvtt-prism-rpg.Roll", options, rollData, roll) === false) return return roll } /** @override */ async render(chatOptions = {}) { let chatData = await this._getChatCardData(chatOptions.isPrivate) return await foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, chatData) } 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, actingCharName: this.actorName, actingCharImg: this.actorImage, resultType: this.resultType, hasTarget: this.hasTarget, targetName: this.targetName, rollData: this.rollData, isPrivate: isPrivate } cardData.cssClass = cardData.css.join(" ") cardData.tooltip = isPrivate ? "" : await this.getTooltip() return cardData } async toMessage(messageData = {}, { rollMode, create = true } = {}) { super.toMessage( { isFailure: this.resultType === "failure", rollType: this.type, rollTarget: this.rollTarget, actingCharName: this.actorName, actingCharImg: this.actorImage, hasTarget: this.hasTarget, targetName: this.targetName, rollData: this.rollData, ...messageData, }, { rollMode: rollMode }, ) } }