3df46b5848
- Extract all inline HTML from JS into 21 Handlebars templates (chat/, dialogs/, ui/) - Split utils.mjs (1507) into barrel + helpers.mjs, combat.mjs, d30.mjs - Split roll.mjs (1632) into barrel + roll-base.mjs, roll-prompt.mjs, roll-combat.mjs, roll-damage.mjs - Split lethal-fantasy.mjs (1426) into bootstrap + chat-reaction.mjs - Fix: missing async on injectDiceTray (free-roll.mjs:29 SyntaxError) - Fix: weapon._id fallback for deserialized chat-message weapon objects - Fix: missing await on rollModifier.evaluate() calls in roll-combat.mjs - Fix: choices→choicesList ReferenceError in utils.mjs - Fix: add 12 missing i18n keys (chooseWeapon, chooseSave, attackRoll, etc.) - Fix: restore sideLabel in bonus-die-select.hbs - Clean: remove dead messageContent param, console.log→log() - Style: barrel files preserve existing import paths
129 lines
4.6 KiB
JavaScript
129 lines
4.6 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 async 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 => ({ value: d, label: d.toUpperCase() }))
|
||
const countOptions = Array.from({ length: 9 }, (_, i) => i + 1)
|
||
bar.innerHTML = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/ui/dice-tray.hbs", {
|
||
countTitle: game.i18n.localize("LETHALFANTASY.DiceTray.CountTitle"),
|
||
explodeTitle: game.i18n.localize("LETHALFANTASY.DiceTray.ExplodeTitle"),
|
||
countOptions,
|
||
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<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 dieChipsWithClasses = dieChips.map(chip => ({
|
||
...chip,
|
||
classes: ["lf-frc-die-chip", !chip.exploded && chip.value === sides ? "lf-frc-max" : "", chip.value === 1 ? "lf-frc-min" : ""].filter(Boolean).join(" ")
|
||
}))
|
||
|
||
const totalLabel = game.i18n.localize("LETHALFANTASY.Label.total").toUpperCase()
|
||
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/free-roll-card.hbs", {
|
||
titleText: game.i18n.localize("LETHALFANTASY.DiceTray.ChatTitle"),
|
||
badge: label,
|
||
dieChips: dieChipsWithClasses,
|
||
totalLabel,
|
||
total
|
||
})
|
||
|
||
const rollMode = game.settings.get("core", "rollMode")
|
||
// Normalize old-style rollMode keys (v12/v13) to new-style (v14), fallback to "public"
|
||
const modeMap = { publicroll: "public", gmroll: "gm", blindroll: "blind", selfroll: "self" }
|
||
const mode = modeMap[rollMode] ?? rollMode ?? "public"
|
||
const msgData = {
|
||
speaker: ChatMessage.getSpeaker(),
|
||
content,
|
||
sound: CONFIG.sounds.dice,
|
||
mode,
|
||
}
|
||
await ChatMessage.create(msgData)
|
||
}
|