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
+19 -74
View File
@@ -28,63 +28,34 @@ export async function processD30BonusDice(d30Message, side, naturalRoll = null,
return { modifier, specialEffect: null, specialName: null }
}
// ── Choice type ── present all options to the player
// ── Choice type ── auto-roll bonus dice, alert about special effects
if (d30Message.type === "choice") {
// If we can't show dialogs (wrong client), skip — the primary client
// will communicate its choice result via socket. Auto-rolling here
// would give a different modifier on each client, causing divergence.
// Non-controlling client can't roll dice here — the controlling client
// sends the updated values via socket.
if (!canDialog) {
return { modifier: 0, specialEffect: null, specialName: null }
}
const buttons = d30Message.choices.map(c => {
let label
let icon
if (c.type === "bonus_dice") {
label = `Roll ${c.dice.toUpperCase()} and add to ${side}`
icon = "fa-solid fa-dice"
} else if (c.type === "special_strike") {
label = _buildSpecialLabel(c, naturalRoll)
icon = "fa-solid fa-star"
} else if (c.type === "special_defense") {
label = _buildSpecialLabel(c, naturalRoll)
icon = "fa-solid fa-shield-halved"
} else {
label = c.type.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase())
icon = "fa-solid fa-question"
}
return {
action: c.type,
type: "button",
label,
icon,
callback: () => c
}
})
const choice = await foundry.applications.api.DialogV2.wait({
window: { title: "D30 Special — Choose Effect" },
classes: ["lethalfantasy"],
content: await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/dialogs/d30-special-choice.hbs", {
description: d30Message.description
}),
buttons,
rejectClose: false
})
if (!choice) return { modifier: 0, specialEffect: null, specialName: null }
if (choice.type === "bonus_dice") {
const modifier = await _rollD30BonusDie(choice.dice, actor)
return { modifier, specialEffect: null, specialName: null }
// Auto-roll bonus dice (like d6E on 27 — no dialog)
const bonusChoice = d30Message.choices.find(c => c.type === "bonus_dice")
let modifier = 0
if (bonusChoice) {
modifier = await _rollD30BonusDie(bonusChoice.dice, actor)
}
if (choice.type === "special_strike" || choice.type === "special_defense") {
return { modifier: 0, specialEffect: "auto", specialName: _buildSpecialName(choice, naturalRoll) }
// Inform about special strike/defense or other effects (informational only)
const specialChoice = d30Message.choices.find(c => c.type === "special_strike" || c.type === "special_defense")
if (specialChoice) {
return { modifier, specialEffect: "flag", specialName: _buildSpecialName(specialChoice, naturalRoll) }
}
// Non-standard choice (spell_calamity, etc.) — report it
return { modifier: 0, specialEffect: "flag", specialName: choice.type }
const nonStandardChoice = d30Message.choices.find(c => c.type !== "bonus_dice")
if (nonStandardChoice) {
return { modifier, specialEffect: "flag", specialName: _buildSpecialName(nonStandardChoice, naturalRoll) }
}
return { modifier, specialEffect: null, specialName: null }
}
// ── Combo type (bleed / internal injury) — flag for wound creation
@@ -130,32 +101,6 @@ export async function _rollD30BonusDie(formula, actor, silent = false) {
return roll.total
}
/**
* Build a human-readable label for a special strike/defense choice in the D30 prompt.
* @param {Object} specialChoice The choice object with type and options
* @param {number|null} naturalRoll The natural D20 roll
* @returns {string} Display label
*/
export function _buildSpecialLabel(specialChoice, naturalRoll) {
if (specialChoice.type === "special_strike") {
if (specialChoice.options.includes("lethal")) {
if (naturalRoll === 20) return "Lethal Strike (auto-hit)"
if (naturalRoll >= 16 && naturalRoll <= 19) return "Vital Strike (auto-hit)"
return "Lethal/Vital Strike (auto-hit)"
}
if (specialChoice.options.includes("vicious")) return "Vicious Strike (auto-hit)"
return "Special Strike (auto-hit)"
}
if (specialChoice.type === "special_defense") {
if (specialChoice.options.includes("perfect_spell")) return "Perfect Spell Defense (auto-block)"
if (specialChoice.options.includes("flawless")) return "Flawless Defense (auto-block)"
if (specialChoice.options.includes("legendary")) return "Legendary Defense (auto-block)"
if (specialChoice.options.includes("perfect")) return "Perfect Defense (auto-block)"
return "Special Defense (auto-block)"
}
return "Special Effect"
}
/**
* Build the special effect name based on the D30 result and natural roll.
* @param {Object} specialChoice The choice object with type and options
@@ -179,5 +124,5 @@ export function _buildSpecialName(specialChoice, naturalRoll) {
if (specialChoice.options.includes("perfect")) return "Perfect Defense"
return "Special Defense"
}
return "Special Effect"
return specialChoice.type.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase())
}