Various initiative fixes + shield management messages
All checks were successful
Release Creation / build (release) Successful in 46s
All checks were successful
Release Creation / build (release) Successful in 46s
This commit is contained in:
@@ -250,6 +250,18 @@ Hooks.on(hookName, (message, html, data) => {
|
||||
const attackWeaponId = message.rolls[0]?.rollTarget?.weapon?.id || message.rolls[0]?.rollTarget?.weapon?._id
|
||||
const attackRollType = message.rolls[0]?.type
|
||||
const attackRollKey = message.rolls[0]?.rollTarget?.rollKey
|
||||
const attackD30result = message.rolls[0]?.options?.D30result || null
|
||||
const attackD30message = message.rolls[0]?.options?.D30message || null
|
||||
const attackRerollContext = {
|
||||
rollType: message.rolls[0]?.options?.rollType,
|
||||
rollTarget: foundry.utils.duplicate(message.rolls[0]?.options?.rollTarget ?? {}),
|
||||
actorId: message.rolls[0]?.options?.actorId,
|
||||
actorName: message.rolls[0]?.options?.actorName,
|
||||
actorImage: message.rolls[0]?.options?.actorImage,
|
||||
defenderId: combatant.actor?.id || null,
|
||||
defenderTokenId: tokenId || combatant.token?.id || null,
|
||||
rollContext: foundry.utils.duplicate(message.rolls[0]?.options?.rollData ?? {})
|
||||
}
|
||||
|
||||
// Préparer le message de demande de défense
|
||||
const defenseMsg = {
|
||||
@@ -262,6 +274,9 @@ Hooks.on(hookName, (message, html, data) => {
|
||||
attackWeaponId: attackWeaponId,
|
||||
attackRollType: attackRollType,
|
||||
attackRollKey: attackRollKey,
|
||||
attackD30result: attackD30result,
|
||||
attackD30message: attackD30message,
|
||||
attackRerollContext: attackRerollContext,
|
||||
combatantId: combatantId,
|
||||
tokenId: tokenId
|
||||
}
|
||||
@@ -318,6 +333,7 @@ Hooks.on(hookName, (message, html, data) => {
|
||||
let attackerId = button.data("attacker-id")
|
||||
const defenderId = button.data("defender-id")
|
||||
const defenderTokenId = button.data("defender-token-id") || null
|
||||
const extraShieldDr = Number(button.data("extra-shield-dr") || 0)
|
||||
const damageType = button.data("damage-type")
|
||||
const damageFormula = button.data("damage-formula")
|
||||
const damageModifier = button.data("damage-modifier")
|
||||
@@ -332,7 +348,7 @@ Hooks.on(hookName, (message, html, data) => {
|
||||
|
||||
// Pour les boutons de résultat de combat (monster damage)
|
||||
if (damageType === "monster" && attackKey) {
|
||||
await actor.system.prepareMonsterRoll("monster-damage", attackKey, undefined, undefined, undefined, defenderId, defenderTokenId)
|
||||
await actor.system.prepareMonsterRoll("monster-damage", attackKey, undefined, undefined, undefined, defenderId, defenderTokenId, extraShieldDr)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -351,7 +367,7 @@ Hooks.on(hookName, (message, html, data) => {
|
||||
|
||||
// Lancer les dégâts avec la bonne méthode
|
||||
const rollType = damageType === "small" ? "weapon-damage-small" : "weapon-damage-medium"
|
||||
await actor.prepareRoll(rollType, weaponId, undefined, defenderId, defenderTokenId)
|
||||
await actor.prepareRoll(rollType, weaponId, undefined, defenderId, defenderTokenId, extraShieldDr)
|
||||
})
|
||||
|
||||
// Masquer les boutons de dommages dans les messages de résultat de combat si l'utilisateur n'est pas l'attaquant
|
||||
@@ -410,8 +426,21 @@ Hooks.on("createChatMessage", async (message) => {
|
||||
return
|
||||
}
|
||||
|
||||
const { attackerId, attackRoll, attackerName, defenderName, attackWeaponId, attackRollType, attackRollKey, defenderId, defenderTokenId } = attackData
|
||||
const {
|
||||
attackerId,
|
||||
attackRoll,
|
||||
attackerName,
|
||||
defenderName,
|
||||
attackWeaponId,
|
||||
attackRollType,
|
||||
attackRollKey,
|
||||
attackD30message,
|
||||
attackRerollContext,
|
||||
defenderId,
|
||||
defenderTokenId
|
||||
} = attackData
|
||||
let defenseRoll = message.rolls[0]?.options?.rollTotal || message.rolls[0]?.total || 0
|
||||
const defenseD30message = message.rolls[0]?.options?.D30message || null
|
||||
|
||||
console.log("Processing defense:", { attackRoll, defenseRoll, attackerId, defenderId })
|
||||
|
||||
@@ -423,26 +452,165 @@ Hooks.on("createChatMessage", async (message) => {
|
||||
// Récupérer le défenseur et l'attaquant
|
||||
const defender = game.actors.get(defenderId)
|
||||
const attacker = game.actors.get(attackerId)
|
||||
const defenseRerollContext = {
|
||||
rollType: message.rolls[0]?.options?.rollType,
|
||||
rollTarget: foundry.utils.duplicate(message.rolls[0]?.options?.rollTarget ?? {}),
|
||||
actorId: message.rolls[0]?.options?.actorId,
|
||||
actorName: message.rolls[0]?.options?.actorName,
|
||||
actorImage: message.rolls[0]?.options?.actorImage,
|
||||
defenderId,
|
||||
defenderTokenId,
|
||||
rollContext: foundry.utils.duplicate(message.rolls[0]?.options?.rollData ?? {})
|
||||
}
|
||||
|
||||
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 createReactionMessage = async (actorDocument, content) => {
|
||||
await ChatMessage.create({
|
||||
content,
|
||||
speaker: ChatMessage.getSpeaker({ actor: actorDocument })
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
let defenderHandledBonus = false
|
||||
if (defender?.type === "character" && defenseRoll < attackRoll && !game.user.isGM && defender.isOwner) {
|
||||
const hasGritOrLuck = (defender.system.grit.current > 0) || (defender.system.luck.current > 0)
|
||||
let shieldReaction = null
|
||||
if (defender && defenseRoll < attackRoll && isPrimaryController(defender)) {
|
||||
const shieldData = LethalFantasyUtils.getShieldReactionData(defender)
|
||||
let canRerollDefense = LethalFantasyUtils.hasD30Reroll(defenseD30message)
|
||||
let canShieldReact = !!shieldData
|
||||
|
||||
if (hasGritOrLuck) {
|
||||
const bonusRoll = await LethalFantasyUtils.offerGritLuckBonus(
|
||||
defender,
|
||||
attackRoll,
|
||||
defenseRoll,
|
||||
attackerName,
|
||||
defenderName
|
||||
)
|
||||
if (bonusRoll > 0) {
|
||||
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"
|
||||
})
|
||||
}
|
||||
|
||||
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}</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)
|
||||
defenseRoll += shieldBonus
|
||||
shieldReaction = {
|
||||
damageReduction: shieldData.damageReduction,
|
||||
label: shieldData.label,
|
||||
bonus: shieldBonus
|
||||
}
|
||||
canShieldReact = false
|
||||
await createReactionMessage(
|
||||
defender,
|
||||
`<p><strong>${defenderName}</strong> rolls <strong>${shieldData.label}</strong> and adds <strong>${shieldBonus}</strong> to defense.${defenseRoll < attackRoll ? ` The hit still lands, but shield DR <strong>${shieldData.damageReduction}</strong> will reduce the damage.` : ""}</p>`
|
||||
)
|
||||
}
|
||||
}
|
||||
defenderHandledBonus = true
|
||||
}
|
||||
|
||||
let attackRollFinal = attackRoll
|
||||
@@ -450,31 +618,98 @@ Hooks.on("createChatMessage", async (message) => {
|
||||
|
||||
// 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 (attacker?.type === "character" && attackRollFinal <= defenseRoll && attacker.system.grit.current > 0) {
|
||||
// Vérifier si l'utilisateur est un propriétaire non-GM de l'attaquant
|
||||
const isAttackerOwner = !game.user.isGM && attacker.testUserPermission(game.user, "OWNER")
|
||||
if (!defenderHandledBonus && attacker && attackRollFinal <= defenseRoll && isPrimaryController(attacker)) {
|
||||
let canRerollAttack = LethalFantasyUtils.hasD30Reroll(attackD30message)
|
||||
|
||||
if (isAttackerOwner) {
|
||||
console.log("Offering Grit to attacker")
|
||||
while (attackRollFinal <= defenseRoll) {
|
||||
const currentGrit = Number(attacker.system?.grit?.current) || 0
|
||||
const buttons = []
|
||||
|
||||
const attackBonus = await LethalFantasyUtils.offerAttackerGritBonus(
|
||||
attacker,
|
||||
attackRollFinal,
|
||||
defenseRoll,
|
||||
attackerName,
|
||||
defenderName
|
||||
)
|
||||
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",
|
||||
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}</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
|
||||
|
||||
attackRollFinal += attackBonus
|
||||
attackerHandledBonus = true
|
||||
} else {
|
||||
console.log("Not attacker owner or is GM, skipping Grit offer")
|
||||
|
||||
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>.</p>`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const shieldDamageReduction = shieldReaction && attackRollFinal > defenseRoll ? shieldReaction.damageReduction : 0
|
||||
const outcome = shieldDamageReduction > 0 ? "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 && defender.isOwner)
|
||||
const shouldCreateMessage = attackerHandledBonus || (!attackerHandledBonus && defenderHandledBonus) || (!attackerHandledBonus && !defenderHandledBonus && isPrimaryController(defender))
|
||||
|
||||
if (shouldCreateMessage) {
|
||||
console.log("Creating comparison message", { attackerHandledBonus, defenderHandledBonus, isDefenderOwner: defender.isOwner })
|
||||
@@ -489,7 +724,9 @@ Hooks.on("createChatMessage", async (message) => {
|
||||
defenderName,
|
||||
defenderId,
|
||||
defenderTokenId,
|
||||
defenseRoll
|
||||
defenseRoll,
|
||||
outcome,
|
||||
shieldDamageReduction
|
||||
})
|
||||
} else {
|
||||
console.log("Skipping message creation", { attackerHandledBonus, defenderHandledBonus })
|
||||
@@ -552,7 +789,9 @@ Hooks.on("createChatMessage", async (message) => {
|
||||
|
||||
// Calculer les DR
|
||||
const armorDR = defender.computeDamageReduction() || 0
|
||||
const finalDamage = Math.max(0, damageTotal - armorDR)
|
||||
const extraShieldDr = Number(message.rolls[0]?.options?.extraShieldDr) || 0
|
||||
const totalDR = armorDR + extraShieldDr
|
||||
const finalDamage = Math.max(0, damageTotal - totalDR)
|
||||
|
||||
// Prefer the token ID stored in roll options (set at attack time when the exact token is known).
|
||||
// For unlinked tokens (default for monsters), this ensures we target the right instance even
|
||||
@@ -584,7 +823,7 @@ Hooks.on("createChatMessage", async (message) => {
|
||||
{
|
||||
targetName: defender.name,
|
||||
damage: finalDamage,
|
||||
drText: armorDR > 0 ? `Armor DR: ${armorDR}` : "",
|
||||
drText: totalDR > 0 ? `Armor DR: ${armorDR}${extraShieldDr > 0 ? ` + Shield DR: ${extraShieldDr}` : ""}` : "",
|
||||
weaponName: weaponName,
|
||||
attackerName: attackerName,
|
||||
rawDamage: damageTotal
|
||||
@@ -624,4 +863,4 @@ async function registerWorldCount(registerKey) {
|
||||
console.log("No usage log ")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user