/** * Free Dice Roll widget injected into the Foundry chat sidebar. * * Provides a compact bar for GM and players to roll any dice pool without * needing an actor โ€” useful for quick checks, table rolls, narration, etc. * * Features: * - Pool size (1โ€“20 dice) * - Color: White (4+), Red (3+), Black (2+) * - Explode on 5+ checkbox */ import { _rollPool, _diceHtml } from "../rolls.mjs" /** * Inject the Free Roll bar into the ChatLog HTML. * Called from `Hooks.on("renderChatLog", ...)`. * * @param {Application} _chatLog * @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 (el.querySelector(".oh-free-roll-bar")) return const bar = document.createElement("div") bar.className = "oh-free-roll-bar" bar.innerHTML = ` ${game.i18n.localize("OATHHAMMER.FreeRoll.Label")}
` // 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 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 โ€” 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 { el.appendChild(bar) } } /** * Execute a free dice roll and post the result to chat. * * @param {number} pool Number of d6 to roll * @param {string} colorType "white" | "red" | "black" * @param {boolean} explode5 True to explode on 5+ */ export async function rollFree(pool, colorType, explode5 = false) { const threshold = colorType === "black" ? 2 : colorType === "red" ? 3 : 4 const colorEmoji = colorType === "black" ? "โฌ›" : colorType === "red" ? "๐Ÿ”ด" : "โฌœ" const colorLabel = colorType === "black" ? game.i18n.localize("OATHHAMMER.FreeRoll.ColorBlack") : colorType === "red" ? game.i18n.localize("OATHHAMMER.FreeRoll.ColorRed") : game.i18n.localize("OATHHAMMER.FreeRoll.ColorWhite") const { rolls, successes, diceResults } = await _rollPool(pool, threshold, explode5) const explodedCount = diceResults.filter(d => d.exploded).length const diceHtml = _diceHtml(diceResults, threshold) const modParts = [] if (explode5) 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(" ยท ")}
` : "" const resultClass = successes > 0 ? "roll-success" : "roll-failure" const resultLabel = successes > 0 ? game.i18n.localize("OATHHAMMER.Roll.Success") : game.i18n.localize("OATHHAMMER.Roll.Failure") const content = `
${game.i18n.localize("OATHHAMMER.FreeRoll.CardTitle")}
${colorEmoji} ${pool}d6 (${threshold}+) — ${colorLabel}
${modLine}
${diceHtml}
${successes} ${resultLabel}
` const rollMode = game.settings.get("core", "rollMode") const msgData = { speaker: ChatMessage.getSpeaker(), content, rolls, sound: CONFIG.sounds.dice, } ChatMessage.applyRollMode(msgData, rollMode) await ChatMessage.create(msgData) }