Files
fvtt-prism-rpg/module/documents/roll.mjs
T
2025-12-20 17:20:01 +01:00

374 lines
12 KiB
JavaScript

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<PrismRPGRoll|null>} 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 },
)
}
}