Compare commits

...

6 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
uberwald 3b0d4e032e fix: cache results.length before explosion loop to prevent double-count
Release Creation / build (release) Successful in 50s
Pushing explosion dice to DieTerm.results made the for loop
condition (j < results.length) grow mid-iteration, re-processing
the explosion result as a normal die. This produced spurious
entries like `1D6 → 4` alongside the correct `1D6-1 → 3`.

Fixes prompt() (for loop) and rollSpellDamageToMessage()
(for...of) by caching result count / snapshotting the array
before iterating.
2026-06-16 19:34:13 +02:00
uberwald 539841c4ff fix: push explosion dice to DieTerm results so DSN displays them
Explosion rolls were evaluated as separate Roll instances but never
added to the original DieTerm's results array. Dice So Nice reads
DieTerm.results to render 3D dice, so explosions were invisible.

Now each explosion result is pushed into the DieTerm's results array
({result, active:true}), letting DSN render explosion dice in the
correct chronological order alongside the main die.

Applies to prompt(), promptRangedDefense(), promptRangedAttack(),
and rollSpellDamageToMessage().
2026-06-16 19:29:07 +02:00
uberwald ffba37b59e Fix D30 management, again 2026-06-14 23:00:39 +02:00
4 changed files with 64 additions and 11 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ jobs:
token: ${{ secrets.FOUNDRY_PUBLISH_KEY }}
id: "fvtt-lethal-fantasy"
version: ${{github.event.release.tag_name}}
manifest: "https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{github.event.release.tag_name}}/system.json"
manifest: "https://www.uberwald.me/gitea/uberwald/fvtt-lethal-fantasy/releases/download/latest/system.json"
notes: "https://www.uberwald.me/gitea/public/fvtt-lethal-fantasy/raw/branch/main/changelog.md"
compatibility-minimum: "14"
compatibility-verified: "14"
+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
Hooks.on("createChatMessage", async (message) => {
const rollType = message.rolls[0]?.options?.rollType
@@ -580,15 +591,15 @@ Hooks.on("createChatMessage", async (message) => {
attackWeaponId,
attackRollType,
attackRollKey,
attackD30message,
attackRerollContext,
attackNaturalRoll,
damageTier,
defenderId,
defenderTokenId
} = attackData
let { attackD30message } = attackData
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 })
@@ -806,7 +817,18 @@ Hooks.on("createChatMessage", async (message) => {
canRerollDefense = false
if (!reroll) continue
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
mulliganRestart = true
break
@@ -1003,7 +1025,18 @@ Hooks.on("createChatMessage", async (message) => {
canRerollAttack = false
if (!reroll) continue
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
mulliganRestart = true
break
+26 -5
View File
@@ -555,7 +555,13 @@ export default class LethalFantasyRoll extends Roll {
if (rollContext.favor === "favor") {
rollFavor = new this(baseFormula, options.data, rollData)
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) {
game.dice3d.showForRoll(rollFavor, game.user, true)
}
@@ -571,6 +577,13 @@ export default class LethalFantasyRoll extends Roll {
if (rollContext.favor === "disfavor") {
rollFavor = new this(baseFormula, options.data, rollData)
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) {
game.dice3d.showForRoll(rollFavor, game.user, true)
}
@@ -616,8 +629,10 @@ export default class LethalFantasyRoll extends Roll {
let singleDice = `1D${maxValue}`
for (let i = 0; i < rollBase.dice.length; i++) {
for (let j = 0; j < rollBase.dice[i].results.length; j++) {
let diceResult = rollBase.dice[i].results[j].result
const dieResults = rollBase.dice[i].results
const resultCount = dieResults.length
for (let j = 0; j < resultCount; j++) {
let diceResult = dieResults[j].result
diceResults.push({ dice: `${singleDice.toUpperCase()}`, value: diceResult })
diceSum += diceResult
if (hasMaxValue) {
@@ -626,6 +641,8 @@ export default class LethalFantasyRoll extends Roll {
diceResult = r.dice[0].results[0].result
diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 })
diceSum += (diceResult - 1)
// Add to DieTerm results so DSN/Foundry display shows explosion dice
dieResults.push({ result: diceResult, active: true })
}
}
}
@@ -1214,6 +1231,7 @@ export default class LethalFantasyRoll extends Roll {
diceResult = r.dice[0].results[0].result
diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 })
diceSum += (diceResult - 1)
rollBase.dice[0].results.push({ result: diceResult, active: true })
}
if (fullModifier !== 0) {
diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total })
@@ -1376,6 +1394,7 @@ export default class LethalFantasyRoll extends Roll {
diceResult = r.dice[0].results[0].result
diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 })
diceSum += (diceResult - 1)
rollBase.dice[0].results.push({ result: diceResult, active: true })
}
if (fullModifier !== 0) {
@@ -1486,7 +1505,7 @@ export default class LethalFantasyRoll extends Roll {
if (this.type === "weapon-attack" && this.rollTarget?.weapon) {
const weapon = this.rollTarget.weapon
weaponDamageOptions = {
weaponId: weapon.id,
weaponId: weapon._id || weapon.id,
weaponName: weapon.name,
damageM: weapon.system?.damage?.damageM
}
@@ -1588,7 +1607,8 @@ export default class LethalFantasyRoll extends Roll {
let diceSum = 0
for (const term of roll.dice) {
const singleDice = `1D${term.faces}`
for (const r of term.results) {
const termResults = Array.from(term.results)
for (const r of termResults) {
let diceResult = r.result
diceResults.push({ dice: singleDice.toUpperCase(), value: diceResult })
diceSum += diceResult
@@ -1599,6 +1619,7 @@ export default class LethalFantasyRoll extends Roll {
diceResult = xr.dice?.[0]?.results?.[0]?.result ?? (term.faces - 1)
diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 })
diceSum += (diceResult - 1)
term.results.push({ result: diceResult, active: true })
}
}
}
-1
View File
@@ -1128,7 +1128,6 @@ export default class LethalFantasyUtils {
return await RollClass.prompt({
...foundry.utils.duplicate(rerollContext),
rollContext: foundry.utils.duplicate(rerollContext.rollContext || {}),
forceNoD30: true,
hasTarget: false,
target: false
})