fix: attack/defense cross-client reaction flow
- C1: Stop D30 auto-roll on non-primary clients (caused divergence) - C2: defenderOwner fallback to GM for monster defenders - C3: Fix tie outcome in handleAttackBoosted (>= not >) - C5: Convert handleAttackBoosted to while-loop (multi-reaction) - C4/C6: shouldCreateMessage cross-client guard - M2: Coordinate main flow defender dialog vs socket handler - M3: Fresh grit/luck reads each socket handler iteration - M4: Include defenseD30message in socket payload + re-process - M5: Communicate attackerHandledBonus in socket payload - i18n: Add missing COMBAT.* keys, fix weapon.hbs label localize - d30_results_tables: Fix string typo
This commit is contained in:
+30
-12
@@ -633,6 +633,10 @@ Hooks.on("createChatMessage", async (message) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Detect cross-client scenario: attacker has an active non-GM owner on another client
|
||||
const attackerHasNonGMOwner = attacker && game.users.some(u => u.active && !u.isGM && attacker.testUserPermission(u, "OWNER"))
|
||||
const attackerIsCrossClient = attackerHasNonGMOwner && !isPrimaryController(attacker)
|
||||
|
||||
// Reaction phase — both sides may use grit/luck/shield/mulligan before the outcome is resolved.
|
||||
// After a mulligan reroll (either side), the comparison restarts so both sides can react to the new numbers.
|
||||
let defenderHandledBonus = false
|
||||
@@ -663,7 +667,7 @@ Hooks.on("createChatMessage", async (message) => {
|
||||
attackerHandledBonus = false
|
||||
|
||||
// ── D30 bonus dice (defense) — resolved before grit/luck/shield ───────
|
||||
if (defenseD30message && !defenseD30Processed && isPrimaryController(defender)) {
|
||||
if (defenseD30message && !defenseD30Processed && isPrimaryController(defender) && !attackerIsCrossClient) {
|
||||
const d30Result = await LethalFantasyUtils.processD30BonusDice(defenseD30message, "defense", null, defender, true)
|
||||
if (d30Result.modifier) {
|
||||
defenseRoll += d30Result.modifier
|
||||
@@ -694,7 +698,9 @@ Hooks.on("createChatMessage", async (message) => {
|
||||
}
|
||||
|
||||
// ── Defense reaction loop ──────────────────────────────────────────────
|
||||
if (defender && defenseRoll < attackRollFinal && isPrimaryController(defender) && !isSpellOrMiracle) {
|
||||
// Skip when attacker is cross-client — the socket handler (handleAttackBoosted)
|
||||
// will show the defense dialog and create the comparison message.
|
||||
if (defender && defenseRoll < attackRollFinal && isPrimaryController(defender) && !isSpellOrMiracle && !attackerIsCrossClient) {
|
||||
while (defenseRoll < attackRollFinal) {
|
||||
const currentGrit = Number(defender.system?.grit?.current) || 0
|
||||
const currentLuck = Number(defender.system?.luck?.current) || 0
|
||||
@@ -1003,41 +1009,53 @@ Hooks.on("createChatMessage", async (message) => {
|
||||
}
|
||||
}
|
||||
|
||||
// If attacker boosted past defense, let the defender react
|
||||
if (attackerHandledBonus && defenseRoll < attackRollFinal && defender) {
|
||||
// Cross-client coordination: delegate the remaining reaction + message
|
||||
// to the defender's controller via socket. Only the attacker's owning
|
||||
// client sends — preventing duplicate emissions from other clients.
|
||||
if (defender && isPrimaryController(attacker)) {
|
||||
const defenderOwner = game.users.find(u => u.active && !u.isGM && defender.testUserPermission(u, "OWNER"))
|
||||
|| game.users.find(u => u.active && u.isGM)
|
||||
if (defenderOwner && defenderOwner.id !== game.user.id) {
|
||||
// Cross-client: send socket to defender's client
|
||||
const sData = LethalFantasyUtils.getShieldReactionData(defender)
|
||||
game.socket.emit(`system.${SYSTEM.id}`, {
|
||||
type: "attackBoosted",
|
||||
userId: defenderOwner.id,
|
||||
attackerName, attackerId, defenderName, defenderId, defenderTokenId,
|
||||
attackRollFinal, defenseRoll, attackWeaponId, attackRollType, attackRollKey,
|
||||
attackerHandledBonus, attackRollFinal, defenseRoll, attackWeaponId, attackRollType, attackRollKey,
|
||||
shieldDamageReduction: shieldBlocked ? shieldReaction?.damageReduction ?? 0 : 0,
|
||||
d30Bleed: d30Bleed ? "true" : "",
|
||||
d30DamageMultiplier, d30DrMultiplier,
|
||||
damageTier: damageTier || "standard",
|
||||
attackD30message,
|
||||
defenseD30message,
|
||||
hasShield: !!sData,
|
||||
shieldLabel: sData?.label || "",
|
||||
shieldFormula: sData?.formula || "",
|
||||
shieldDr: sData?.damageReduction || 0,
|
||||
canAdHocShield: !sData,
|
||||
})
|
||||
return // Comparison message created by defender's client
|
||||
return
|
||||
}
|
||||
// Same client: restart for defender loop if attacker boosted past defense
|
||||
if (defenseRoll < attackRollFinal && attackerHandledBonus) {
|
||||
mulliganRestart = true
|
||||
}
|
||||
// Single-client (GM controls both): restart so defender loop can run
|
||||
mulliganRestart = true
|
||||
}
|
||||
} while (mulliganRestart)
|
||||
|
||||
const shieldDamageReduction = shieldBlocked ? shieldReaction?.damageReduction ?? 0 : 0
|
||||
const outcome = shieldBlocked ? "shielded-hit" : (attackRollFinal > defenseRoll ? "hit" : "miss")
|
||||
|
||||
// Créer le message de comparaison - uniquement par le client qui a géré le dernier bonus
|
||||
// Priorité: attaquant si il a géré le bonus, sinon défenseur si il a géré le bonus, sinon défenseur
|
||||
const shouldCreateMessage = attackerHandledBonus || (!attackerHandledBonus && defenderHandledBonus) || (!attackerHandledBonus && !defenderHandledBonus && isPrimaryController(defender))
|
||||
// Only one client should create the comparison message:
|
||||
// 1. Attacker boosted → attacker's client creates (or socket handler for cross-client)
|
||||
// 2. Defender boosted → defender's client creates
|
||||
// 3. Nobody boosted → attacker's client (preferred) or defender's client (fallback if same-client)
|
||||
const shouldCreateMessage = attackerHandledBonus
|
||||
|| (!attackerHandledBonus && defenderHandledBonus)
|
||||
|| (!attackerHandledBonus && !defenderHandledBonus && (
|
||||
(isPrimaryController(defender) && !attackerIsCrossClient)
|
||||
|| isPrimaryController(attacker)
|
||||
))
|
||||
|
||||
if (shouldCreateMessage) {
|
||||
console.log("Creating comparison message", { attackerHandledBonus, defenderHandledBonus, isDefenderOwner: defender.isOwner })
|
||||
|
||||
Reference in New Issue
Block a user