|
|
|
@@ -526,281 +526,294 @@ Hooks.on("createChatMessage", async (message) => {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Si le défenseur est un personnage qui perd, proposer Grit/Luck (seulement s'il a des points)
|
|
|
|
|
// Seulement si l'utilisateur actuel est le propriétaire du défenseur
|
|
|
|
|
// 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
|
|
|
|
|
let attackerHandledBonus = false
|
|
|
|
|
let shieldReaction = null
|
|
|
|
|
let shieldBlocked = false
|
|
|
|
|
const isSpellOrMiracle = attackRollType === "spell-attack" || attackRollType === "miracle-attack"
|
|
|
|
|
if (defender && defenseRoll < attackRoll && isPrimaryController(defender) && !isSpellOrMiracle) {
|
|
|
|
|
const shieldData = LethalFantasyUtils.getShieldReactionData(defender)
|
|
|
|
|
let canRerollDefense = LethalFantasyUtils.hasD30Reroll(defenseD30message)
|
|
|
|
|
let canShieldReact = !!shieldData
|
|
|
|
|
let canAdHocShield = !shieldData
|
|
|
|
|
|
|
|
|
|
while (defenseRoll < attackRoll) {
|
|
|
|
|
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 (canRerollDefense) {
|
|
|
|
|
buttons.push({
|
|
|
|
|
action: "rerollDefense",
|
|
|
|
|
label: "Re-roll defense",
|
|
|
|
|
icon: "fa-solid fa-rotate-right",
|
|
|
|
|
callback: () => "rerollDefense"
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (canShieldReact) {
|
|
|
|
|
buttons.push({
|
|
|
|
|
action: "shieldReact",
|
|
|
|
|
label: `Roll shield (${shieldData.label})`,
|
|
|
|
|
icon: "fa-solid fa-shield",
|
|
|
|
|
callback: () => "shieldReact"
|
|
|
|
|
})
|
|
|
|
|
} else if (canAdHocShield) {
|
|
|
|
|
// No pre-configured shield — offer ad-hoc shield option (useful for monsters)
|
|
|
|
|
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" },
|
|
|
|
|
classes: ["lethalfantasy"],
|
|
|
|
|
content: `
|
|
|
|
|
<div class="grit-luck-dialog">
|
|
|
|
|
<div class="combat-status">
|
|
|
|
|
<p><strong>${attackerName}</strong> rolled <strong>${attackRoll}</strong></p>
|
|
|
|
|
<p><strong>${defenderName}</strong> currently has <strong>${defenseRoll}</strong></p>
|
|
|
|
|
${defenseD30message ? `<p class="bonus-info">D30 special: ${defenseD30message.description}</p>` : ""}
|
|
|
|
|
</div>
|
|
|
|
|
<p class="offer-text">Choose how to improve the defense before resolving the hit.</p>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
|
|
|
|
buttons,
|
|
|
|
|
rejectClose: false
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!choice || choice === "continue") break
|
|
|
|
|
|
|
|
|
|
defenderHandledBonus = true
|
|
|
|
|
|
|
|
|
|
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>`)
|
|
|
|
|
defenseRoll += bonusRoll
|
|
|
|
|
await defender.update({ "system.grit.current": currentGrit - 1 })
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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>`)
|
|
|
|
|
defenseRoll += bonusRoll
|
|
|
|
|
await defender.update({ "system.luck.current": currentLuck - 1 })
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (choice === "bonusDie") {
|
|
|
|
|
const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(defenderName, "attack", defenseRoll, attackRoll)
|
|
|
|
|
if (!bonusDie) continue
|
|
|
|
|
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>`)
|
|
|
|
|
defenseRoll += bonusRoll
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (choice === "rerollDefense" && canRerollDefense) {
|
|
|
|
|
const oldDefenseRoll = defenseRoll
|
|
|
|
|
const reroll = await LethalFantasyUtils.rerollConfiguredRoll(defenseRerollContext)
|
|
|
|
|
canRerollDefense = false
|
|
|
|
|
if (!reroll) continue
|
|
|
|
|
defenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll
|
|
|
|
|
await createReactionMessage(defender, `<p><strong>${defenderName}</strong> uses Mulligan and re-rolls defense: <strong>${oldDefenseRoll}</strong> → <strong>${defenseRoll}</strong>.</p>`)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (choice === "shieldReact" && canShieldReact) {
|
|
|
|
|
const shieldBonus = await LethalFantasyUtils.rollBonusDie(shieldData.formula, defender)
|
|
|
|
|
const newDefenseTotal = defenseRoll + shieldBonus
|
|
|
|
|
defenseRoll = newDefenseTotal
|
|
|
|
|
canShieldReact = false
|
|
|
|
|
|
|
|
|
|
if (newDefenseTotal >= attackRoll) {
|
|
|
|
|
shieldBlocked = true
|
|
|
|
|
shieldReaction = {
|
|
|
|
|
damageReduction: shieldData.damageReduction,
|
|
|
|
|
label: shieldData.label,
|
|
|
|
|
bonus: shieldBonus
|
|
|
|
|
}
|
|
|
|
|
await createReactionMessage(
|
|
|
|
|
defender,
|
|
|
|
|
`<p><strong>${defenderName}</strong> rolls <strong>${shieldData.label}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} ≥ ${attackRoll}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${shieldData.damageReduction}</strong> will apply to damage.</p>`
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
shieldReaction = null
|
|
|
|
|
await createReactionMessage(
|
|
|
|
|
defender,
|
|
|
|
|
`<p><strong>${defenderName}</strong> rolls <strong>${shieldData.label}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRoll}). Shield did not block — normal hit, armor DR only.</p>`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (choice === "adHocShield") {
|
|
|
|
|
const adHoc = await LethalFantasyUtils.promptAdHocShield(defenderName, attackRoll, defenseRoll)
|
|
|
|
|
if (!adHoc) continue
|
|
|
|
|
const shieldBonus = await LethalFantasyUtils.rollBonusDie(adHoc.formula, defender)
|
|
|
|
|
const newDefenseTotal = defenseRoll + shieldBonus
|
|
|
|
|
defenseRoll = newDefenseTotal
|
|
|
|
|
canShieldReact = false
|
|
|
|
|
canAdHocShield = false
|
|
|
|
|
|
|
|
|
|
if (newDefenseTotal >= attackRoll) {
|
|
|
|
|
shieldBlocked = true
|
|
|
|
|
shieldReaction = {
|
|
|
|
|
damageReduction: adHoc.damageReduction,
|
|
|
|
|
label: `${adHoc.formula.toUpperCase()} shield`,
|
|
|
|
|
bonus: shieldBonus
|
|
|
|
|
}
|
|
|
|
|
await createReactionMessage(
|
|
|
|
|
defender,
|
|
|
|
|
`<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} ≥ ${attackRoll}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${adHoc.damageReduction}</strong> will apply to damage.</p>`
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
shieldReaction = null
|
|
|
|
|
await createReactionMessage(
|
|
|
|
|
defender,
|
|
|
|
|
`<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRoll}). Shield did not block — normal hit, armor DR only.</p>`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// These persist across mulligan restarts (once used they stay consumed)
|
|
|
|
|
const shieldData = LethalFantasyUtils.getShieldReactionData(defender)
|
|
|
|
|
let canRerollDefense = LethalFantasyUtils.hasD30Reroll(defenseD30message)
|
|
|
|
|
let canShieldReact = !!shieldData
|
|
|
|
|
let canAdHocShield = !shieldData
|
|
|
|
|
let attackRollFinal = attackRoll
|
|
|
|
|
let attackerHandledBonus = false
|
|
|
|
|
let canRerollAttack = LethalFantasyUtils.hasD30Reroll(attackD30message)
|
|
|
|
|
let mulliganRestart = false
|
|
|
|
|
|
|
|
|
|
// Si l'attaquant est un personnage qui perd et a du Grit
|
|
|
|
|
// Seulement si l'utilisateur actuel est le propriétaire de l'attaquant (pas le MJ)
|
|
|
|
|
if (!defenderHandledBonus && attacker && attackRollFinal <= defenseRoll && isPrimaryController(attacker)) {
|
|
|
|
|
let canRerollAttack = LethalFantasyUtils.hasD30Reroll(attackD30message)
|
|
|
|
|
do {
|
|
|
|
|
mulliganRestart = false
|
|
|
|
|
defenderHandledBonus = false
|
|
|
|
|
attackerHandledBonus = false
|
|
|
|
|
|
|
|
|
|
while (attackRollFinal <= defenseRoll) {
|
|
|
|
|
const currentGrit = Number(attacker.system?.grit?.current) || 0
|
|
|
|
|
const buttons = []
|
|
|
|
|
// ── Defense reaction loop ──────────────────────────────────────────────
|
|
|
|
|
if (defender && defenseRoll < attackRollFinal && isPrimaryController(defender) && !isSpellOrMiracle) {
|
|
|
|
|
while (defenseRoll < 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"
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentGrit > 0) {
|
|
|
|
|
buttons.push({
|
|
|
|
|
action: "grit",
|
|
|
|
|
label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
|
|
|
|
|
icon: "fa-solid fa-fist-raised",
|
|
|
|
|
callback: () => "grit"
|
|
|
|
|
action: "bonusDie",
|
|
|
|
|
label: "Add bonus die",
|
|
|
|
|
icon: "fa-solid fa-dice",
|
|
|
|
|
callback: () => "bonusDie"
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buttons.push({
|
|
|
|
|
action: "bonusDie",
|
|
|
|
|
label: "Add bonus die",
|
|
|
|
|
icon: "fa-solid fa-dice",
|
|
|
|
|
callback: () => "bonusDie"
|
|
|
|
|
})
|
|
|
|
|
if (canRerollDefense) {
|
|
|
|
|
buttons.push({
|
|
|
|
|
action: "rerollDefense",
|
|
|
|
|
label: "Re-roll defense (Mulligan)",
|
|
|
|
|
icon: "fa-solid fa-rotate-right",
|
|
|
|
|
callback: () => "rerollDefense"
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (canShieldReact) {
|
|
|
|
|
buttons.push({
|
|
|
|
|
action: "shieldReact",
|
|
|
|
|
label: `Roll shield (${shieldData.label})`,
|
|
|
|
|
icon: "fa-solid fa-shield",
|
|
|
|
|
callback: () => "shieldReact"
|
|
|
|
|
})
|
|
|
|
|
} else if (canAdHocShield) {
|
|
|
|
|
buttons.push({
|
|
|
|
|
action: "adHocShield",
|
|
|
|
|
label: "Roll ad-hoc shield (choose dice + DR)",
|
|
|
|
|
icon: "fa-solid fa-shield-halved",
|
|
|
|
|
callback: () => "adHocShield"
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (canRerollAttack && attackRerollContext) {
|
|
|
|
|
buttons.push({
|
|
|
|
|
action: "rerollAttack",
|
|
|
|
|
label: "Re-roll attack",
|
|
|
|
|
icon: "fa-solid fa-rotate-right",
|
|
|
|
|
callback: () => "rerollAttack"
|
|
|
|
|
action: "continue",
|
|
|
|
|
label: "Continue (no defense bonus)",
|
|
|
|
|
icon: "fa-solid fa-forward",
|
|
|
|
|
callback: () => "continue"
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buttons.push({
|
|
|
|
|
action: "continue",
|
|
|
|
|
label: "Continue (no attack bonus)",
|
|
|
|
|
icon: "fa-solid fa-forward",
|
|
|
|
|
callback: () => "continue"
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const choice = await foundry.applications.api.DialogV2.wait({
|
|
|
|
|
window: { title: "Attack reactions" },
|
|
|
|
|
classes: ["lethalfantasy"],
|
|
|
|
|
content: `
|
|
|
|
|
<div class="grit-luck-dialog">
|
|
|
|
|
<div class="combat-status">
|
|
|
|
|
<p><strong>${attackerName}</strong> currently has <strong>${attackRollFinal}</strong></p>
|
|
|
|
|
<p><strong>${defenderName}</strong> rolled <strong>${defenseRoll}</strong></p>
|
|
|
|
|
${attackD30message ? `<p class="bonus-info">D30 special: ${attackD30message.description}</p>` : ""}
|
|
|
|
|
const choice = await foundry.applications.api.DialogV2.wait({
|
|
|
|
|
window: { title: "Defense reactions" },
|
|
|
|
|
classes: ["lethalfantasy"],
|
|
|
|
|
content: `
|
|
|
|
|
<div class="grit-luck-dialog">
|
|
|
|
|
<div class="combat-status">
|
|
|
|
|
<p><strong>${attackerName}</strong> rolled <strong>${attackRollFinal}</strong></p>
|
|
|
|
|
<p><strong>${defenderName}</strong> currently has <strong>${defenseRoll}</strong></p>
|
|
|
|
|
${defenseD30message ? `<p class="bonus-info">D30 special: ${defenseD30message.description}</p>` : ""}
|
|
|
|
|
</div>
|
|
|
|
|
<p class="offer-text">Choose how to improve the defense before resolving the hit.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="offer-text">Choose how to improve the attack before resolving the combat result.</p>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
|
|
|
|
buttons,
|
|
|
|
|
rejectClose: false
|
|
|
|
|
})
|
|
|
|
|
`,
|
|
|
|
|
buttons,
|
|
|
|
|
rejectClose: false
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!choice || choice === "continue") break
|
|
|
|
|
if (!choice || choice === "continue") break
|
|
|
|
|
|
|
|
|
|
attackerHandledBonus = true
|
|
|
|
|
defenderHandledBonus = true
|
|
|
|
|
|
|
|
|
|
if (choice === "grit") {
|
|
|
|
|
const attackBonus = await LethalFantasyUtils.rollBonusDie("1d6", attacker, (total) => `<p><strong>${attackerName}</strong> spends 1 Grit and rolls <strong>${total}</strong> for attack.</p>`)
|
|
|
|
|
attackRollFinal += attackBonus
|
|
|
|
|
await attacker.update({ "system.grit.current": currentGrit - 1 })
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
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>`)
|
|
|
|
|
defenseRoll += bonusRoll
|
|
|
|
|
await defender.update({ "system.grit.current": currentGrit - 1 })
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (choice === "bonusDie") {
|
|
|
|
|
const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(attackerName, "defense", attackRollFinal, defenseRoll)
|
|
|
|
|
if (!bonusDie) continue
|
|
|
|
|
const attackBonus = await LethalFantasyUtils.rollBonusDie(bonusDie, attacker, (total, formula) => `<p><strong>${attackerName}</strong> adds <strong>${formula.toUpperCase()}</strong> and rolls <strong>${total}</strong> for attack.</p>`)
|
|
|
|
|
attackRollFinal += attackBonus
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
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>`)
|
|
|
|
|
defenseRoll += bonusRoll
|
|
|
|
|
await defender.update({ "system.luck.current": currentLuck - 1 })
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (choice === "rerollAttack" && canRerollAttack && attackRerollContext) {
|
|
|
|
|
const oldAttackRoll = attackRollFinal
|
|
|
|
|
const reroll = await LethalFantasyUtils.rerollConfiguredRoll(attackRerollContext)
|
|
|
|
|
canRerollAttack = false
|
|
|
|
|
if (!reroll) continue
|
|
|
|
|
attackRollFinal = reroll.options?.rollTotal || reroll.total || oldAttackRoll
|
|
|
|
|
await createReactionMessage(attacker, `<p><strong>${attackerName}</strong> uses Mulligan and re-rolls attack: <strong>${oldAttackRoll}</strong> → <strong>${attackRollFinal}</strong>.</p>`)
|
|
|
|
|
if (choice === "bonusDie") {
|
|
|
|
|
const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(defenderName, "attack", defenseRoll, attackRollFinal)
|
|
|
|
|
if (!bonusDie) continue
|
|
|
|
|
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>`)
|
|
|
|
|
defenseRoll += bonusRoll
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (choice === "rerollDefense" && canRerollDefense) {
|
|
|
|
|
const oldDefenseRoll = defenseRoll
|
|
|
|
|
const reroll = await LethalFantasyUtils.rerollConfiguredRoll(defenseRerollContext)
|
|
|
|
|
canRerollDefense = false
|
|
|
|
|
if (!reroll) continue
|
|
|
|
|
defenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll
|
|
|
|
|
await createReactionMessage(defender, `<p><strong>${defenderName}</strong> uses Mulligan and re-rolls defense: <strong>${oldDefenseRoll}</strong> → <strong>${defenseRoll}</strong>. Both sides may now react to the new numbers.</p>`)
|
|
|
|
|
// Restart the full comparison so both sides can react to the new roll
|
|
|
|
|
mulliganRestart = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (choice === "shieldReact" && canShieldReact) {
|
|
|
|
|
const shieldBonus = await LethalFantasyUtils.rollBonusDie(shieldData.formula, defender)
|
|
|
|
|
const newDefenseTotal = defenseRoll + shieldBonus
|
|
|
|
|
defenseRoll = newDefenseTotal
|
|
|
|
|
canShieldReact = false
|
|
|
|
|
|
|
|
|
|
if (newDefenseTotal >= attackRollFinal) {
|
|
|
|
|
shieldBlocked = true
|
|
|
|
|
shieldReaction = {
|
|
|
|
|
damageReduction: shieldData.damageReduction,
|
|
|
|
|
label: shieldData.label,
|
|
|
|
|
bonus: shieldBonus
|
|
|
|
|
}
|
|
|
|
|
await createReactionMessage(
|
|
|
|
|
defender,
|
|
|
|
|
`<p><strong>${defenderName}</strong> rolls <strong>${shieldData.label}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} ≥ ${attackRollFinal}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${shieldData.damageReduction}</strong> will apply to damage.</p>`
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
shieldReaction = null
|
|
|
|
|
await createReactionMessage(
|
|
|
|
|
defender,
|
|
|
|
|
`<p><strong>${defenderName}</strong> rolls <strong>${shieldData.label}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRollFinal}). Shield did not block — normal hit, armor DR only.</p>`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (choice === "adHocShield") {
|
|
|
|
|
const adHoc = await LethalFantasyUtils.promptAdHocShield(defenderName, attackRollFinal, defenseRoll)
|
|
|
|
|
if (!adHoc) continue
|
|
|
|
|
const shieldBonus = await LethalFantasyUtils.rollBonusDie(adHoc.formula, defender)
|
|
|
|
|
const newDefenseTotal = defenseRoll + shieldBonus
|
|
|
|
|
defenseRoll = newDefenseTotal
|
|
|
|
|
canShieldReact = false
|
|
|
|
|
canAdHocShield = false
|
|
|
|
|
|
|
|
|
|
if (newDefenseTotal >= attackRollFinal) {
|
|
|
|
|
shieldBlocked = true
|
|
|
|
|
shieldReaction = {
|
|
|
|
|
damageReduction: adHoc.damageReduction,
|
|
|
|
|
label: `${adHoc.formula.toUpperCase()} shield`,
|
|
|
|
|
bonus: shieldBonus
|
|
|
|
|
}
|
|
|
|
|
await createReactionMessage(
|
|
|
|
|
defender,
|
|
|
|
|
`<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>`
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
shieldReaction = null
|
|
|
|
|
await createReactionMessage(
|
|
|
|
|
defender,
|
|
|
|
|
`<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>`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mulliganRestart) continue
|
|
|
|
|
|
|
|
|
|
// ── Attack reaction loop ───────────────────────────────────────────────
|
|
|
|
|
if (!defenderHandledBonus && attacker && attackRollFinal <= defenseRoll && isPrimaryController(attacker)) {
|
|
|
|
|
while (attackRollFinal <= defenseRoll) {
|
|
|
|
|
const currentGrit = Number(attacker.system?.grit?.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"
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buttons.push({
|
|
|
|
|
action: "bonusDie",
|
|
|
|
|
label: "Add bonus die",
|
|
|
|
|
icon: "fa-solid fa-dice",
|
|
|
|
|
callback: () => "bonusDie"
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (canRerollAttack && attackRerollContext) {
|
|
|
|
|
buttons.push({
|
|
|
|
|
action: "rerollAttack",
|
|
|
|
|
label: "Re-roll attack (Mulligan)",
|
|
|
|
|
icon: "fa-solid fa-rotate-right",
|
|
|
|
|
callback: () => "rerollAttack"
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buttons.push({
|
|
|
|
|
action: "continue",
|
|
|
|
|
label: "Continue (no attack bonus)",
|
|
|
|
|
icon: "fa-solid fa-forward",
|
|
|
|
|
callback: () => "continue"
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const choice = await foundry.applications.api.DialogV2.wait({
|
|
|
|
|
window: { title: "Attack reactions" },
|
|
|
|
|
classes: ["lethalfantasy"],
|
|
|
|
|
content: `
|
|
|
|
|
<div class="grit-luck-dialog">
|
|
|
|
|
<div class="combat-status">
|
|
|
|
|
<p><strong>${attackerName}</strong> currently has <strong>${attackRollFinal}</strong></p>
|
|
|
|
|
<p><strong>${defenderName}</strong> rolled <strong>${defenseRoll}</strong></p>
|
|
|
|
|
${attackD30message ? `<p class="bonus-info">D30 special: ${attackD30message.description}</p>` : ""}
|
|
|
|
|
</div>
|
|
|
|
|
<p class="offer-text">Choose how to improve the attack before resolving the combat result.</p>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
|
|
|
|
buttons,
|
|
|
|
|
rejectClose: false
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!choice || choice === "continue") break
|
|
|
|
|
|
|
|
|
|
attackerHandledBonus = true
|
|
|
|
|
|
|
|
|
|
if (choice === "grit") {
|
|
|
|
|
const attackBonus = await LethalFantasyUtils.rollBonusDie("1d6", attacker, total => `<p><strong>${attackerName}</strong> spends 1 Grit and rolls <strong>${total}</strong> for attack.</p>`)
|
|
|
|
|
attackRollFinal += attackBonus
|
|
|
|
|
await attacker.update({ "system.grit.current": currentGrit - 1 })
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (choice === "bonusDie") {
|
|
|
|
|
const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(attackerName, "defense", attackRollFinal, defenseRoll)
|
|
|
|
|
if (!bonusDie) continue
|
|
|
|
|
const attackBonus = await LethalFantasyUtils.rollBonusDie(bonusDie, attacker, (total, formula) => `<p><strong>${attackerName}</strong> adds <strong>${formula.toUpperCase()}</strong> and rolls <strong>${total}</strong> for attack.</p>`)
|
|
|
|
|
attackRollFinal += attackBonus
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (choice === "rerollAttack" && canRerollAttack && attackRerollContext) {
|
|
|
|
|
const oldAttackRoll = attackRollFinal
|
|
|
|
|
const reroll = await LethalFantasyUtils.rerollConfiguredRoll(attackRerollContext)
|
|
|
|
|
canRerollAttack = false
|
|
|
|
|
if (!reroll) continue
|
|
|
|
|
attackRollFinal = reroll.options?.rollTotal || reroll.total || oldAttackRoll
|
|
|
|
|
await createReactionMessage(attacker, `<p><strong>${attackerName}</strong> uses Mulligan and re-rolls attack: <strong>${oldAttackRoll}</strong> → <strong>${attackRollFinal}</strong>. Both sides may now react to the new numbers.</p>`)
|
|
|
|
|
// Restart the full comparison so both sides can react to the new roll
|
|
|
|
|
mulliganRestart = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} while (mulliganRestart)
|
|
|
|
|
|
|
|
|
|
const shieldDamageReduction = shieldBlocked ? shieldReaction.damageReduction : 0
|
|
|
|
|
const outcome = shieldBlocked ? "shielded-hit" : (attackRollFinal > defenseRoll ? "hit" : "miss")
|
|
|
|
|