diff --git a/css/fvtt-oath-hammer.css b/css/fvtt-oath-hammer.css index 4a95fc4..235ac44 100644 --- a/css/fvtt-oath-hammer.css +++ b/css/fvtt-oath-hammer.css @@ -2818,11 +2818,13 @@ gap: 6px; padding: 4px 6px; border-top: 1px solid rgba(83, 81, 40, 0.4); - background: rgba(83, 81, 40, 0.08); + border-bottom: 1px solid rgba(83, 81, 40, 0.4); + background: #f5ead0; flex-shrink: 0; flex-wrap: wrap; z-index: 1; position: relative; + pointer-events: auto; } .oh-free-roll-bar .oh-frb-label { font-family: "BlueDragon", "Palatino Linotype", serif; @@ -2849,6 +2851,7 @@ border: 1px solid #535128; border-radius: 3px; background: #fff; + color: #2a1a0a; cursor: pointer; } .oh-free-roll-bar .oh-frb-controls .oh-frb-pool { diff --git a/less/free-roll.less b/less/free-roll.less index 389eb6b..d240e1f 100644 --- a/less/free-roll.less +++ b/less/free-roll.less @@ -9,11 +9,13 @@ gap: 6px; padding: 4px 6px; border-top: 1px solid fade(@color-olive, 40%); - background: fade(@color-olive, 8%); + border-bottom: 1px solid fade(@color-olive, 40%); + background: @color-paper; flex-shrink: 0; flex-wrap: wrap; z-index: 1; position: relative; + pointer-events: auto; .oh-frb-label { font-family: @font-secondary; @@ -41,6 +43,7 @@ border: 1px solid @color-olive; border-radius: 3px; background: #fff; + color: @color-dark; cursor: pointer; } diff --git a/module/applications/free-roll.mjs b/module/applications/free-roll.mjs index b18913e..ec055dc 100644 --- a/module/applications/free-roll.mjs +++ b/module/applications/free-roll.mjs @@ -20,8 +20,12 @@ import { _rollPool, _diceHtml } from "../rolls.mjs" * @param {HTMLElement} html */ export function injectFreeRollBar(_chatLog, html) { + // Normalise: renderChatLog may pass jQuery (AppV1) or HTMLElement (AppV2/v13) + const el = (html instanceof HTMLElement) ? html : (html[0] ?? html) + if (!el?.querySelector) return + // Avoid double-injection on re-renders - if (html.querySelector(".oh-free-roll-bar")) return + if (el.querySelector(".oh-free-roll-bar")) return const bar = document.createElement("div") bar.className = "oh-free-roll-bar" @@ -49,20 +53,30 @@ export function injectFreeRollBar(_chatLog, html) { ` - bar.querySelector(".oh-frb-roll-btn").addEventListener("click", () => { + // Use event delegation on the bar container — direct child listeners can be + // swallowed by Foundry's own delegated click handlers in the sidebar. + bar.addEventListener("click", async (ev) => { + if (!ev.target.closest(".oh-frb-roll-btn")) return + ev.stopPropagation() const pool = parseInt(bar.querySelector(".oh-frb-pool").value) || 2 const color = bar.querySelector(".oh-frb-color").value const explode5 = bar.querySelector(".oh-frb-explode").checked - rollFree(pool, color, explode5) + try { + await rollFree(pool, color, explode5) + } catch (err) { + console.error("Oath Hammer | Free Roll error:", err) + ui.notifications?.error("Free Roll failed — see console") + } }) - // Insert before the chat form — use chatForm.parentElement for AppV2 compatibility - // (in v13 parts are nested inside the app element, not direct children) - const chatForm = html.querySelector(".chat-form") - if (chatForm) { - chatForm.parentElement.insertBefore(bar, chatForm) + // Insert before the chat form — try multiple selectors for v12/v13 compatibility + const anchor = el.querySelector(".chat-form") + ?? el.querySelector(".chat-message-form") + ?? el.querySelector("form") + if (anchor) { + anchor.parentElement.insertBefore(bar, anchor) } else { - html.appendChild(bar) + el.appendChild(bar) } } diff --git a/module/applications/sheets/npc-sheet.mjs b/module/applications/sheets/npc-sheet.mjs index a991b2e..e8e72e9 100644 --- a/module/applications/sheets/npc-sheet.mjs +++ b/module/applications/sheets/npc-sheet.mjs @@ -181,22 +181,20 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { } ) - const result = await foundry.applications.api.DialogV2.prompt({ - window: { title: attack.name, resizable: true }, - classes: ["fvtt-oath-hammer"], - position: { width: 420 }, - content, - ok: { label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-burst" }, + const result = await foundry.applications.api.DialogV2.wait({ + window: { title: attack.name, resizable: true }, + classes: ["fvtt-oath-hammer"], position: { width: 420 }, content, + rejectClose: false, + buttons: [{ label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-burst", + callback: (_ev, btn) => { const o = {}; for (const el of btn.form.elements) { if (!el.name) continue; o[el.name] = el.type === "checkbox" ? String(el.checked) : el.value } return o } + }], }) if (!result) return - const form = new DOMParser().parseFromString(result, "text/html") - const getValue = name => form.querySelector(`[name="${name}"]`)?.value - await rollNPCAttackDamage(this.document, attack, { - bonus: parseInt(getValue("bonus")) || 0, - explodeOn5: getValue("explodeOn5") === "true", - visibility: getValue("visibility"), + bonus: parseInt(result.bonus) || 0, + explodeOn5: result.explodeOn5 === "true", + visibility: result.visibility, }) } @@ -236,24 +234,22 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { } ) - const result = await foundry.applications.api.DialogV2.prompt({ - window: { title: spell.name, resizable: true }, - classes: ["fvtt-oath-hammer"], - position: { width: 420 }, - content, - ok: { label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-wand-sparkles" }, + const result = await foundry.applications.api.DialogV2.wait({ + window: { title: spell.name, resizable: true }, + classes: ["fvtt-oath-hammer"], position: { width: 420 }, content, + rejectClose: false, + buttons: [{ label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-wand-sparkles", + callback: (_ev, btn) => { const o = {}; for (const el of btn.form.elements) { if (!el.name) continue; o[el.name] = el.type === "checkbox" ? String(el.checked) : el.value } return o } + }], }) if (!result) return - const form = new DOMParser().parseFromString(result, "text/html") - const getValue = name => form.querySelector(`[name="${name}"]`)?.value - await rollNPCSpell(this.document, spell, { - dicePool: parseInt(getValue("dicePool")) || 3, - bonus: parseInt(getValue("bonus")) || 0, - colorOverride: getValue("colorOverride") || null, - explodeOn5: getValue("explodeOn5") === "true", - visibility: getValue("visibility"), + dicePool: parseInt(result.dicePool) || 3, + bonus: parseInt(result.bonus) || 0, + colorOverride: result.colorOverride || null, + explodeOn5: result.explodeOn5 === "true", + visibility: result.visibility, }) } @@ -282,23 +278,21 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { } ) - const result = await foundry.applications.api.DialogV2.prompt({ - window: { title: miracle.name, resizable: true }, - classes: ["fvtt-oath-hammer"], - position: { width: 420 }, - content, - ok: { label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-hands-praying" }, + const result = await foundry.applications.api.DialogV2.wait({ + window: { title: miracle.name, resizable: true }, + classes: ["fvtt-oath-hammer"], position: { width: 420 }, content, + rejectClose: false, + buttons: [{ label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-hands-praying", + callback: (_ev, btn) => { const o = {}; for (const el of btn.form.elements) { if (!el.name) continue; o[el.name] = el.type === "checkbox" ? String(el.checked) : el.value } return o } + }], }) if (!result) return - const form = new DOMParser().parseFromString(result, "text/html") - const getValue = name => form.querySelector(`[name="${name}"]`)?.value - await rollNPCMiracle(this.document, miracle, { - dicePool: parseInt(getValue("dicePool")) || 3, - bonus: parseInt(getValue("bonus")) || 0, - explodeOn5: getValue("explodeOn5") === "true", - visibility: getValue("visibility"), + dicePool: parseInt(result.dicePool) || 3, + bonus: parseInt(result.bonus) || 0, + explodeOn5: result.explodeOn5 === "true", + visibility: result.visibility, }) } @@ -335,23 +329,21 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { } ) - const result = await foundry.applications.api.DialogV2.prompt({ - window: { title: game.i18n.localize("OATHHAMMER.Label.ArmorDice"), resizable: true }, - classes: ["fvtt-oath-hammer"], - position: { width: 420 }, - content, - ok: { label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-dice-d6" }, + const result = await foundry.applications.api.DialogV2.wait({ + window: { title: game.i18n.localize("OATHHAMMER.Label.ArmorDice"), resizable: true }, + classes: ["fvtt-oath-hammer"], position: { width: 420 }, content, + rejectClose: false, + buttons: [{ label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-dice-d6", + callback: (_ev, btn) => { const o = {}; for (const el of btn.form.elements) { if (!el.name) continue; o[el.name] = el.type === "checkbox" ? String(el.checked) : el.value } return o } + }], }) if (!result) return - const form = new DOMParser().parseFromString(result, "text/html") - const getValue = name => form.querySelector(`[name="${name}"]`)?.value - await rollNPCArmor(actor, { - bonus: parseInt(getValue("bonus")) || 0, - colorOverride: getValue("colorOverride") || null, - explodeOn5: getValue("explodeOn5") === "true", - visibility: getValue("visibility"), + bonus: parseInt(result.bonus) || 0, + colorOverride: result.colorOverride || null, + explodeOn5: result.explodeOn5 === "true", + visibility: result.visibility, }) } @@ -395,23 +387,21 @@ export default class OathHammerNPCSheet extends OathHammerActorSheet { } ) - const result = await foundry.applications.api.DialogV2.prompt({ - window: { title: item.name, resizable: true }, - classes: ["fvtt-oath-hammer"], - position: { width: 420 }, - content, - ok: { label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-dice-d6" }, + const result = await foundry.applications.api.DialogV2.wait({ + window: { title: item.name, resizable: true }, + classes: ["fvtt-oath-hammer"], position: { width: 420 }, content, + rejectClose: false, + buttons: [{ label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-dice-d6", + callback: (_ev, btn) => { const o = {}; for (const el of btn.form.elements) { if (!el.name) continue; o[el.name] = el.type === "checkbox" ? String(el.checked) : el.value } return o } + }], }) if (!result) return - const form = new DOMParser().parseFromString(result, "text/html") - const getValue = name => form.querySelector(`[name="${name}"]`)?.value - await rollNPCSkill(this.document, item, { - bonus: parseInt(getValue("bonus")) || 0, - colorOverride: getValue("colorOverride") || null, - explodeOn5: getValue("explodeOn5") === "true", - visibility: getValue("visibility"), + bonus: parseInt(result.bonus) || 0, + colorOverride: result.colorOverride || null, + explodeOn5: result.explodeOn5 === "true", + visibility: result.visibility, }) } diff --git a/module/applications/sheets/regiment-sheet.mjs b/module/applications/sheets/regiment-sheet.mjs index b10a4fd..c7bb20b 100644 --- a/module/applications/sheets/regiment-sheet.mjs +++ b/module/applications/sheets/regiment-sheet.mjs @@ -163,19 +163,20 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet { visibility: game.settings.get("core", "rollMode") } ) - const result = await foundry.applications.api.DialogV2.prompt({ + const result = await foundry.applications.api.DialogV2.wait({ window: { title: `${doc.name} — ${game.i18n.localize("OATHHAMMER.Roll.ArmorRoll")}`, resizable: true }, classes: ["fvtt-oath-hammer"], position: { width: 420 }, content, - ok: { label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-dice-d6" }, + rejectClose: false, + buttons: [{ label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-dice-d6", + callback: (_ev, btn) => { const o = {}; for (const el of btn.form.elements) { if (!el.name) continue; o[el.name] = el.type === "checkbox" ? String(el.checked) : el.value } return o } + }], }) if (!result) return - const form = new DOMParser().parseFromString(result, "text/html") - const getValue = n => form.querySelector(`[name="${n}"]`)?.value await rollNPCArmor(doc, { - bonus: parseInt(getValue("bonus")) || 0, - colorOverride: getValue("colorOverride") || null, - explodeOn5: getValue("explodeOn5") === "true", - visibility: getValue("visibility"), + bonus: parseInt(result.bonus) || 0, + colorOverride: result.colorOverride || null, + explodeOn5: result.explodeOn5 === "true", + visibility: result.visibility, }) } @@ -200,19 +201,20 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet { visibility: game.settings.get("core", "rollMode") } ) - const result = await foundry.applications.api.DialogV2.prompt({ + const result = await foundry.applications.api.DialogV2.wait({ window: { title: `${skill.name} — ${game.i18n.localize("OATHHAMMER.Tab.Skills")}`, resizable: true }, classes: ["fvtt-oath-hammer"], position: { width: 420 }, content, - ok: { label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-dice-d6" }, + rejectClose: false, + buttons: [{ label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-dice-d6", + callback: (_ev, btn) => { const o = {}; for (const el of btn.form.elements) { if (!el.name) continue; o[el.name] = el.type === "checkbox" ? String(el.checked) : el.value } return o } + }], }) if (!result) return - const form = new DOMParser().parseFromString(result, "text/html") - const getValue = n => form.querySelector(`[name="${n}"]`)?.value await rollNPCSkill(this.document, skill, { - bonus: parseInt(getValue("bonus")) || 0, - colorOverride: getValue("colorOverride") || null, - explodeOn5: getValue("explodeOn5") === "true", - visibility: getValue("visibility"), + bonus: parseInt(result.bonus) || 0, + colorOverride: result.colorOverride || null, + explodeOn5: result.explodeOn5 === "true", + visibility: result.visibility, }) } @@ -244,19 +246,20 @@ export default class OathHammerRegimentSheet extends OathHammerActorSheet { visibility: game.settings.get("core", "rollMode") } ) - const result = await foundry.applications.api.DialogV2.prompt({ + const result = await foundry.applications.api.DialogV2.wait({ window: { title: `${attack.name} — ${game.i18n.localize("OATHHAMMER.Dialog.Damage")}`, resizable: true }, classes: ["fvtt-oath-hammer"], position: { width: 420 }, content, - ok: { label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-dice-d6" }, + rejectClose: false, + buttons: [{ label: game.i18n.localize("OATHHAMMER.Dialog.Roll"), icon: "fa-solid fa-dice-d6", + callback: (_ev, btn) => { const o = {}; for (const el of btn.form.elements) { if (!el.name) continue; o[el.name] = el.type === "checkbox" ? String(el.checked) : el.value } return o } + }], }) if (!result) return - const form = new DOMParser().parseFromString(result, "text/html") - const getValue = n => form.querySelector(`[name="${n}"]`)?.value await rollNPCAttackDamage(this.document, attack, { - bonus: parseInt(getValue("bonus")) || 0, - colorOverride: getValue("colorOverride") || null, - explodeOn5: getValue("explodeOn5") === "true", - visibility: getValue("visibility"), + bonus: parseInt(result.bonus) || 0, + colorOverride: result.colorOverride || null, + explodeOn5: result.explodeOn5 === "true", + visibility: result.visibility, }) } diff --git a/module/rolls.mjs b/module/rolls.mjs index f6b2195..c83092e 100644 --- a/module/rolls.mjs +++ b/module/rolls.mjs @@ -56,7 +56,7 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) { const allRolls = [roll] // Count successes — exploding dice produce additional dice - const explodeThreshold = explodeOn5 ? 5 : 6 + const explodeThreshold = explodeOn5 ? 5 : 6 // default: always explode on 6 let successes = 0 const diceResults = [] let extraDice = 0 @@ -97,9 +97,10 @@ export async function rollSkillCheck(actor, skillKey, dv, options = {}) { // 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 * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`) - if (supporters > 0) modParts.push(`+${supporters} ${game.i18n.localize("OATHHAMMER.Dialog.Supporters")}`) + if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`) + if (luckSpend > 0) modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`) + if (supporters > 0) modParts.push(`+${supporters} ${game.i18n.localize("OATHHAMMER.Dialog.Supporters")}`) + if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`) if (explodedCount > 0) modParts.push(`💥 ${explodedCount} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" @@ -184,7 +185,7 @@ export async function rollRarityCheck(actor, rarityKey, itemName) { * @returns {Promise<{roll: Roll, successes: number, diceResults: Array}>} */ export async function _rollPool(pool, threshold, explodeOn5 = false) { - const explodeThreshold = explodeOn5 ? 5 : 6 + const explodeThreshold = explodeOn5 ? 5 : 6 // default: always explode on 6 const roll = await new Roll(`${Math.max(pool, 1)}d6`).evaluate() const rolls = [roll] let successes = 0 @@ -274,6 +275,7 @@ export async function rollWeaponAttack(actor, weapon, options = {}) { if (autoAttackBonus > 0) modParts.push(`+${autoAttackBonus} auto`) 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 (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`) if (explodedCount > 0) modParts.push(`💥 ${explodedCount} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" @@ -473,6 +475,7 @@ export async function rollSpellCast(actor, spell, options = {}) { if (grimPenalty < 0) modParts.push(`${grimPenalty} ${game.i18n.localize("OATHHAMMER.Dialog.GrimoireNo")}`) if (luckSpend > 0) modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`) const explodedCountSpell = diceResults.filter(d => d.exploded).length + if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`) if (explodedCountSpell > 0) modParts.push(`💥 ${explodedCountSpell} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" @@ -567,6 +570,7 @@ export async function rollMiracleCast(actor, miracle, options = {}) { if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`) if (luckSpend > 0) modParts.push(`+${luckSpend * luckDicePerPoint} ${game.i18n.localize("OATHHAMMER.Dialog.LuckSpend")} (${luckSpend}LP${luckIsHuman ? " 👤" : ""})`) const explodedCountMiracle = diceResults.filter(d => d.exploded).length + if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`) if (explodedCountMiracle > 0) modParts.push(`💥 ${explodedCountMiracle} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" @@ -753,6 +757,7 @@ export async function rollWeaponDefense(actor, weapon, options = {}) { if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`) 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 (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`) if (explodedCountWDef > 0) modParts.push(`💥 ${explodedCountWDef} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" @@ -851,6 +856,7 @@ export async function rollArmorSave(actor, armor, options = {}) { if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`) 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 (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`) if (explodedCountArmor > 0) modParts.push(`💥 ${explodedCountArmor} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" @@ -936,6 +942,7 @@ export async function rollInitiativeCheck(actor, options = {}) { const modParts = [] if (bonus !== 0) modParts.push(`${bonus > 0 ? "+" : ""}${bonus} ${game.i18n.localize("OATHHAMMER.Dialog.Modifier")}`) const explodedCountInit = diceResults.filter(d => d.exploded).length + if (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`) if (explodedCountInit > 0) modParts.push(`💥 ${explodedCountInit} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : "" @@ -990,6 +997,7 @@ export async function rollNPCSkill(actor, skillItem, options = {}) { 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 (explodeOn5) modParts.push(`💥 ${game.i18n.localize("OATHHAMMER.Dialog.ExplodeOn5")}`) if (explodedCount > 0) modParts.push(`💥 ${explodedCount} ${game.i18n.localize("OATHHAMMER.Roll.Exploded")}`) const modLine = modParts.length ? `
${modParts.join(" · ")}
` : ""