fix: allow defender to react when attacker boosts past defense via cross-client socket
This commit is contained in:
+29
-1
@@ -1002,9 +1002,37 @@ Hooks.on("createChatMessage", async (message) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If attacker boosted past defense, let the defender react
|
||||||
|
if (attackerHandledBonus && defenseRoll < attackRollFinal && defender) {
|
||||||
|
const defenderOwner = game.users.find(u => u.active && !u.isGM && defender.testUserPermission(u, "OWNER"))
|
||||||
|
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,
|
||||||
|
shieldDamageReduction: shieldBlocked ? shieldReaction?.damageReduction ?? 0 : 0,
|
||||||
|
d30Bleed: d30Bleed ? "true" : "",
|
||||||
|
d30DamageMultiplier, d30DrMultiplier,
|
||||||
|
damageTier: damageTier || "standard",
|
||||||
|
attackD30message,
|
||||||
|
hasShield: !!sData,
|
||||||
|
shieldLabel: sData?.label || "",
|
||||||
|
shieldFormula: sData?.formula || "",
|
||||||
|
shieldDr: sData?.damageReduction || 0,
|
||||||
|
canAdHocShield: !sData,
|
||||||
|
})
|
||||||
|
return // Comparison message created by defender's client
|
||||||
|
}
|
||||||
|
// Single-client (GM controls both): restart so defender loop can run
|
||||||
|
mulliganRestart = true
|
||||||
|
}
|
||||||
} while (mulliganRestart)
|
} while (mulliganRestart)
|
||||||
|
|
||||||
const shieldDamageReduction = shieldBlocked ? shieldReaction.damageReduction : 0
|
const shieldDamageReduction = shieldBlocked ? shieldReaction?.damageReduction ?? 0 : 0
|
||||||
const outcome = shieldBlocked ? "shielded-hit" : (attackRollFinal > defenseRoll ? "hit" : "miss")
|
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
|
// Créer le message de comparaison - uniquement par le client qui a géré le dernier bonus
|
||||||
|
|||||||
@@ -189,6 +189,11 @@ export default class LethalFantasyUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
case "attackBoosted":
|
||||||
|
if (msg.userId === game.user.id) {
|
||||||
|
LethalFantasyUtils.handleAttackBoosted(msg)
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,6 +231,180 @@ export default class LethalFantasyUtils {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------- */
|
||||||
|
static async handleAttackBoosted(msg) {
|
||||||
|
const {
|
||||||
|
attackerName, attackerId, defenderName, defenderId, defenderTokenId,
|
||||||
|
attackRollFinal, defenseRoll, attackWeaponId, attackRollType, attackRollKey,
|
||||||
|
shieldDamageReduction: initialShieldDR,
|
||||||
|
d30Bleed, d30DamageMultiplier, d30DrMultiplier,
|
||||||
|
damageTier, attackD30message,
|
||||||
|
hasShield, shieldLabel, shieldFormula, shieldDr, canAdHocShield
|
||||||
|
} = msg
|
||||||
|
|
||||||
|
const defender = game.actors.get(defenderId)
|
||||||
|
if (!defender) return
|
||||||
|
|
||||||
|
let updatedDefenseRoll = defenseRoll
|
||||||
|
let shieldBlocked = false
|
||||||
|
let shieldReaction = null
|
||||||
|
let canShieldReact = hasShield
|
||||||
|
let canAdHoc = canAdHocShield
|
||||||
|
|
||||||
|
// Show the defense reaction dialog
|
||||||
|
if (defender && updatedDefenseRoll < attackRollFinal) {
|
||||||
|
const currentGrit = Number(defender.system?.grit?.current) || 0
|
||||||
|
const currentLuck = Number(defender.system?.luck?.current) || 0
|
||||||
|
const buttons = []
|
||||||
|
|
||||||
|
if (currentGrit > 0) {
|
||||||
|
buttons.push({
|
||||||
|
action: "grit",
|
||||||
|
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
|
||||||
|
icon: "fa-solid fa-fist-raised",
|
||||||
|
callback: () => "grit"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLuck > 0) {
|
||||||
|
buttons.push({
|
||||||
|
action: "luck",
|
||||||
|
label: `Spend 1 Luck (+1D6) [${currentLuck} left]`,
|
||||||
|
icon: "fa-solid fa-clover",
|
||||||
|
callback: () => "luck"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push({
|
||||||
|
action: "bonusDie",
|
||||||
|
label: "Add bonus die",
|
||||||
|
icon: "fa-solid fa-dice",
|
||||||
|
callback: () => "bonusDie"
|
||||||
|
})
|
||||||
|
|
||||||
|
if (canShieldReact) {
|
||||||
|
buttons.push({
|
||||||
|
action: "shieldReact",
|
||||||
|
label: `Roll shield (${shieldLabel})`,
|
||||||
|
icon: "fa-solid fa-shield",
|
||||||
|
callback: () => "shieldReact"
|
||||||
|
})
|
||||||
|
} else if (canAdHoc) {
|
||||||
|
buttons.push({
|
||||||
|
action: "adHocShield",
|
||||||
|
label: "Roll ad-hoc shield (choose dice + DR)",
|
||||||
|
icon: "fa-solid fa-shield-halved",
|
||||||
|
callback: () => "adHocShield"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push({
|
||||||
|
action: "continue",
|
||||||
|
label: "Continue (no defense bonus)",
|
||||||
|
icon: "fa-solid fa-forward",
|
||||||
|
callback: () => "continue"
|
||||||
|
})
|
||||||
|
|
||||||
|
const choice = await foundry.applications.api.DialogV2.wait({
|
||||||
|
window: { title: "Defense reactions — attack boosted" },
|
||||||
|
classes: ["lethalfantasy"],
|
||||||
|
content: `
|
||||||
|
<div class="grit-luck-dialog">
|
||||||
|
<div class="combat-status">
|
||||||
|
<p><strong>${attackerName}</strong> boosted attack to <strong>${attackRollFinal}</strong></p>
|
||||||
|
<p><strong>${defenderName}</strong> currently has <strong>${updatedDefenseRoll}</strong></p>
|
||||||
|
</div>
|
||||||
|
<p class="offer-text">The attack was boosted! Choose how to improve the defense.</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
buttons,
|
||||||
|
rejectClose: false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (choice === "grit") {
|
||||||
|
const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender,
|
||||||
|
total => `<p><strong>${defenderName}</strong> spends 1 Grit and rolls <strong>${total}</strong> for defense.</p>`)
|
||||||
|
updatedDefenseRoll += bonusRoll
|
||||||
|
await defender.update({ "system.grit.current": currentGrit - 1 })
|
||||||
|
} else if (choice === "luck") {
|
||||||
|
const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender,
|
||||||
|
total => `<p><strong>${defenderName}</strong> spends 1 Luck and rolls <strong>${total}</strong> for defense.</p>`)
|
||||||
|
updatedDefenseRoll += bonusRoll
|
||||||
|
await defender.update({ "system.luck.current": currentLuck - 1 })
|
||||||
|
} else if (choice === "bonusDie") {
|
||||||
|
const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(defenderName, "attack", updatedDefenseRoll, attackRollFinal)
|
||||||
|
if (bonusDie) {
|
||||||
|
const bonusRoll = await LethalFantasyUtils.rollBonusDie(bonusDie, defender,
|
||||||
|
(total, formula) => `<p><strong>${defenderName}</strong> adds <strong>${formula.toUpperCase()}</strong> and rolls <strong>${total}</strong> for defense.</p>`)
|
||||||
|
updatedDefenseRoll += bonusRoll
|
||||||
|
}
|
||||||
|
} else if (choice === "shieldReact" && canShieldReact) {
|
||||||
|
const shieldBonus = await LethalFantasyUtils.rollBonusDie(shieldFormula, defender)
|
||||||
|
const newDefenseTotal = updatedDefenseRoll + shieldBonus
|
||||||
|
updatedDefenseRoll = newDefenseTotal
|
||||||
|
canShieldReact = false
|
||||||
|
if (newDefenseTotal >= attackRollFinal) {
|
||||||
|
shieldBlocked = true
|
||||||
|
shieldReaction = { damageReduction: shieldDr, label: shieldLabel, bonus: shieldBonus }
|
||||||
|
await ChatMessage.create({
|
||||||
|
content: `<p><strong>${defenderName}</strong> rolls <strong>${shieldLabel}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} ≥ ${attackRollFinal}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${shieldDr}</strong> will apply to damage.</p>`,
|
||||||
|
speaker: ChatMessage.getSpeaker({ actor: defender })
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await ChatMessage.create({
|
||||||
|
content: `<p><strong>${defenderName}</strong> rolls <strong>${shieldLabel}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRollFinal}). Shield did not block — normal hit, armor DR only.</p>`,
|
||||||
|
speaker: ChatMessage.getSpeaker({ actor: defender })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (choice === "adHocShield" && canAdHoc) {
|
||||||
|
const adHoc = await LethalFantasyUtils.promptAdHocShield(defenderName, attackRollFinal, updatedDefenseRoll)
|
||||||
|
if (adHoc) {
|
||||||
|
const shieldBonus = await LethalFantasyUtils.rollBonusDie(adHoc.formula, defender)
|
||||||
|
const newDefenseTotal = updatedDefenseRoll + shieldBonus
|
||||||
|
updatedDefenseRoll = newDefenseTotal
|
||||||
|
canShieldReact = false
|
||||||
|
canAdHoc = false
|
||||||
|
if (newDefenseTotal >= attackRollFinal) {
|
||||||
|
shieldBlocked = true
|
||||||
|
shieldReaction = { damageReduction: adHoc.damageReduction, label: `${adHoc.formula.toUpperCase()} shield`, bonus: shieldBonus }
|
||||||
|
await ChatMessage.create({
|
||||||
|
content: `<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} ≥ ${attackRollFinal}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${adHoc.damageReduction}</strong> will apply to damage.</p>`,
|
||||||
|
speaker: ChatMessage.getSpeaker({ actor: defender })
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await ChatMessage.create({
|
||||||
|
content: `<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRollFinal}). Shield did not block — normal hit, armor DR only.</p>`,
|
||||||
|
speaker: ChatMessage.getSpeaker({ actor: defender })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalShieldDR = shieldBlocked ? (shieldReaction?.damageReduction || 0) : 0
|
||||||
|
const outcome = shieldBlocked ? "shielded-hit" : (updatedDefenseRoll > attackRollFinal ? "miss" : "hit")
|
||||||
|
|
||||||
|
await LethalFantasyUtils.compareAttackDefense({
|
||||||
|
attackerName,
|
||||||
|
attackerId,
|
||||||
|
attackRoll: attackRollFinal,
|
||||||
|
attackWeaponId,
|
||||||
|
attackRollType,
|
||||||
|
attackRollKey,
|
||||||
|
defenderName,
|
||||||
|
defenderId,
|
||||||
|
defenderTokenId,
|
||||||
|
defenseRoll: updatedDefenseRoll,
|
||||||
|
outcome,
|
||||||
|
shieldDamageReduction: finalShieldDR,
|
||||||
|
d30Bleed: d30Bleed || "",
|
||||||
|
d30DamageMultiplier: d30DamageMultiplier || 1,
|
||||||
|
d30DrMultiplier: d30DrMultiplier || 1,
|
||||||
|
damageTier: damageTier || "standard",
|
||||||
|
attackD30message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------- */
|
/* -------------------------------------------- */
|
||||||
static async showDefenseRequest(msg) {
|
static async showDefenseRequest(msg) {
|
||||||
const attackerName = msg.attackerName
|
const attackerName = msg.attackerName
|
||||||
|
|||||||
Reference in New Issue
Block a user