/** * Free Dice Tray — injected into the Foundry chat sidebar. * * Provides a compact bar for GM and players to roll any standard die (d4–d30) * or its exploding variant (dXx) without needing an actor sheet. * Supports selecting how many dice to roll (1–9). */ /** Standard dice available in Lethal Fantasy */ const DICE_TYPES = ["d4", "d6", "d8", "d10", "d12", "d20", "d30"] /** * Inject the dice tray bar into the ChatLog HTML. * Called from `Hooks.on("renderChatLog", ...)`. * * @param {Application} _chatLog * @param {HTMLElement|jQuery} html */ export function injectDiceTray(_chatLog, html) { const el = (html instanceof HTMLElement) ? html : (html[0] ?? html) if (!el?.querySelector) return if (el.querySelector(".lf-dice-tray")) return const bar = document.createElement("div") bar.className = "lf-dice-tray" const diceButtons = DICE_TYPES.map(d => `` ).join("") const countOptions = Array.from({ length: 9 }, (_, i) => `` ).join("") bar.innerHTML = `
${diceButtons}
` bar.addEventListener("click", async ev => { const btn = ev.target.closest(".lf-dt-die-btn") if (!btn) return ev.stopPropagation() const dieType = btn.dataset.die const count = parseInt(bar.querySelector(".lf-dt-count").value) || 1 const explode = bar.querySelector(".lf-dt-explode").checked try { await rollFreeDie(dieType, count, explode) } catch (err) { console.error("Lethal Fantasy | Dice Tray error:", err) ui.notifications?.error("Dice Tray roll failed — see console") } }) 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) } } /** * Roll one or more dice of the given type and post the result to chat. * For exploding dice, follows the Lethal Fantasy rule: each exploded reroll * contributes (result − 1) to the total, same as all other system rolls. * * @param {string} dieType Die face, e.g. "d20" * @param {number} count Number of dice to roll (1–9) * @param {boolean} explode Whether to use the exploding variant (max triggers reroll at −1) * @returns {Promise} */ export async function rollFreeDie(dieType, count = 1, explode = false) { const sides = parseInt(dieType.replace("d", "")) || 20 const baseFormula = `1d${sides}` const label = explode ? `${count}${dieType.toUpperCase()}E` : `${count}${dieType.toUpperCase()}` const dieLabel = dieType.toUpperCase() const dieChips = [] let total = 0 for (let i = 0; i < count; i++) { const r0 = await new Roll(baseFormula).evaluate() if (game?.dice3d) await game.dice3d.showForRoll(r0, game.user, true) let diceResult = r0.dice[0].results[0].result dieChips.push({ label: dieLabel, value: diceResult, exploded: false }) total += diceResult if (explode) { while (diceResult === sides) { const rx = await new Roll(baseFormula).evaluate() if (game?.dice3d) await game.dice3d.showForRoll(rx, game.user, true) diceResult = rx.dice[0].results[0].result const contrib = diceResult - 1 dieChips.push({ label: `${dieLabel}-1`, value: contrib, exploded: true }) total += contrib } } } const resultHtml = dieChips.map(chip => { const isMax = !chip.exploded && chip.value === sides const isMin = chip.value === 1 const explodeIcon = chip.exploded ? `` : "" const classes = ["lf-frc-die-chip", isMax ? "lf-frc-max" : "", isMin ? "lf-frc-min" : ""].filter(Boolean).join(" ") return `
${chip.label} ${chip.value}${explodeIcon}
` }).join("") const totalLabel = game.i18n.localize("LETHALFANTASY.Label.total").toUpperCase() const content = `
${game.i18n.localize("LETHALFANTASY.DiceTray.ChatTitle")} ${label}
${resultHtml}
${totalLabel} ${total}
` const rollMode = game.settings.get("core", "rollMode") const msgData = { speaker: ChatMessage.getSpeaker(), content, sound: CONFIG.sounds.dice, } ChatMessage.applyRollMode(msgData, rollMode) await ChatMessage.create(msgData) }