Files
fvtt-lethal-fantasy/module/applications/free-roll.mjs
LeRatierBretonnier b8174d5e22
All checks were successful
Release Creation / build (release) Successful in 55s
Fix E dice in dice Tray
2026-04-17 23:21:49 +02:00

153 lines
5.4 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 Tray — injected into the Foundry chat sidebar.
*
* Provides a compact bar for GM and players to roll any standard die (d4d30)
* or its exploding variant (dXx) without needing an actor sheet.
* Supports selecting how many dice to roll (19).
*/
/** 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.
* 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 (19)
* @param {boolean} explode Whether to use the exploding variant (max triggers reroll at 1)
* @returns {Promise<void>}
*/
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 ? `<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">${chip.label}</span>
<span class="lf-frc-die-sep">→</span>
<span class="lf-frc-die-val">${chip.value}${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,
sound: CONFIG.sounds.dice,
}
ChatMessage.applyRollMode(msgData, rollMode)
await ChatMessage.create(msgData)
}