Various fixes and changes based on tester feedback
This commit is contained in:
272
module/rolls.mjs
272
module/rolls.mjs
@@ -24,7 +24,7 @@ import { SYSTEM } from "./config/system.mjs"
|
||||
* @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 { bonus = 0, luckSpend = 0, supporters = 0, attrOverride, visibility, flavor, explodeOn5 = false } = options
|
||||
|
||||
const sys = actor.system
|
||||
const skillDef = SYSTEM.SKILLS[skillKey]
|
||||
@@ -52,7 +52,8 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
|
||||
// Roll the dice pool
|
||||
const roll = await new Roll(`${totalDice}d6`).evaluate()
|
||||
|
||||
// Count successes — exploding 6s produce additional dice
|
||||
// Count successes — exploding dice produce additional dice
|
||||
const explodeThreshold = explodeOn5 ? 5 : 6
|
||||
let successes = 0
|
||||
const diceResults = []
|
||||
let extraDice = 0
|
||||
@@ -60,7 +61,7 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
|
||||
for (const r of roll.dice[0].results) {
|
||||
const val = r.result
|
||||
if (val >= threshold) successes++
|
||||
if (val === 6) extraDice++
|
||||
if (val >= explodeThreshold) extraDice++
|
||||
diceResults.push({ val, exploded: false })
|
||||
}
|
||||
|
||||
@@ -70,12 +71,13 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
|
||||
for (const r of xRoll.dice[0].results) {
|
||||
const val = r.result
|
||||
if (val >= threshold) successes++
|
||||
if (val === 6) extraDice++
|
||||
if (val >= explodeThreshold) extraDice++
|
||||
diceResults.push({ val, exploded: true })
|
||||
}
|
||||
}
|
||||
|
||||
const isSuccess = successes >= dv
|
||||
const isOpposed = dv === 0
|
||||
const isSuccess = isOpposed ? null : 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)}`)
|
||||
@@ -89,18 +91,26 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
|
||||
}).join(" ")
|
||||
|
||||
// Build modifier summary
|
||||
const explodedCount = diceResults.filter(d => d.exploded).length
|
||||
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")}`)
|
||||
if (explodedCount > 0) modParts.push(`💥 ${explodedCount} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
|
||||
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 resultClass = isOpposed ? "roll-opposed" : isSuccess ? "roll-success" : "roll-failure"
|
||||
const resultLabel = isOpposed
|
||||
? game.i18n.localize("OATHHAMMER.Roll.Opposed")
|
||||
: 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 cardFlavor = flavor ?? (isOpposed
|
||||
? `${skillLabel} ${game.i18n.localize("OATHHAMMER.Roll.Check")} (${game.i18n.localize("OATHHAMMER.Roll.Opposed")})`
|
||||
: `${skillLabel} ${game.i18n.localize("OATHHAMMER.Roll.Check")} (DV ${dv})`)
|
||||
|
||||
const successDisplay = isOpposed ? String(successes) : `${successes} / ${dv}`
|
||||
|
||||
const content = `
|
||||
<div class="oh-roll-card">
|
||||
@@ -112,7 +122,7 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) {
|
||||
${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-successes">${successDisplay}</span>
|
||||
<span class="oh-roll-verdict">${resultLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -549,7 +559,247 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
|
||||
ChatMessage.applyRollMode(msgData, rollMode)
|
||||
await ChatMessage.create(msgData)
|
||||
|
||||
if (!isSuccess) {
|
||||
await actor.update({ "system.miracleBlocked": true })
|
||||
}
|
||||
|
||||
return { successes, dv, isSuccess }
|
||||
}
|
||||
|
||||
function _cap(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : "" }
|
||||
|
||||
// ============================================================
|
||||
// DEFENSE ROLL
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Roll a defense check (Agility/Might + Defense skill) and post to chat.
|
||||
*
|
||||
* @param {Actor} actor
|
||||
* @param {object} options From OathHammerDefenseDialog.prompt()
|
||||
*/
|
||||
export async function rollDefense(actor, options = {}) {
|
||||
const {
|
||||
attackType = "melee",
|
||||
attrRank = 0,
|
||||
attrChoice = "agility",
|
||||
redDice = false,
|
||||
traitBonus = 0,
|
||||
armorPenalty = 0,
|
||||
bonus = 0,
|
||||
visibility,
|
||||
} = options
|
||||
|
||||
const defRank = actor.system.skills.defense.rank
|
||||
const totalDice = Math.max(attrRank + defRank + traitBonus + armorPenalty + bonus, 1)
|
||||
const threshold = redDice ? 3 : 4
|
||||
const colorEmoji = redDice ? "🔴" : "⬜"
|
||||
|
||||
const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
|
||||
const diceHtml = _diceHtml(diceResults, threshold)
|
||||
|
||||
const attrLabel = game.i18n.localize(`OATHHAMMER.Attribute.${attrChoice.charAt(0).toUpperCase() + attrChoice.slice(1)}`)
|
||||
const skillLabel = game.i18n.localize("OATHHAMMER.Skill.Defense")
|
||||
const typeLabel = game.i18n.localize(attackType === "ranged" ? "OATHHAMMER.Dialog.DefenseRanged" : "OATHHAMMER.Dialog.DefenseMelee")
|
||||
|
||||
const modParts = []
|
||||
if (traitBonus > 0) modParts.push(`+${traitBonus} ${game.i18n.localize(attackType === "melee" ? "OATHHAMMER.WeaponTrait.Parry" : "OATHHAMMER.WeaponTrait.Block")}`)
|
||||
if (armorPenalty < 0) modParts.push(`${armorPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.ArmorPenalty")}`)
|
||||
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
|
||||
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
|
||||
|
||||
const content = `
|
||||
<div class="oh-roll-card oh-defense-card">
|
||||
<div class="oh-roll-header">
|
||||
<i class="fa-solid fa-shield-halved oh-defense-icon"></i>
|
||||
<span>${game.i18n.localize("OATHHAMMER.Roll.Defense")} — ${typeLabel}</span>
|
||||
</div>
|
||||
<div class="oh-roll-info">
|
||||
<span>${attrLabel} ${attrRank} + ${skillLabel} ${defRank}</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>
|
||||
<span class="oh-roll-verdict">${game.i18n.localize("OATHHAMMER.Roll.DefenseResult")}</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 }
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// WEAPON DEFENSE ROLL
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Roll a defense check triggered from a specific weapon, applying the
|
||||
* weapon's Parry / Block traits, and post to chat.
|
||||
*
|
||||
* Pool = (Agility or Might) + Defense skill
|
||||
* + traitBonus (Parry: +1 if two Parry weapons; Block: +1 vs ranged)
|
||||
* + armorPenalty (≤ 0)
|
||||
* + diminishPenalty (0 / −2 / −4 for 1st / 2nd / 3rd+ defense)
|
||||
* + bonus
|
||||
*
|
||||
* Parry trait → red dice (3+) when defending vs melee attacks
|
||||
* Block trait → red dice (3+) + +1 bonus when defending vs ranged attacks
|
||||
*
|
||||
* @param {Actor} actor
|
||||
* @param {Item} weapon The weapon used to defend
|
||||
* @param {object} options From OathHammerWeaponDialog.promptDefense()
|
||||
*/
|
||||
export async function rollWeaponDefense(actor, weapon, options = {}) {
|
||||
const {
|
||||
attackType = "melee",
|
||||
attrRank = 0,
|
||||
attrChoice = "agility",
|
||||
redDice = false,
|
||||
traitBonus = 0,
|
||||
armorPenalty = 0,
|
||||
diminishPenalty = 0,
|
||||
bonus = 0,
|
||||
visibility,
|
||||
} = options
|
||||
|
||||
const defRank = actor.system.skills.defense.rank
|
||||
const totalDice = Math.max(attrRank + defRank + traitBonus + armorPenalty + diminishPenalty + bonus, 1)
|
||||
const threshold = redDice ? 3 : 4
|
||||
const colorEmoji = redDice ? "🔴" : "⬜"
|
||||
|
||||
const { roll, successes, diceResults } = await _rollPool(totalDice, threshold)
|
||||
const diceHtml = _diceHtml(diceResults, threshold)
|
||||
|
||||
const attrLabel = game.i18n.localize(`OATHHAMMER.Attribute.${_cap(attrChoice)}`)
|
||||
const skillLabel = game.i18n.localize("OATHHAMMER.Skill.Defense")
|
||||
const typeLabel = game.i18n.localize(attackType === "ranged" ? "OATHHAMMER.Dialog.DefenseRanged" : "OATHHAMMER.Dialog.DefenseMelee")
|
||||
|
||||
const modParts = []
|
||||
if (traitBonus > 0) modParts.push(`+${traitBonus} ${game.i18n.localize(attackType === "melee" ? "OATHHAMMER.WeaponTrait.Parry" : "OATHHAMMER.WeaponTrait.Block")}`)
|
||||
if (armorPenalty < 0) modParts.push(`${armorPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.ArmorPenalty")}`)
|
||||
if (diminishPenalty < 0) modParts.push(`${diminishPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.DiminishingDefense")}`)
|
||||
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
|
||||
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
|
||||
|
||||
const content = `
|
||||
<div class="oh-roll-card oh-defense-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.Roll.Defense")} (${typeLabel})</span>
|
||||
</div>
|
||||
<div class="oh-roll-info">
|
||||
<span>${attrLabel} ${attrRank} + ${skillLabel} ${defRank}</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>
|
||||
<span class="oh-roll-verdict">${game.i18n.localize("OATHHAMMER.Roll.DefenseResult")}</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 }
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ARMOR ROLL
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Roll an armor saving roll (AV dice − AP) and post to chat.
|
||||
* Unlike other rolls, AP can reduce the pool to 0 (armor bypassed).
|
||||
* Each success reduces incoming damage by 1.
|
||||
*
|
||||
* @param {Actor} actor
|
||||
* @param {Item} armor
|
||||
* @param {object} options From OathHammerArmorDialog.prompt()
|
||||
*/
|
||||
export async function rollArmorSave(actor, armor, options = {}) {
|
||||
const {
|
||||
av = armor.system.armorValue ?? 0,
|
||||
isReinforced = false,
|
||||
apPenalty = 0,
|
||||
bonus = 0,
|
||||
visibility,
|
||||
} = options
|
||||
|
||||
// Armor CAN be reduced to 0 dice (fully bypassed by AP)
|
||||
const totalDice = Math.max(av + apPenalty + bonus, 0)
|
||||
const threshold = isReinforced ? 3 : 4
|
||||
const colorEmoji = isReinforced ? "🔴" : "⬜"
|
||||
|
||||
let successes = 0
|
||||
let diceHtml = `<em>${game.i18n.localize("OATHHAMMER.Roll.ArmorBypassed")}</em>`
|
||||
let roll
|
||||
|
||||
if (totalDice > 0) {
|
||||
const result = await _rollPool(totalDice, threshold)
|
||||
roll = result.roll
|
||||
successes = result.successes
|
||||
diceHtml = _diceHtml(result.diceResults, threshold)
|
||||
} else {
|
||||
// Zero dice — create a dummy roll with no results so Foundry can still attach it
|
||||
roll = new Roll("0d6")
|
||||
await roll.evaluate()
|
||||
}
|
||||
|
||||
const modParts = []
|
||||
if (apPenalty < 0) modParts.push(`AP ${apPenalty}`)
|
||||
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
|
||||
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
|
||||
|
||||
const content = `
|
||||
<div class="oh-roll-card oh-armor-card">
|
||||
<div class="oh-roll-header">
|
||||
<img src="${armor.img}" class="oh-card-weapon-img" alt="${armor.name}" />
|
||||
<span>${armor.name} (AV ${av})</span>
|
||||
</div>
|
||||
<div class="oh-roll-info">
|
||||
<span>${game.i18n.localize("OATHHAMMER.Roll.ArmorRoll")}</span>
|
||||
<span>${colorEmoji} ${totalDice}d6 (${threshold}+)</span>
|
||||
</div>
|
||||
${modLine}
|
||||
<div class="oh-roll-dice">${diceHtml}</div>
|
||||
<div class="oh-roll-result ${successes > 0 ? "roll-success" : ""}">
|
||||
<span class="oh-roll-successes">${successes} ${game.i18n.localize("OATHHAMMER.Roll.Successes")}</span>
|
||||
<span class="oh-roll-verdict">−${successes} ${game.i18n.localize("OATHHAMMER.Roll.Damage")}</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, totalDice }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user