Compare commits

...

3 Commits

Author SHA1 Message Date
uberwald 05c93f9475 Full reroll management
Release Creation / build (release) Successful in 43s
2026-06-28 08:39:55 +02:00
uberwald bb005ee9fc feat: full reroll includes D30 + shows dice breakdown in chat
- Remove forceNoD30 from rerollConfiguredRoll so mulligan
  rerolls the D30 along with the d20 and modifier
- Reset defenseD30Processed/attackD30Processed after mulligan
  so the new D30's effects (bonus dice, specials) are applied
- Render reroll dice breakdown (diceResults + D30 result) as
  inline HTML in the reaction chat message using existing
  CSS classes so players see what was rolled
2026-06-28 08:04:20 +02:00
uberwald fa5c4cc9ce debug: log favor/disfavor dice totals and results for RNG investigation
9/12 identical d20 results under disfavor suggests RNG issue.
Add structured logging of both roll totals, per-die results,
and baseFormula. Enable debug mode in system settings to see
output in browser console.
2026-06-22 22:33:00 +02:00
3 changed files with 52 additions and 7 deletions
+37 -4
View File
@@ -553,6 +553,17 @@ Hooks.on("preCreateChatMessage", (message) => {
} }
}) })
// Build dice breakdown HTML from a reroll result
function formatRerollBreakdown(reroll) {
const breakdown = (reroll.options?.diceResults || [])
.map(r => `<span class="dice-item"><span class="dice-type">${r.dice}</span><span class="dice-separator">→</span><span class="dice-value">${r.value}</span></span>`)
.join("")
const d30 = reroll.options?.D30message
? `<div class="d30-result"><span class="d30-value">D30 → ${reroll.options.D30result || "?"}</span> — ${reroll.options.D30message.description}</div>`
: ""
return { breakdown, d30 }
}
// Hook global pour gérer l'offre de Grit à l'attaquant après une défense // Hook global pour gérer l'offre de Grit à l'attaquant après une défense
Hooks.on("createChatMessage", async (message) => { Hooks.on("createChatMessage", async (message) => {
const rollType = message.rolls[0]?.options?.rollType const rollType = message.rolls[0]?.options?.rollType
@@ -580,15 +591,15 @@ Hooks.on("createChatMessage", async (message) => {
attackWeaponId, attackWeaponId,
attackRollType, attackRollType,
attackRollKey, attackRollKey,
attackD30message,
attackRerollContext, attackRerollContext,
attackNaturalRoll, attackNaturalRoll,
damageTier, damageTier,
defenderId, defenderId,
defenderTokenId defenderTokenId
} = attackData } = attackData
let { attackD30message } = attackData
let defenseRoll = message.rolls[0]?.options?.rollTotal || message.rolls[0]?.total || 0 let defenseRoll = message.rolls[0]?.options?.rollTotal || message.rolls[0]?.total || 0
const defenseD30message = message.rolls[0]?.options?.D30message || null let defenseD30message = message.rolls[0]?.options?.D30message || null
log("Processing defense:", { attackRoll, defenseRoll, attackerId, defenderId }) log("Processing defense:", { attackRoll, defenseRoll, attackerId, defenderId })
@@ -806,7 +817,18 @@ Hooks.on("createChatMessage", async (message) => {
canRerollDefense = false canRerollDefense = false
if (!reroll) continue if (!reroll) continue
defenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll defenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll
await createReactionMessage(defender, `<p><strong>${defenderName}</strong> uses Mulligan and re-rolls defense: <strong>${oldDefenseRoll}</strong> → <strong>${defenseRoll}</strong>. Both sides may now react to the new numbers.</p>`) // Build dice breakdown HTML from the reroll
const { breakdown: rerollBreakdown, d30: rerollD30 } = formatRerollBreakdown(reroll)
await createReactionMessage(defender,
`<p><strong>${defenderName}</strong> uses Mulligan and re-rolls defense: <strong>${oldDefenseRoll}</strong> → <strong>${defenseRoll}</strong>.</p>
<div class="dice-breakdown">${rerollBreakdown}</div>${rerollD30}
<p>Both sides may now react to the new numbers.</p>`
)
// Apply new D30 result on the restart
if (reroll.options?.D30message) {
defenseD30message = reroll.options.D30message
defenseD30Processed = false
}
// Restart the full comparison so both sides can react to the new roll // Restart the full comparison so both sides can react to the new roll
mulliganRestart = true mulliganRestart = true
break break
@@ -1003,7 +1025,18 @@ Hooks.on("createChatMessage", async (message) => {
canRerollAttack = false canRerollAttack = false
if (!reroll) continue if (!reroll) continue
attackRollFinal = reroll.options?.rollTotal || reroll.total || oldAttackRoll attackRollFinal = reroll.options?.rollTotal || reroll.total || oldAttackRoll
await createReactionMessage(attacker, `<p><strong>${attackerName}</strong> uses Mulligan and re-rolls attack: <strong>${oldAttackRoll}</strong> → <strong>${attackRollFinal}</strong>. Both sides may now react to the new numbers.</p>`) // Build dice breakdown HTML from the reroll
const { breakdown: rerollBreakdown, d30: rerollD30 } = formatRerollBreakdown(reroll)
await createReactionMessage(attacker,
`<p><strong>${attackerName}</strong> uses Mulligan and re-rolls attack: <strong>${oldAttackRoll}</strong> → <strong>${attackRollFinal}</strong>.</p>
<div class="dice-breakdown">${rerollBreakdown}</div>${rerollD30}
<p>Both sides may now react to the new numbers.</p>`
)
// Apply new D30 result on the restart
if (reroll.options?.D30message) {
attackD30message = reroll.options.D30message
attackD30Processed = false
}
// Restart the full comparison so both sides can react to the new roll // Restart the full comparison so both sides can react to the new roll
mulliganRestart = true mulliganRestart = true
break break
+15 -2
View File
@@ -555,7 +555,13 @@ export default class LethalFantasyRoll extends Roll {
if (rollContext.favor === "favor") { if (rollContext.favor === "favor") {
rollFavor = new this(baseFormula, options.data, rollData) rollFavor = new this(baseFormula, options.data, rollData)
await rollFavor.evaluate() await rollFavor.evaluate()
log("Rolling with favor", rollFavor) log("Favor dice", {
rollBaseTotal: rollBase.total,
rollFavorTotal: rollFavor.total,
rollBaseResults: rollBase.dice.map(d => d.results.map(r => r.result)),
rollFavorResults: rollFavor.dice.map(d => d.results.map(r => r.result)),
baseFormula
})
if (game?.dice3d) { if (game?.dice3d) {
game.dice3d.showForRoll(rollFavor, game.user, true) game.dice3d.showForRoll(rollFavor, game.user, true)
} }
@@ -571,6 +577,13 @@ export default class LethalFantasyRoll extends Roll {
if (rollContext.favor === "disfavor") { if (rollContext.favor === "disfavor") {
rollFavor = new this(baseFormula, options.data, rollData) rollFavor = new this(baseFormula, options.data, rollData)
await rollFavor.evaluate() await rollFavor.evaluate()
log("Disfavor dice", {
rollBaseTotal: rollBase.total,
rollFavorTotal: rollFavor.total,
rollBaseResults: rollBase.dice.map(d => d.results.map(r => r.result)),
rollFavorResults: rollFavor.dice.map(d => d.results.map(r => r.result)),
baseFormula
})
if (game?.dice3d) { if (game?.dice3d) {
game.dice3d.showForRoll(rollFavor, game.user, true) game.dice3d.showForRoll(rollFavor, game.user, true)
} }
@@ -1492,7 +1505,7 @@ export default class LethalFantasyRoll extends Roll {
if (this.type === "weapon-attack" && this.rollTarget?.weapon) { if (this.type === "weapon-attack" && this.rollTarget?.weapon) {
const weapon = this.rollTarget.weapon const weapon = this.rollTarget.weapon
weaponDamageOptions = { weaponDamageOptions = {
weaponId: weapon.id, weaponId: weapon._id || weapon.id,
weaponName: weapon.name, weaponName: weapon.name,
damageM: weapon.system?.damage?.damageM damageM: weapon.system?.damage?.damageM
} }
-1
View File
@@ -1128,7 +1128,6 @@ export default class LethalFantasyUtils {
return await RollClass.prompt({ return await RollClass.prompt({
...foundry.utils.duplicate(rerollContext), ...foundry.utils.duplicate(rerollContext),
rollContext: foundry.utils.duplicate(rerollContext.rollContext || {}), rollContext: foundry.utils.duplicate(rerollContext.rollContext || {}),
forceNoD30: true,
hasTarget: false, hasTarget: false,
target: false target: false
}) })