342 lines
10 KiB
JavaScript
342 lines
10 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"
|
|
|
|
// 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
|
|
// Value already set in actor.mjs
|
|
break
|
|
|
|
case "challenge":
|
|
case "save":
|
|
options.rollName = game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`)
|
|
break
|
|
|
|
case "skill":
|
|
options.rollName = options.rollTarget.name
|
|
options.rollTarget.value = Math.floor(options.rollTarget.system.skillTotal / 10)
|
|
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 },
|
|
)
|
|
}
|
|
}
|