140 lines
4.7 KiB
JavaScript
140 lines
4.7 KiB
JavaScript
/**
|
||
* 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 =>
|
||
`<button type="button" class="lf-dt-die-btn" data-die="${d}" title="${d.toUpperCase()}">${d.toUpperCase()}</button>`
|
||
).join("")
|
||
|
||
const countOptions = Array.from({ length: 9 }, (_, i) =>
|
||
`<option value="${i + 1}">${i + 1}</option>`
|
||
).join("")
|
||
|
||
bar.innerHTML = `
|
||
<div class="lf-dt-row">
|
||
<span class="lf-dt-label"><i class="fa-solid fa-dice"></i></span>
|
||
<select class="lf-dt-count" title="${game.i18n.localize("LETHALFANTASY.DiceTray.CountTitle")}">
|
||
${countOptions}
|
||
</select>
|
||
<div class="lf-dt-dice">${diceButtons}</div>
|
||
<label class="lf-dt-explode-label" title="${game.i18n.localize("LETHALFANTASY.DiceTray.ExplodeTitle")}">
|
||
<input type="checkbox" class="lf-dt-explode" />
|
||
<i class="fa-solid fa-explosion"></i>
|
||
</label>
|
||
</div>
|
||
`
|
||
|
||
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.
|
||
*
|
||
* @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 modifier (x = max-value explode)
|
||
* @returns {Promise<void>}
|
||
*/
|
||
export async function rollFreeDie(dieType, count = 1, explode = false) {
|
||
const sides = parseInt(dieType.replace("d", "")) || 20
|
||
const formula = explode ? `${count}d${sides}x` : `${count}d${sides}`
|
||
const label = explode
|
||
? `${count}${dieType.toUpperCase()}E`
|
||
: `${count}${dieType.toUpperCase()}`
|
||
|
||
const roll = new Roll(formula)
|
||
await roll.evaluate()
|
||
|
||
const results = roll.terms
|
||
.filter(t => t.results)
|
||
.flatMap(t => t.results)
|
||
|
||
const total = roll.total
|
||
|
||
const dieLabel = dieType.toUpperCase()
|
||
const resultHtml = results.map(r => {
|
||
const val = r.result
|
||
const isMax = val === sides
|
||
const isMin = val === 1
|
||
const explodeIcon = r.exploded ? `<i class="fa-solid fa-burst lf-dt-explode-icon"></i>` : ""
|
||
const classes = ["lf-frc-die-chip", isMax ? "lf-frc-max" : "", isMin ? "lf-frc-min" : ""].filter(Boolean).join(" ")
|
||
return `<div class="${classes}">
|
||
<span class="lf-frc-die-type">${dieLabel}</span>
|
||
<span class="lf-frc-die-sep">→</span>
|
||
<span class="lf-frc-die-val">${val}${explodeIcon}</span>
|
||
</div>`
|
||
}).join("")
|
||
|
||
const totalLabel = game.i18n.localize("LETHALFANTASY.Label.total").toUpperCase()
|
||
const content = `
|
||
<div class="lf-free-roll-card">
|
||
<div class="lf-frc-header">
|
||
<i class="fa-solid fa-dice"></i>
|
||
<span class="lf-frc-title-text">${game.i18n.localize("LETHALFANTASY.DiceTray.ChatTitle")}</span>
|
||
<span class="lf-frc-badge">${label}</span>
|
||
</div>
|
||
<div class="lf-frc-dice">${resultHtml}</div>
|
||
<div class="lf-frc-total-bar">
|
||
<span class="lf-frc-total-label">${totalLabel}</span>
|
||
<span class="lf-frc-total-value">${total}</span>
|
||
</div>
|
||
</div>
|
||
`
|
||
|
||
const rollMode = game.settings.get("core", "rollMode")
|
||
const msgData = {
|
||
speaker: ChatMessage.getSpeaker(),
|
||
content,
|
||
rolls: [roll],
|
||
sound: CONFIG.sounds.dice,
|
||
}
|
||
ChatMessage.applyRollMode(msgData, rollMode)
|
||
await ChatMessage.create(msgData)
|
||
}
|