Files
fvtt-oath-hammer/module/applications/free-roll.mjs

139 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 (120 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 = `
<span class="oh-frb-label">
<i class="fa-solid fa-dice-d6"></i>
${game.i18n.localize("OATHHAMMER.FreeRoll.Label")}
</span>
<div class="oh-frb-controls">
<select class="oh-frb-pool" title="${game.i18n.localize("OATHHAMMER.FreeRoll.PoolTitle")}">
${Array.from({length: 16}, (_, i) => `<option value="${i+1}"${i+1===2?" selected":""}>${i+1}d</option>`).join("")}
</select>
<select class="oh-frb-color" title="${game.i18n.localize("OATHHAMMER.FreeRoll.ColorTitle")}">
<option value="white"> ${game.i18n.localize("OATHHAMMER.FreeRoll.ColorWhite")}</option>
<option value="red">🔴 ${game.i18n.localize("OATHHAMMER.FreeRoll.ColorRed")}</option>
<option value="black"> ${game.i18n.localize("OATHHAMMER.FreeRoll.ColorBlack")}</option>
</select>
<label class="oh-frb-explode-label" title="${game.i18n.localize("OATHHAMMER.FreeRoll.ExplodeTitle")}">
<input type="checkbox" class="oh-frb-explode">
💥 5+
</label>
<button type="button" class="oh-frb-roll-btn">
${game.i18n.localize("OATHHAMMER.FreeRoll.Roll")}
</button>
</div>
`
// 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 ? `<div class="oh-roll-mods">${modParts.join(" · ")}</div>` : ""
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 = `
<div class="oh-roll-card">
<div class="oh-roll-header">${game.i18n.localize("OATHHAMMER.FreeRoll.CardTitle")}</div>
<div class="oh-roll-info">
<span>${colorEmoji} ${pool}d6 (${threshold}+) &mdash; ${colorLabel}</span>
</div>
${modLine}
<div class="oh-roll-dice">${diceHtml}</div>
<div class="oh-roll-result ${resultClass}">
<span class="oh-roll-successes">${successes}</span>
<span class="oh-roll-verdict">${resultLabel}</span>
</div>
</div>
`
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)
}