fix: D30 special effects breaking combat comparison, map auto-pan, dead targets
Release Creation / build (release) Successful in 1m35s

- Fix handleAttackBoosted guard: replaced attackerIsCrossClient check
  with defenderIsMine so the socket handler runs whenever the current
  user controls the defender. Previously, when a GM-controlled monster
  attacked with a D30 special result (damage multiplier, bleed, DR
  multiplier, mulligan), the createChatMessage hook was suppressed
  (d30PendingFromGM) but handleAttackBoosted returned early because
  the attacker wasn't a cross-client PC, so nobody created the
  comparison message.
- Remove map auto-pan on target/damage button hover (mouseenter/mouseleave
  handlers on .request-defense-btn and .apply-wounds-btn).
- Filter defeated combatants and dead monster tokens from the target
  selection list. Characters at 0 HP are still selectable (death saves).
- Restore 3D dice animation for damage rolls (lethal-fantasy.mjs,
  character-sheet.mjs).
- Allow negative bonuses on weapon items (damaged weapons).
- Allow negative HP for characters (death saving throws).
- Fix D30Roll.convertToInternalType guard to warn on missing weapon
  regardless of isRanged flag.
- Fix ranged weapon loading comparison (<= to <).
- Add isRangedAttack to rollTarget for consistent ranged detection.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-08-17 20:12:12 +02:00
parent 0228f351c7
commit f47f2906a4
3 changed files with 14 additions and 35 deletions
+10 -8
View File
@@ -108,20 +108,22 @@ export async function handleAttackBoosted(msg) {
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"))
// The createChatMessage hook is suppressed on this client in two cases:
// 1. Attacker is a PC on another client — the hook's defense reaction loop
// and shouldCreateMessage are both skipped.
// 2. Attacker is GM-controlled with D30 — the hook skips everything and
// waits for this socket handler.
// In both cases, this handler must run to show the defense dialog and create
// the comparison message. The common condition: the current user is the
// defender's primary controller.
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
const defenderIsMine = _isPrimaryController(defender)
if (!defenderIsMine) return
let updatedDefenseRoll = defenseRoll
let shieldBlocked = false