fix: D30 special effects breaking combat comparison, map auto-pan, dead targets
Release Creation / build (release) Successful in 1m35s
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:
@@ -163,7 +163,7 @@ export default class LethalFantasyRoll extends Roll {
|
||||
// D'abord, ajouter les combattants du combat actif
|
||||
if (game?.combat?.combatants) {
|
||||
for (let c of game.combat.combatants) {
|
||||
if (c.actorId !== this.actorId) {
|
||||
if (c.actorId !== this.actorId && !c.isDefeated) {
|
||||
combatants.push({ id: c.id, name: c.name, tokenId: c.token.id })
|
||||
}
|
||||
}
|
||||
@@ -174,6 +174,9 @@ export default class LethalFantasyRoll extends Roll {
|
||||
const existingTokenIds = new Set(combatants.map(c => c.tokenId))
|
||||
for (let token of canvas.scene.tokens) {
|
||||
if (token.actorId !== this.actorId && !existingTokenIds.has(token.id)) {
|
||||
// Skip dead tokens: monsters at 0 HP are dead; characters use the defeated flag
|
||||
const tokenActor = game.actors.get(token.actorId)
|
||||
if (tokenActor?.type === "monster" && (Number(tokenActor?.system?.hp?.value) || 0) <= 0) continue
|
||||
combatants.push({
|
||||
id: token.id,
|
||||
name: token.name,
|
||||
|
||||
@@ -33,38 +33,12 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => {
|
||||
}
|
||||
|
||||
for (const btn of html.querySelectorAll(".apply-wounds-btn")) {
|
||||
btn.addEventListener("mouseenter", () => {
|
||||
const combatantId = btn.dataset.combatantId
|
||||
if (combatantId && game.combat) {
|
||||
const combatant = game.combat.combatants.get(combatantId)
|
||||
if (combatant?.token) {
|
||||
const token = canvas.tokens.get(combatant.token.id)
|
||||
if (token) {
|
||||
token.control({ releaseOthers: true })
|
||||
canvas.animatePan(token.center)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
btn.addEventListener("mouseleave", () => canvas.tokens.releaseAll())
|
||||
btn.addEventListener("click", event => LethalFantasyUtils.applyDamage(message, event))
|
||||
}
|
||||
}
|
||||
|
||||
// Gestion du survol et du clic sur les boutons de défense
|
||||
for (const btn of html.querySelectorAll(".request-defense-btn")) {
|
||||
btn.addEventListener("mouseenter", () => {
|
||||
const tokenId = btn.dataset.tokenId
|
||||
if (tokenId) {
|
||||
const token = canvas.tokens.get(tokenId)
|
||||
if (token) {
|
||||
token.control({ releaseOthers: true })
|
||||
canvas.animatePan(token.center)
|
||||
}
|
||||
}
|
||||
})
|
||||
btn.addEventListener("mouseleave", () => canvas.tokens.releaseAll())
|
||||
|
||||
// Gestionnaire pour les boutons de demande de défense
|
||||
btn.addEventListener("click", async event => {
|
||||
event.preventDefault()
|
||||
|
||||
+10
-8
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user