fix: pre-process D30 attack bonus at source for cross-client agreement
Release Creation / build (release) Successful in 48s
Release Creation / build (release) Successful in 48s
D30 bonus dice now rolled at defense request time, not in defense handler. Sends boosted attackRoll + d30AttackEffects in socket/nextDefenseData so both clients agree on the value. Adds stale flag for mulligan rerolls to bypass stale pre-computed effects and process the new D30 fresh.
This commit is contained in:
@@ -66,7 +66,7 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => {
|
||||
btn.addEventListener("mouseleave", () => canvas.tokens.releaseAll())
|
||||
|
||||
// Gestionnaire pour les boutons de demande de défense
|
||||
btn.addEventListener("click", event => {
|
||||
btn.addEventListener("click", async event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
@@ -140,13 +140,24 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => {
|
||||
|| (attackerWeapon?.system?.weaponType === "ranged")
|
||||
|| (rollTargetOptions?.isRangedAttack === true)
|
||||
|
||||
// Process D30 attack bonus at source so both clients get the same boosted value.
|
||||
// The d30BonusRoll chat message is created here (by _rollD30BonusDie inside processD30BonusDice).
|
||||
let d30AttackEffects = null
|
||||
let boostedAttackRoll = attackRoll
|
||||
if (attackD30message && attacker) {
|
||||
d30AttackEffects = await LethalFantasyUtils.processD30BonusDice(attackD30message, "attack", attackNaturalRoll, attacker, true)
|
||||
if (d30AttackEffects.modifier) {
|
||||
boostedAttackRoll = attackRoll + d30AttackEffects.modifier
|
||||
}
|
||||
}
|
||||
|
||||
const defenseMsg = {
|
||||
type: "requestDefense",
|
||||
attackerName,
|
||||
attackerId,
|
||||
defenderName,
|
||||
weaponName,
|
||||
attackRoll,
|
||||
attackRoll: boostedAttackRoll,
|
||||
attackWeaponId,
|
||||
attackRollType,
|
||||
attackRollKey,
|
||||
@@ -157,7 +168,8 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => {
|
||||
damageTier,
|
||||
combatantId,
|
||||
tokenId,
|
||||
isRanged: isRangedAttack
|
||||
isRanged: isRangedAttack,
|
||||
d30AttackEffects
|
||||
}
|
||||
|
||||
// Envoyer le message socket à l'utilisateur contrôlant le combatant
|
||||
@@ -448,6 +460,10 @@ Hooks.on("createChatMessage", async (message) => {
|
||||
let d30Bleed = false
|
||||
let d30DamageMultiplier = 1
|
||||
let d30DrMultiplier = 1
|
||||
// Tracks whether the pre-computed D30 effects are stale (invalidated by a mulligan reroll
|
||||
// that produced a new D30 message). When a reroll updates attackD30message, the original
|
||||
// d30AttackEffects from the defense request no longer apply — the new D30 must be processed.
|
||||
let d30AttackPrecomputedStale = false
|
||||
|
||||
do {
|
||||
mulliganRestart = false
|
||||
@@ -605,26 +621,42 @@ Hooks.on("createChatMessage", async (message) => {
|
||||
// ── D30 bonus dice (attack) — resolved before grit/luck ────────────────
|
||||
if (attackD30message && !attackD30Processed) {
|
||||
const preD30AttackRoll = attackRollFinal
|
||||
const canDialog = isPrimaryController(attacker)
|
||||
const d30Result = await LethalFantasyUtils.processD30BonusDice(attackD30message, "attack", attackNaturalRoll, attacker, canDialog)
|
||||
const canShow = isPrimaryController(attacker)
|
||||
|
||||
// Use pre-computed D30 effects (processed at attack time in the defense
|
||||
// request handler) to avoid re-rolling dice on the defense handler.
|
||||
// This ensures both clients agree on the boosted attack roll.
|
||||
// When d30AttackPrecomputedStale is true (invalidated by a mulligan reroll),
|
||||
// skip the pre-computed effects and process the new D30 message fresh.
|
||||
const usePrecomputed = !!attackData.d30AttackEffects && !d30AttackPrecomputedStale
|
||||
const d30Result = usePrecomputed
|
||||
? attackData.d30AttackEffects
|
||||
: await LethalFantasyUtils.processD30BonusDice(attackD30message, "attack", attackNaturalRoll, attacker, canShow)
|
||||
d30AttackPrecomputedStale = false
|
||||
|
||||
if (d30Result.modifier) {
|
||||
attackRollFinal += d30Result.modifier
|
||||
if (d30Result.modifier > 0 && canDialog) {
|
||||
if (!usePrecomputed) {
|
||||
// Legacy or post-reroll path: bonus was just rolled, apply now
|
||||
attackRollFinal += d30Result.modifier
|
||||
}
|
||||
// d30BonusRoll message was created at attack time (pre-processed)
|
||||
// or inside _rollD30BonusDie (legacy). Either way, show the application.
|
||||
if (d30Result.modifier > 0 && canShow) {
|
||||
await createReactionMessage(attacker, {type:"d30Bonus", actorName:attackerName, value:d30Result.modifier, side:"attack"})
|
||||
}
|
||||
}
|
||||
if (d30Result.specialEffect === "flag" && canDialog) {
|
||||
if (d30Result.specialEffect === "flag" && canShow) {
|
||||
await createReactionMessage(attacker, {type:"d30Flag", actorName:attackerName, specialName:d30Result.specialName||"Special Effect"})
|
||||
}
|
||||
if (d30Result.specialEffect === "bleed") {
|
||||
d30Bleed = true
|
||||
if (canDialog) {
|
||||
if (canShow) {
|
||||
await createReactionMessage(attacker, {type:"d30Bleed", actorName:attackerName})
|
||||
}
|
||||
}
|
||||
if (d30Result.specialEffect === "damageMultiplier") {
|
||||
d30DamageMultiplier = d30Result.multiplier
|
||||
if (canDialog) {
|
||||
if (canShow) {
|
||||
await createReactionMessage(attacker, {type:"d30DamageMultiplier", actorName:attackerName, value:d30Result.multiplier})
|
||||
}
|
||||
}
|
||||
@@ -735,6 +767,8 @@ Hooks.on("createChatMessage", async (message) => {
|
||||
if (reroll.options?.D30message) {
|
||||
attackD30message = reroll.options.D30message
|
||||
attackD30Processed = false
|
||||
// Invalidate pre-computed effects — the new D30 message must be processed fresh
|
||||
d30AttackPrecomputedStale = true
|
||||
}
|
||||
// Restart the full comparison so both sides can react to the new roll
|
||||
mulliganRestart = true
|
||||
|
||||
@@ -302,6 +302,7 @@ export async function showDefenseRequest(msg) {
|
||||
const attackD30result = msg.attackD30result
|
||||
const attackD30message = msg.attackD30message
|
||||
const attackRerollContext = msg.attackRerollContext
|
||||
const d30AttackEffects = msg.d30AttackEffects
|
||||
const combatantId = msg.combatantId
|
||||
const tokenId = msg.tokenId
|
||||
|
||||
@@ -351,6 +352,7 @@ export async function showDefenseRequest(msg) {
|
||||
damageTier: msg.damageTier,
|
||||
defenderId: defender.id, defenderTokenId,
|
||||
...(msg.attackNaturalRoll !== undefined && { attackNaturalRoll: msg.attackNaturalRoll }),
|
||||
...(d30AttackEffects !== undefined && { d30AttackEffects }),
|
||||
...(opts.isRanged !== undefined && { isRanged: opts.isRanged })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user