Enhancements as per issue tracking sheet

This commit is contained in:
2026-03-19 15:39:25 +01:00
parent b2befe039e
commit b67d85c6be
22 changed files with 588 additions and 55 deletions

View File

@@ -183,7 +183,7 @@ export async function rollRarityCheck(actor, rarityKey, itemName) {
* @param {number} threshold Minimum value to count as a success
* @returns {Promise<{roll: Roll, successes: number, diceResults: Array}>}
*/
async function _rollPool(pool, threshold, explodeOn5 = false) {
export async function _rollPool(pool, threshold, explodeOn5 = false) {
const explodeThreshold = explodeOn5 ? 5 : 6
const roll = await new Roll(`${Math.max(pool, 1)}d6`).evaluate()
const rolls = [roll]
@@ -216,7 +216,7 @@ async function _rollPool(pool, threshold, explodeOn5 = false) {
/**
* Render dice results as HTML spans.
*/
function _diceHtml(diceResults, threshold) {
export 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>`
@@ -236,12 +236,12 @@ function _diceHtml(diceResults, threshold) {
* @param {object} options From OathHammerWeaponDialog.promptAttack()
*/
export async function rollWeaponAttack(actor, weapon, options = {}) {
const { attackBonus = 0, rangeCondition = 0, attrOverride, colorOverride, visibility, autoAttackBonus = 0, explodeOn5 = false } = options
const { attackBonus = 0, rangeCondition = 0, attrOverride, colorOverride, visibility, autoAttackBonus = 0, explodeOn5 = false, luckSpend = 0, luckIsHuman = false } = options
const sys = weapon.system
const actorSys = actor.system
const isRanged = !sys.usesMight && (sys.shortRange > 0 || sys.longRange > 0)
const isRanged = sys.proficiencyGroup === "bows" || sys.proficiencyGroup === "throwing"
const skillKey = isRanged ? "shooting" : "fighting"
const skillDef = SYSTEM.SKILLS[skillKey]
const defaultAttr = skillDef.attribute
@@ -254,7 +254,13 @@ export async function rollWeaponAttack(actor, weapon, options = {}) {
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 luckDicePerPoint = luckIsHuman ? 3 : 2
const totalDice = Math.max(attrRank + skillRank + attackBonus + rangeCondition + autoAttackBonus + (luckSpend * luckDicePerPoint), 1)
if (luckSpend > 0) {
const currentLuck = actorSys.luck?.value ?? 0
await actor.update({ "system.luck.value": Math.max(0, currentLuck - luckSpend) })
}
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
@@ -266,7 +272,9 @@ export async function rollWeaponAttack(actor, weapon, options = {}) {
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`)
if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
if (luckSpend > 0) modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`)
const explodedCount = diceResults.filter(d => d.exploded).length
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 content = `
@@ -342,6 +350,8 @@ export async function rollWeaponDamage(actor, weapon, options = {}) {
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 explodedCount = diceResults.filter(d => d.exploded).length
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 apNote = sys.ap > 0 ? `<span class="oh-ap-note">AP ${sys.ap}</span>` : ""
@@ -445,7 +455,8 @@ export async function rollSpellCast(actor, spell, options = {}) {
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")}`)
if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
const explodedCountSpell = diceResults.filter(d => d.exploded).length
if (explodedCountSpell > 0) modParts.push(`💥 ${explodedCountSpell} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const stressLine = `<div class="oh-stress-line${isBlocked ? " stress-blocked" : ""}">
@@ -530,7 +541,8 @@ export async function rollMiracleCast(actor, miracle, options = {}) {
const modParts = []
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
const explodedCountMiracle = diceResults.filter(d => d.exploded).length
if (explodedCountMiracle > 0) modParts.push(`💥 ${explodedCountMiracle} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const blockedLine = !isSuccess
@@ -618,6 +630,8 @@ export async function rollDefense(actor, options = {}) {
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 explodedCountDef = diceResults.filter(d => d.exploded).length
if (explodedCountDef > 0) modParts.push(`💥 ${explodedCountDef} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = `
@@ -685,13 +699,21 @@ export async function rollWeaponDefense(actor, weapon, options = {}) {
bonus = 0,
visibility,
explodeOn5 = false,
luckSpend = 0,
luckIsHuman = false,
} = options
const defRank = actor.system.skills.defense.rank
const totalDice = Math.max(attrRank + defRank + traitBonus + armorPenalty + diminishPenalty + bonus, 1)
const defRank = actor.system.skills.defense.rank
const luckDicePerPoint = luckIsHuman ? 3 : 2
const totalDice = Math.max(attrRank + defRank + traitBonus + armorPenalty + diminishPenalty + bonus + (luckSpend * luckDicePerPoint), 1)
const threshold = colorOverride === "black" ? 2 : colorOverride === "red" ? 3 : 4
const colorEmoji = colorOverride === "black" ? "⬛" : colorOverride === "red" ? "🔴" : "⬜"
if (luckSpend > 0) {
const currentLuck = actor.system.luck?.value ?? 0
await actor.update({ "system.luck.value": Math.max(0, currentLuck - luckSpend) })
}
const { roll, rolls, successes, diceResults } = await _rollPool(totalDice, threshold, explodeOn5)
const diceHtml = _diceHtml(diceResults, threshold)
@@ -704,7 +726,9 @@ export async function rollWeaponDefense(actor, weapon, options = {}) {
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")}`)
if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
if (luckSpend > 0) modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`)
const explodedCountWDef = diceResults.filter(d => d.exploded).length
if (explodedCountWDef > 0) modParts.push(`💥 ${explodedCountWDef} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = `
@@ -761,24 +785,34 @@ export async function rollArmorSave(actor, armor, options = {}) {
bonus = 0,
visibility,
explodeOn5 = false,
luckSpend = 0,
luckIsHuman = false,
} = options
// Armor CAN be reduced to 0 dice (fully bypassed by AP)
const totalDice = Math.max(av + apPenalty + bonus, 0)
const luckDicePerPoint = luckIsHuman ? 3 : 2
// Armor CAN be reduced to 0 dice (fully bypassed by AP) — luck can still rescue
const totalDice = Math.max(av + apPenalty + bonus + (luckSpend * luckDicePerPoint), 0)
const colorType = colorOverride || (isReinforced ? "red" : "white")
const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4
const colorEmoji = colorType === "black" ? "⬛" : colorType === "red" ? "🔴" : "⬜"
if (luckSpend > 0) {
const currentLuck = actor.system.luck?.value ?? 0
await actor.update({ "system.luck.value": Math.max(0, currentLuck - luckSpend) })
}
let successes = 0
let diceHtml = `<em>${game.i18n.localize("OATHHAMMER.Roll.ArmorBypassed")}</em>`
let roll
let rolls = []
let armorDiceResults = []
if (totalDice > 0) {
const result = await _rollPool(totalDice, threshold, explodeOn5)
roll = result.roll
rolls = result.rolls
successes = result.successes
armorDiceResults = result.diceResults
diceHtml = _diceHtml(result.diceResults, threshold)
} else {
// Zero dice — create a dummy roll with no results so Foundry can still attach it
@@ -790,7 +824,9 @@ export async function rollArmorSave(actor, armor, options = {}) {
const modParts = []
if (apPenalty < 0) modParts.push(`AP ${apPenalty}`)
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
if (luckSpend > 0) modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`)
const explodedCountArmor = armorDiceResults.filter(d => d.exploded).length
if (explodedCountArmor > 0) modParts.push(`💥 ${explodedCountArmor} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = `
@@ -866,7 +902,8 @@ export async function rollInitiativeCheck(actor, options = {}) {
const modParts = []
if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`)
if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`)
const explodedCountInit = diceResults.filter(d => d.exploded).length
if (explodedCountInit > 0) modParts.push(`💥 ${explodedCountInit} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`)
const modLine = modParts.length ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
const content = `