Add roll windows from actor sheet
This commit is contained in:
555
module/rolls.mjs
Normal file
555
module/rolls.mjs
Normal file
@@ -0,0 +1,555 @@
|
||||
import { SYSTEM } from "./config/system.mjs"
|
||||
|
||||
/**
|
||||
* Perform an Oath Hammer skill check and post results to chat.
|
||||
*
|
||||
* Dice rules (p.38-40):
|
||||
* - Pool = Attribute rank + Skill rank + per-skill modifier + bonus + (luckSpend × 2) + supporters
|
||||
* - White dice succeed on 4+ | Red: 3+ | Black: 2+
|
||||
* - All dice explode on 6 (roll extra die, continues while rolling 6s)
|
||||
* - Pool can never drop below 1 die (penalties rule)
|
||||
* - Luck Points: 1 LP spent = +2 dice; LP are deducted from actor.system.luck.value
|
||||
* - Supporters: each ally with ranks in the skill adds +1 die
|
||||
*
|
||||
* @param {Actor} actor The actor performing the check
|
||||
* @param {string} skillKey Skill key (e.g. "fortune")
|
||||
* @param {number} dv Difficulty Value (successes required)
|
||||
* @param {object} [options]
|
||||
* @param {number} [options.bonus] Extra dice from dialog modifier (can be negative)
|
||||
* @param {number} [options.luckSpend] Luck Points to spend (each adds +2 dice)
|
||||
* @param {number} [options.supporters] Allies supporting the check (each adds +1 die)
|
||||
* @param {string} [options.attrOverride] Override governing attribute (for dual-attr skills)
|
||||
* @param {string} [options.visibility] Roll mode (public/gmroll/blindroll/selfroll)
|
||||
* @param {string} [options.flavor] Optional flavor text for the chat card
|
||||
* @returns {Promise<{successes: number, dv: number, isSuccess: boolean}>}
|
||||
*/
|
||||
export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
|
||||
const { bonus = 0, luckSpend = 0, supporters = 0, attrOverride, visibility, flavor } = options
|
||||
|
||||
const sys = actor.system
|
||||
const skillDef = SYSTEM.SKILLS[skillKey]
|
||||
if (!skillDef) throw new Error(`Unknown skill: ${skillKey}`)
|
||||
|
||||
// Attribute — use override if provided (dual-attribute skills: Defense, Fighting, Magic)
|
||||
const attrKey = attrOverride && sys.attributes[attrOverride] ? attrOverride : skillDef.attribute
|
||||
const attrRank = sys.attributes[attrKey].rank
|
||||
|
||||
const skill = sys.skills[skillKey]
|
||||
const skillRank = skill.rank
|
||||
const skillMod = skill.modifier ?? 0
|
||||
const colorType = skill.colorDiceType ?? "white"
|
||||
const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
|
||||
|
||||
// Total dice pool (never below 1)
|
||||
const totalDice = Math.max(attrRank + skillRank + skillMod + bonus + (luckSpend * 2) + supporters, 1)
|
||||
|
||||
// Deduct spent Luck Points from actor
|
||||
if (luckSpend > 0) {
|
||||
const currentLuck = sys.luck?.value ?? 0
|
||||
await actor.update({ "system.luck.value": Math.max(0, currentLuck - luckSpend) })
|
||||
}
|
||||
|
||||
// Roll the dice pool
|
||||
const roll = await new Roll(`${totalDice}d6`).evaluate()
|
||||
|
||||
// Count successes — exploding 6s produce additional dice
|
||||
let successes = 0
|
||||
const diceResults = []
|
||||
let extraDice = 0
|
||||
|
||||
for (const r of roll.dice[0].results) {
|
||||
const val = r.result
|
||||
if (val >= threshold) successes++
|
||||
if (val === 6) extraDice++
|
||||
diceResults.push({ val, exploded: false })
|
||||
}
|
||||
|
||||
while (extraDice > 0) {
|
||||
const xRoll = await new Roll(`${extraDice}d6`).evaluate()
|
||||
extraDice = 0
|
||||
for (const r of xRoll.dice[0].results) {
|
||||
const val = r.result
|
||||
if (val >= threshold) successes++
|
||||
if (val === 6) extraDice++
|
||||
diceResults.push({ val, exploded: true })
|
||||
}
|
||||
}
|
||||
|
||||
const isSuccess = successes >= dv
|
||||
const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
|
||||
const skillLabel = game.i18n.localize(skillDef.label)
|
||||
const attrLabel = game.i18n.localize(`OATHHAMMER.Attribute.${attrKey.charAt(0).toUpperCase()}${attrKey.slice(1)}`)
|
||||
|
||||
// Build dice display HTML
|
||||
const diceHtml = diceResults.map(({ val, exploded }) => {
|
||||
const success = val >= threshold
|
||||
const cssClass = success ? "die-success" : "die-fail"
|
||||
const explodedClass = exploded ? " die-exploded" : ""
|
||||
return `<span class="oh-die ${cssClass}${explodedClass}" title="${exploded ? "💥 Exploded" : ""}">${val}</span>`
|
||||
}).join(" ")
|
||||
|
||||
// Build modifier summary
|
||||
const modParts = []
|
||||
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
|
||||
if (luckSpend > 0) modParts.push(`+${luckSpend * 2} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP)`)
|
||||
if (supporters > 0) modParts.push(`+${supporters} ${game.i18n.localize("OATHHAMMER.Dialog.Supporters")}`)
|
||||
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
|
||||
|
||||
const resultClass = isSuccess ? "roll-success" : "roll-failure"
|
||||
const resultLabel = isSuccess
|
||||
? game.i18n.localize("OATHHAMMER.Roll.Success")
|
||||
: game.i18n.localize("OATHHAMMER.Roll.Failure")
|
||||
|
||||
const cardFlavor = flavor ?? `${skillLabel} ${game.i18n.localize("OATHHAMMER.Roll.Check")} (DV ${dv})`
|
||||
|
||||
const content = `
|
||||
<div class="oh-roll-card">
|
||||
<div class="oh-roll-header">${cardFlavor}</div>
|
||||
<div class="oh-roll-info">
|
||||
<span>${attrLabel} ${attrRank} + ${skillLabel} ${skillRank}</span>
|
||||
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
|
||||
</div>
|
||||
${modLine}
|
||||
<div class="oh-roll-dice">${diceHtml}</div>
|
||||
<div class="oh-roll-result ${resultClass}">
|
||||
<span class="oh-roll-successes">${successes} / ${dv}</span>
|
||||
<span class="oh-roll-verdict">${resultLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
const rollMode = visibility ?? game.settings.get("core", "rollMode")
|
||||
const msgData = {
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
content,
|
||||
rolls: [roll],
|
||||
sound: CONFIG.sounds.dice,
|
||||
}
|
||||
ChatMessage.applyRollMode(msgData, rollMode)
|
||||
await ChatMessage.create(msgData)
|
||||
|
||||
return { successes, dv, isSuccess }
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll a Fortune check to find an item of a given rarity.
|
||||
* Used by the rollable rarity button on item sheets.
|
||||
* @param {Actor} actor The actor making the check
|
||||
* @param {string} rarityKey Rarity key (e.g. "rare", "very-rare")
|
||||
* @param {string} [itemName] Optional item name for flavor text
|
||||
*/
|
||||
export async function rollRarityCheck(actor, rarityKey, itemName) {
|
||||
const dv = SYSTEM.RARITY_DV[rarityKey] ?? 1
|
||||
if (rarityKey === "always") {
|
||||
const rarityLabel = game.i18n.localize(SYSTEM.RARITY_CHOICES[rarityKey])
|
||||
const content = `
|
||||
<div class="oh-roll-card">
|
||||
<div class="oh-roll-header">${itemName ?? game.i18n.localize("OATHHAMMER.Label.Rarity")}</div>
|
||||
<div class="oh-roll-result roll-success">
|
||||
<span class="oh-roll-verdict">${rarityLabel} — ${game.i18n.localize("OATHHAMMER.Roll.AutoSuccess")}</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor }), content })
|
||||
return { successes: 0, dv: 0, isSuccess: true }
|
||||
}
|
||||
|
||||
const rarityLabel = game.i18n.localize(SYSTEM.RARITY_CHOICES[rarityKey])
|
||||
const flavor = `${game.i18n.localize("OATHHAMMER.Skill.Fortune")} — ${itemName ?? rarityLabel} (DV ${dv})`
|
||||
return rollSkillCheck(actor, "fortune", dv, { flavor })
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SHARED DICE HELPER
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Roll a pool of dice, counting successes (including exploding 6s).
|
||||
* @param {number} pool Number of dice to roll
|
||||
* @param {number} threshold Minimum value to count as a success
|
||||
* @returns {Promise<{roll: Roll, successes: number, diceResults: Array}>}
|
||||
*/
|
||||
async function _rollPool(pool, threshold) {
|
||||
const roll = await new Roll(`${Math.max(pool, 1)}d6`).evaluate()
|
||||
let successes = 0
|
||||
const diceResults = []
|
||||
let extraDice = 0
|
||||
|
||||
for (const r of roll.dice[0].results) {
|
||||
const val = r.result
|
||||
if (val >= threshold) successes++
|
||||
if (val === 6) extraDice++
|
||||
diceResults.push({ val, exploded: false })
|
||||
}
|
||||
|
||||
while (extraDice > 0) {
|
||||
const xRoll = await new Roll(`${extraDice}d6`).evaluate()
|
||||
extraDice = 0
|
||||
for (const r of xRoll.dice[0].results) {
|
||||
const val = r.result
|
||||
if (val >= threshold) successes++
|
||||
if (val === 6) extraDice++
|
||||
diceResults.push({ val, exploded: true })
|
||||
}
|
||||
}
|
||||
|
||||
return { roll, successes, diceResults }
|
||||
}
|
||||
|
||||
/**
|
||||
* Render dice results as HTML spans.
|
||||
*/
|
||||
function _diceHtml(diceResults, threshold) {
|
||||
return diceResults.map(({ val, exploded }) => {
|
||||
const cssClass = val >= threshold ? "die-success" : "die-fail"
|
||||
return `<span class="oh-die ${cssClass}${exploded ? " die-exploded" : ""}" title="${exploded ? "💥" : ""}">${val}</span>`
|
||||
}).join(" ")
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// WEAPON ATTACK ROLL
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Roll a weapon attack and post the result to chat.
|
||||
* The chat card includes a "Roll Damage" button that triggers rollWeaponDamage.
|
||||
*
|
||||
* @param {Actor} actor The attacking actor
|
||||
* @param {Item} weapon The weapon item
|
||||
* @param {object} options From OathHammerWeaponDialog.promptAttack()
|
||||
*/
|
||||
export async function rollWeaponAttack(actor, weapon, options = {}) {
|
||||
const { attackBonus = 0, rangeCondition = 0, attrOverride, visibility, autoAttackBonus = 0 } = options
|
||||
|
||||
const sys = weapon.system
|
||||
const actorSys = actor.system
|
||||
|
||||
const isRanged = !sys.usesMight && (sys.shortRange > 0 || sys.longRange > 0)
|
||||
const skillKey = isRanged ? "shooting" : "fighting"
|
||||
const skillDef = SYSTEM.SKILLS[skillKey]
|
||||
const defaultAttr = skillDef.attribute
|
||||
|
||||
const attrKey = attrOverride && actorSys.attributes[attrOverride] ? attrOverride : defaultAttr
|
||||
const attrRank = actorSys.attributes[attrKey].rank
|
||||
const skillRank = actorSys.skills[skillKey].rank
|
||||
const colorType = actorSys.skills[skillKey].colorDiceType ?? "white"
|
||||
const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
|
||||
const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
|
||||
|
||||
const totalDice = Math.max(attrRank + skillRank + attackBonus + rangeCondition + autoAttackBonus, 1)
|
||||
|
||||
const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
|
||||
|
||||
const skillLabel = game.i18n.localize(skillDef.label)
|
||||
const attrLabel = game.i18n.localize(`OATHHAMMER.Attribute.${attrKey.charAt(0).toUpperCase() + attrKey.slice(1)}`)
|
||||
const diceHtml = _diceHtml(diceResults, threshold)
|
||||
|
||||
// Modifier summary
|
||||
const modParts = []
|
||||
if (attackBonus !== 0) modParts.push(`${attackBonus > 0 ? "+" : ""}${attackBonus} ${game.i18n.localize("OATHHAMMER.Dialog.AttackModifier")}`)
|
||||
if (rangeCondition !== 0) modParts.push(`${rangeCondition} ${game.i18n.localize("OATHHAMMER.Dialog.RangeCondition")}`)
|
||||
if (autoAttackBonus > 0) modParts.push(`+${autoAttackBonus} auto`)
|
||||
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
|
||||
|
||||
const content = `
|
||||
<div class="oh-roll-card oh-weapon-card">
|
||||
<div class="oh-roll-header">
|
||||
<img src="${weapon.img}" class="oh-card-weapon-img" alt="${weapon.name}" />
|
||||
<span>${weapon.name} — ${game.i18n.localize("OATHHAMMER.Dialog.Attack")}</span>
|
||||
</div>
|
||||
<div class="oh-roll-info">
|
||||
<span>${skillLabel} (${attrLabel} ${attrRank}) + ${skillLabel} ${skillRank}</span>
|
||||
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
|
||||
</div>
|
||||
${modLine}
|
||||
<div class="oh-roll-dice">${diceHtml}</div>
|
||||
<div class="oh-roll-result">
|
||||
<span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
|
||||
</div>
|
||||
<div class="oh-weapon-damage-btn-row">
|
||||
<button type="button" class="oh-roll-damage-btn" data-action="rollWeaponDamage">
|
||||
⚔ ${game.i18n.localize("OATHHAMMER.Roll.RollDamage")} (SV ${successes})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
const rollMode = visibility ?? game.settings.get("core", "rollMode")
|
||||
const flagData = { actorUuid: actor.uuid, weaponUuid: weapon.uuid, attackSuccesses: successes }
|
||||
const msgData = {
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
content,
|
||||
rolls: [roll],
|
||||
sound: CONFIG.sounds.dice,
|
||||
flags: { "fvtt-oath-hammer": { weaponAttack: flagData } },
|
||||
}
|
||||
ChatMessage.applyRollMode(msgData, rollMode)
|
||||
await ChatMessage.create(msgData)
|
||||
|
||||
return { successes }
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// WEAPON DAMAGE ROLL
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Roll weapon damage and post to chat.
|
||||
*
|
||||
* @param {Actor} actor The attacking actor
|
||||
* @param {Item} weapon The weapon item
|
||||
* @param {object} options From OathHammerWeaponDialog.promptDamage()
|
||||
*/
|
||||
export async function rollWeaponDamage(actor, weapon, options = {}) {
|
||||
const { sv = 0, damageBonus = 0, visibility, autoDamageBonus = 0 } = options
|
||||
|
||||
const sys = weapon.system
|
||||
const actorSys = actor.system
|
||||
|
||||
const hasBrutal = sys.traits.has("brutal")
|
||||
const hasDeadly = sys.traits.has("deadly")
|
||||
const colorType = hasDeadly ? "black" : hasBrutal ? "red" : "white"
|
||||
const threshold = hasDeadly ? 2 : hasBrutal ? 3 : 4
|
||||
const colorEmoji = hasDeadly ? "⬛" : hasBrutal ? "🔴" : "⬜"
|
||||
const colorLabel = hasDeadly ? "Black" : hasBrutal ? "Red" : "White"
|
||||
|
||||
const mightRank = actorSys.attributes.might.rank
|
||||
const baseDamageDice = sys.usesMight ? Math.max(mightRank + sys.damageMod, 1) : Math.max(sys.damageMod, 1)
|
||||
const totalDice = Math.max(baseDamageDice + sv + damageBonus + autoDamageBonus, 1)
|
||||
|
||||
const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
|
||||
const diceHtml = _diceHtml(diceResults, threshold)
|
||||
|
||||
const modParts = []
|
||||
if (sv > 0) modParts.push(`+${sv} SV`)
|
||||
if (damageBonus !== 0) modParts.push(`${damageBonus > 0 ? "+" : ""}${damageBonus} ${game.i18n.localize("OATHHAMMER.Dialog.DamageModifier")}`)
|
||||
if (autoDamageBonus > 0) modParts.push(`+${autoDamageBonus} auto`)
|
||||
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
|
||||
|
||||
const apNote = sys.ap > 0 ? `<span class="oh-ap-note">AP ${sys.ap}</span>` : ""
|
||||
|
||||
const content = `
|
||||
<div class="oh-roll-card oh-weapon-card">
|
||||
<div class="oh-roll-header">
|
||||
<img src="${weapon.img}" class="oh-card-weapon-img" alt="${weapon.name}" />
|
||||
<span>${weapon.name} — ${game.i18n.localize("OATHHAMMER.Dialog.Damage")}</span>
|
||||
</div>
|
||||
<div class="oh-roll-info">
|
||||
<span>${sys.damageLabel} = ${baseDamageDice}d6 ${sv > 0 ? `+${sv} SV` : ""}</span>
|
||||
<span>${colorEmoji} ${totalDice}d6 (${threshold}+) ${colorLabel}</span>
|
||||
</div>
|
||||
${modLine}
|
||||
<div class="oh-roll-dice">${diceHtml}</div>
|
||||
<div class="oh-roll-result">
|
||||
<span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Damage")}</span>
|
||||
${apNote}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
const rollMode = visibility ?? game.settings.get("core", "rollMode")
|
||||
const msgData = {
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
content,
|
||||
rolls: [roll],
|
||||
sound: CONFIG.sounds.dice,
|
||||
}
|
||||
ChatMessage.applyRollMode(msgData, rollMode)
|
||||
await ChatMessage.create(msgData)
|
||||
|
||||
return { successes }
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// SPELL CAST ROLL
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Roll a spell casting check (Magic / Intelligence) and post to chat.
|
||||
* Counts dice showing 1 and adds Arcane Stress to the actor.
|
||||
*
|
||||
* @param {Actor} actor The caster
|
||||
* @param {Item} spell The spell item
|
||||
* @param {object} options From OathHammerSpellDialog.prompt()
|
||||
*/
|
||||
export async function rollSpellCast(actor, spell, options = {}) {
|
||||
const {
|
||||
dv = spell.system.difficultyValue,
|
||||
enhancement = "none",
|
||||
stressCost = 0,
|
||||
poolPenalty = 0,
|
||||
redDice = false,
|
||||
noStress = false,
|
||||
elementalBonus = 0,
|
||||
bonus = 0,
|
||||
grimPenalty = 0,
|
||||
visibility,
|
||||
} = options
|
||||
|
||||
const sys = spell.system
|
||||
const actorSys = actor.system
|
||||
|
||||
const intRank = actorSys.attributes.intelligence.rank
|
||||
const magicRank = actorSys.skills.magic.rank
|
||||
const totalDice = Math.max(intRank + magicRank + bonus + poolPenalty + elementalBonus + grimPenalty, 1)
|
||||
const threshold = redDice ? 3 : 4
|
||||
const colorEmoji = redDice ? "🔴" : "⬜"
|
||||
|
||||
const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
|
||||
const diceHtml = _diceHtml(diceResults, threshold)
|
||||
|
||||
// Count 1s for Arcane Stress (unless Safe Spell enhancement)
|
||||
const onesCount = noStress ? 0 : diceResults.filter(d => d.val === 1 && !d.exploded).length
|
||||
const totalStressGain = stressCost + onesCount
|
||||
const isSuccess = successes >= dv
|
||||
|
||||
// Update arcane stress
|
||||
if (totalStressGain > 0) {
|
||||
const currentStress = actorSys.arcaneStress.value
|
||||
await actor.update({ "system.arcaneStress.value": currentStress + totalStressGain })
|
||||
}
|
||||
|
||||
const newStress = (actorSys.arcaneStress.value ?? 0) + totalStressGain
|
||||
const stressMax = actorSys.arcaneStress.threshold
|
||||
const isBlocked = newStress >= stressMax
|
||||
|
||||
const skillLabel = game.i18n.localize("OATHHAMMER.Skill.Magic")
|
||||
const attrLabel = game.i18n.localize("OATHHAMMER.Attribute.Intelligence")
|
||||
const resultClass = isSuccess ? "roll-success" : "roll-failure"
|
||||
const resultLabel = isSuccess
|
||||
? game.i18n.localize("OATHHAMMER.Roll.Success")
|
||||
: game.i18n.localize("OATHHAMMER.Roll.Failure")
|
||||
|
||||
const modParts = []
|
||||
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
|
||||
if (poolPenalty !== 0) modParts.push(`${poolPenalty} ${game.i18n.localize("OATHHAMMER.Enhancement." + _cap(enhancement))}`)
|
||||
if (elementalBonus > 0) modParts.push(`+${elementalBonus} ${game.i18n.localize("OATHHAMMER.Dialog.ElementMet")}`)
|
||||
if (grimPenalty < 0) modParts.push(`${grimPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.GrimoireNo")}`)
|
||||
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
|
||||
|
||||
const stressLine = `<div class="oh-stress-line${isBlocked ? " stress-blocked" : ""}">
|
||||
🧠 ${game.i18n.localize("OATHHAMMER.Label.ArcaneStress")}: +${totalStressGain}
|
||||
(${onesCount} × 1s + ${stressCost} enh.) → <strong>${newStress}/${stressMax}</strong>
|
||||
${isBlocked ? ` ⚠ ${game.i18n.localize("OATHHAMMER.Label.StressBlocked")}` : ""}
|
||||
</div>`
|
||||
|
||||
const content = `
|
||||
<div class="oh-roll-card oh-spell-card">
|
||||
<div class="oh-roll-header">
|
||||
<img src="${spell.img}" class="oh-card-weapon-img" alt="${spell.name}" />
|
||||
<span>${spell.name} (DV ${dv})</span>
|
||||
</div>
|
||||
<div class="oh-roll-info">
|
||||
<span>${attrLabel} ${intRank} + ${skillLabel} ${magicRank}</span>
|
||||
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
|
||||
</div>
|
||||
${modLine}
|
||||
<div class="oh-roll-dice">${diceHtml}</div>
|
||||
<div class="oh-roll-result ${resultClass}">
|
||||
<span class="oh-roll-successes">${successes} / ${dv}</span>
|
||||
<span class="oh-roll-verdict">${resultLabel}</span>
|
||||
</div>
|
||||
${stressLine}
|
||||
</div>
|
||||
`
|
||||
|
||||
const rollMode = visibility ?? game.settings.get("core", "rollMode")
|
||||
const msgData = {
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
content,
|
||||
rolls: [roll],
|
||||
sound: CONFIG.sounds.dice,
|
||||
}
|
||||
ChatMessage.applyRollMode(msgData, rollMode)
|
||||
await ChatMessage.create(msgData)
|
||||
|
||||
return { successes, dv, isSuccess, totalStressGain, newStress }
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// MIRACLE CAST ROLL
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Roll a miracle invocation check (Magic / Willpower) and post to chat.
|
||||
* On failure, warns the player they are blocked from miracles for the day.
|
||||
*
|
||||
* @param {Actor} actor The caster
|
||||
* @param {Item} miracle The miracle item
|
||||
* @param {object} options From OathHammerMiracleDialog.prompt()
|
||||
*/
|
||||
export async function rollMiracleCast(actor, miracle, options = {}) {
|
||||
const {
|
||||
dv = 1,
|
||||
isRitual = false,
|
||||
bonus = 0,
|
||||
visibility,
|
||||
} = options
|
||||
|
||||
const sys = miracle.system
|
||||
const actorSys = actor.system
|
||||
|
||||
const wpRank = actorSys.attributes.willpower.rank
|
||||
const magicRank = actorSys.skills.magic.rank
|
||||
const totalDice = Math.max(wpRank + magicRank + bonus, 1)
|
||||
const threshold = 4
|
||||
const colorEmoji = "⬜"
|
||||
|
||||
const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
|
||||
const diceHtml = _diceHtml(diceResults, threshold)
|
||||
const isSuccess = successes >= dv
|
||||
|
||||
const skillLabel = game.i18n.localize("OATHHAMMER.Skill.Magic")
|
||||
const attrLabel = game.i18n.localize("OATHHAMMER.Attribute.Willpower")
|
||||
const resultClass = isSuccess ? "roll-success" : "roll-failure"
|
||||
const resultLabel = isSuccess
|
||||
? game.i18n.localize("OATHHAMMER.Roll.Success")
|
||||
: game.i18n.localize("OATHHAMMER.Roll.Failure")
|
||||
|
||||
const modLine = bonus !== 0
|
||||
? `<div class="oh-roll-mods">${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}</div>`
|
||||
: ""
|
||||
|
||||
const blockedLine = !isSuccess
|
||||
? `<div class="oh-miracle-blocked">⚠ ${game.i18n.localize("OATHHAMMER.Roll.MiracleBlocked")}</div>`
|
||||
: ""
|
||||
|
||||
const dvNote = isRitual
|
||||
? `DV ${dv} (${game.i18n.localize("OATHHAMMER.Label.Ritual")})`
|
||||
: `DV ${dv}`
|
||||
|
||||
const content = `
|
||||
<div class="oh-roll-card oh-miracle-card">
|
||||
<div class="oh-roll-header">
|
||||
<img src="${miracle.img}" class="oh-card-weapon-img" alt="${miracle.name}" />
|
||||
<span>${miracle.name} (${dvNote})</span>
|
||||
</div>
|
||||
<div class="oh-roll-info">
|
||||
<span>${attrLabel} ${wpRank} + ${skillLabel} ${magicRank}</span>
|
||||
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
|
||||
</div>
|
||||
${modLine}
|
||||
<div class="oh-roll-dice">${diceHtml}</div>
|
||||
<div class="oh-roll-result ${resultClass}">
|
||||
<span class="oh-roll-successes">${successes} / ${dv}</span>
|
||||
<span class="oh-roll-verdict">${resultLabel}</span>
|
||||
</div>
|
||||
${blockedLine}
|
||||
</div>
|
||||
`
|
||||
|
||||
const rollMode = visibility ?? game.settings.get("core", "rollMode")
|
||||
const msgData = {
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
content,
|
||||
rolls: [roll],
|
||||
sound: CONFIG.sounds.dice,
|
||||
}
|
||||
ChatMessage.applyRollMode(msgData, rollMode)
|
||||
await ChatMessage.create(msgData)
|
||||
|
||||
return { successes, dv, isSuccess }
|
||||
}
|
||||
|
||||
function _cap(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : "" }
|
||||
Reference in New Issue
Block a user