From 0228f351c7543fe050d18b8268bea48683c92068 Mon Sep 17 00:00:00 2001 From: LeRatierBretonnier Date: Sat, 8 Aug 2026 20:39:38 +0200 Subject: [PATCH] fix(combat): prevent double dice rolls in handleAttackBoosted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a GM-owned attacker attacks a player character, both the createChatMessage hook and handleAttackBoosted ran on the player's client — rolling D30 bonus dice, grit, luck, shield, and mulligan dice twice. Now handleAttackBoosted early-returns when the attacker is not cross-client (GM-owned), leaving the hook as the sole handler. --- module/utils/combat.mjs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/module/utils/combat.mjs b/module/utils/combat.mjs index 19e4125..48104b2 100644 --- a/module/utils/combat.mjs +++ b/module/utils/combat.mjs @@ -105,8 +105,24 @@ export async function handleAttackBoosted(msg) { } = msg const defender = game.actors.get(defenderId) + const attacker = game.actors.get(attackerId) if (!defender) return + // When the attacker is GM-owned (not a PC on another client), the createChatMessage + // hook on the defender's client already handles everything: D30 bonus dice, defense + // reaction dialog (grit/luck/shield/mulligan), and the comparison message. Running + // handleAttackBoosted too would double-roll all dice and show duplicate dialogs. + // Only proceed when the hook is suppressed (attacker is a PC on another client). + const attackerHasNonGMOwner = attacker && game.users.some(u => u.active && !u.isGM && attacker.testUserPermission(u, "OWNER")) + const _isPrimaryController = actor => { + if (!actor) return false + const activePlayerOwners = game.users.filter(u => u.active && !u.isGM && actor.testUserPermission(u, "OWNER")) + if (activePlayerOwners.length > 0) return activePlayerOwners[0].id === game.user.id + return game.user.isGM + } + const attackerIsCrossClient = attackerHasNonGMOwner && !_isPrimaryController(attacker) + if (!attackerIsCrossClient) return + let updatedDefenseRoll = defenseRoll let shieldBlocked = false let shieldReaction = null