refactor: remove D30 choice dialog, extract defense reaction buttons, fix bugs
Release Creation / build (release) Successful in 45s

- Remove D30 choice dialog — auto-roll bonus dice, flag special effects
- Fix d30ChangedAttack infinite loop in defense do-while (missing reset)
- Fix chat button dataset attributes (rollType/rollTarget/rollAvantage)
- Extract buildDefenseReactionButtons from both defense loops
- Merge Aether/Grace deduction via _deductResourceOnCast helper
- Extract HP HUD toggling (_toggleHudWraps/_disableHudWraps)
- Fix SYSTEM.EQUIPMENT_CATEGORIES typo in equipment model
- Add missing imports to combat.mjs
- Remove dead d30Auto branches, _buildSpecialLabel, d30-special-choice.hbs
This commit is contained in:
2026-06-29 11:44:46 +02:00
parent 41b1199704
commit 25648aa2a3
10 changed files with 202 additions and 517 deletions
+39 -149
View File
@@ -14,12 +14,12 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => {
} else {
for (const btn of html.querySelectorAll(".ask-roll-dice")) {
btn.addEventListener("click", () => {
const type = btn.dataset.type
const value = btn.dataset.value
const avantage = btn.dataset.avantage ?? "="
const type = btn.dataset.rollType
const value = btn.dataset.rollTarget
const avantage = btn.dataset.rollAvantage ?? "normal"
const character = game.user.character
if (type === SYSTEM.ROLL_TYPE.RESOURCE) character.rollResource(value)
else if (type === SYSTEM.ROLL_TYPE.SAVE) character.rollSave(value, avantage)
if (type === "resource") character.rollResource(value)
else if (type === "save") character.rollSave(value, avantage)
})
}
}
@@ -453,6 +453,7 @@ Hooks.on("createChatMessage", async (message) => {
mulliganRestart = false
defenderHandledBonus = false
attackerHandledBonus = false
d30ChangedAttack = false
// ── D30 bonus dice (defense) — resolved before grit/luck/shield ───────
if (defenseD30message && !defenseD30Processed && isPrimaryController(defender) && !attackerIsCrossClient) {
@@ -463,10 +464,6 @@ Hooks.on("createChatMessage", async (message) => {
await createReactionMessage(defender, {type:"d30Bonus", actorName:defenderName, value:d30Result.modifier, side:"defense"})
}
}
if (d30Result.specialEffect === "auto") {
defenseRoll = attackRollFinal + 1 // auto-block
await createReactionMessage(defender, {type:"d30Auto", actorName:defenderName, specialName:d30Result.specialName||"Special Defense", side:"defense"})
}
if (d30Result.specialEffect === "flag") {
await createReactionMessage(defender, {type:"d30Flag", actorName:defenderName, specialName:d30Result.specialName||"Special Effect"})
}
@@ -483,66 +480,7 @@ Hooks.on("createChatMessage", async (message) => {
// create the comparison message with the updated attack roll.
if (defender && defenseRoll < attackRollFinal && isPrimaryController(defender) && !isSpellOrMiracle && !attackerIsCrossClient && !d30PendingFromGM) {
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"
})
}
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"
})
}
buttons.push({
action: "continue",
label: "Continue (no defense bonus)",
icon: "fa-solid fa-forward",
callback: () => "continue"
})
const buttons = LethalFantasyUtils.buildDefenseReactionButtons(defender, { canRerollDefense, shieldData, canShieldReact, canAdHocShield })
const dialogContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/defense-reaction.hbs", {
attackerName,
@@ -569,7 +507,7 @@ Hooks.on("createChatMessage", async (message) => {
if (choice === "grit") {
const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender)
defenseRoll += bonusRoll
await defender.update({ "system.grit.current": currentGrit - 1 })
await defender.update({ "system.grit.current": Math.max(0, (Number(defender.system?.grit?.current) || 0) - 1) })
await createReactionMessage(defender, {type:"grit", actorName:defenderName, resource:"Grit", value:bonusRoll, side:"defense"})
continue
}
@@ -577,7 +515,7 @@ Hooks.on("createChatMessage", async (message) => {
if (choice === "luck") {
const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender)
defenseRoll += bonusRoll
await defender.update({ "system.luck.current": currentLuck - 1 })
await defender.update({ "system.luck.current": Math.max(0, (Number(defender.system?.luck?.current) || 0) - 1) })
await createReactionMessage(defender, {type:"luck", actorName:defenderName, resource:"Luck", value:bonusRoll, side:"defense"})
continue
}
@@ -675,12 +613,6 @@ Hooks.on("createChatMessage", async (message) => {
await createReactionMessage(attacker, {type:"d30Bonus", actorName:attackerName, value:d30Result.modifier, side:"attack"})
}
}
if (d30Result.specialEffect === "auto") {
attackRollFinal = defenseRoll + 1 // auto-hit
if (canDialog) {
await createReactionMessage(attacker, {type:"d30Auto", actorName:attackerName, specialName:d30Result.specialName||"Special Strike", side:"attack"})
}
}
if (d30Result.specialEffect === "flag" && canDialog) {
await createReactionMessage(attacker, {type:"d30Flag", actorName:attackerName, specialName:d30Result.specialName||"Special Effect"})
}
@@ -901,93 +833,51 @@ Hooks.on("createChatMessage", async (message) => {
}
})
// Hook: deduct aether when a spell-attack or spell-power roll is posted to chat
Hooks.on("createChatMessage", async (message) => {
if (!["spell-attack", "spell-power"].includes(message.rolls[0]?.options?.rollType)) return
async function _deductResourceOnCast(message, rollTypes, itemType, costFn, resourceField, templateType) {
if (!rollTypes.includes(message.rolls[0]?.options?.rollType)) return
const actorId = message.rolls[0]?.options?.actorId
if (!actorId) return
const actor = game.actors.get(actorId)
if (!actor) return
// Only the primary controller (player owner or GM) handles this
const activePlayerOwners = game.users.filter(u => u.active && !u.isGM && actor.testUserPermission(u, "OWNER"))
const isPrimary = activePlayerOwners.length > 0
? activePlayerOwners[0].id === game.user.id
: game.user.isGM
if (!isPrimary) return
if (!isPrimaryController(actor)) return
const rollTarget = message.rolls[0]?.options?.rollTarget
const spellId = rollTarget?.id || rollTarget?._id
const spell = spellId ? actor.items.get(spellId) : null
if (!spell || spell.type !== "spell") return
const itemId = rollTarget?.id || rollTarget?._id
const item = itemId ? actor.items.get(itemId) : null
if (!item || item.type !== itemType) return
const damageTier = message.rolls[0]?.options?.damageTier || "standard"
const tierCostMap = { standard: "cost", overpowered: "costOverpowered", overpowered2: "costOverpowered2" }
const costField = tierCostMap[damageTier] || "cost"
const cost = Number(spell.system?.[costField]) || 0
const cost = costFn(item, damageTier)
if (cost <= 0) return
const currentAether = Number(actor.system.aetherPoints?.value) || 0
const newAether = Math.max(0, currentAether - cost)
await actor.update({ "system.aetherPoints.value": newAether })
const current = Number(foundry.utils.getProperty(actor.system, resourceField)) || 0
const newValue = Math.max(0, current - cost)
await actor.update({ [`system.${resourceField}`]: newValue })
const tierLabel = damageTier === "standard" ? "" : ` (${damageTier})`
const aetherContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {
type: "aetherSpend",
actorName: actor.name,
spellName: spell.name,
tierLabel,
value: cost,
oldValue: currentAether,
newValue: newAether
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {
type: templateType, actorName: actor.name, spellName: item.name, tierLabel,
value: cost, oldValue: current, newValue
})
await ChatMessage.create({
content: aetherContent,
speaker: ChatMessage.getSpeaker({ actor })
})
})
await ChatMessage.create({ content, speaker: ChatMessage.getSpeaker({ actor }) })
}
// Hook: deduct aether when a spell-attack or spell-power roll is posted to chat
Hooks.on("createChatMessage", (message) => _deductResourceOnCast(message,
["spell-attack", "spell-power"], "spell",
(item, tier) => {
const m = { standard: "cost", overpowered: "costOverpowered", overpowered2: "costOverpowered2" }
return Number(item.system?.[m[tier] || "cost"]) || 0
},
"aetherPoints.value", "aetherSpend"
))
// Hook: deduct grace when a miracle-attack or miracle-power roll is posted to chat
Hooks.on("createChatMessage", async (message) => {
if (!["miracle-attack", "miracle-power"].includes(message.rolls[0]?.options?.rollType)) return
const actorId = message.rolls[0]?.options?.actorId
if (!actorId) return
const actor = game.actors.get(actorId)
if (!actor) return
const activePlayerOwners = game.users.filter(u => u.active && !u.isGM && actor.testUserPermission(u, "OWNER"))
const isPrimary = activePlayerOwners.length > 0
? activePlayerOwners[0].id === game.user.id
: game.user.isGM
if (!isPrimary) return
const rollTarget = message.rolls[0]?.options?.rollTarget
const miracleId = rollTarget?.id || rollTarget?._id
const miracle = miracleId ? actor.items.get(miracleId) : null
if (!miracle || miracle.type !== "miracle") return
const cost = Number(miracle.system?.level) || 0
if (cost <= 0) return
const currentGrace = Number(actor.system.divinityPoints?.value) || 0
const newGrace = Math.max(0, currentGrace - cost)
await actor.update({ "system.divinityPoints.value": newGrace })
const graceContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {
type: "graceSpend",
actorName: actor.name,
spellName: miracle.name,
value: cost,
oldValue: currentGrace,
newValue: newGrace
})
await ChatMessage.create({
content: graceContent,
speaker: ChatMessage.getSpeaker({ actor })
})
})
Hooks.on("createChatMessage", (message) => _deductResourceOnCast(message,
["miracle-attack", "miracle-power"], "miracle",
(item) => Number(item.system?.level) || 0,
"divinityPoints.value", "graceSpend"
))
// Hook pour appliquer automatiquement les dégâts si une cible est définie
Hooks.on("createChatMessage", async (message) => {