feat: D30 combat effects, spell tiers, small damage removal, token HUD luck/grit

- Replace Knockback with Internal Injury on D30 (5, 10, 15); remove Shield Bash from D30 counter-attacks
- Eliminate small weapon damage: keep only medium damage labelled Damage in sheets, rolls, and chat
- D30 bonus dice (20, 27, 30) auto-resolved before grit/luck/shield decisions; choice dialogs for special strikes
- D30 combat effects: bleeding wounds, damage ×2/×3 before DR, DR ×2/×3 with component picker dialog
- Add hp.wounds to monster schema for bleeding support
- Show Save against spell? checkbox for all save rolls (not just magic users)
- Fix mulligan restart: persistent D30 process flags prevent double-application and allow both sides to react
- For Dice So Nice, show main roll animation before explosion dice for correct ordering
- Spell tier selection: force Standard/Overpowered choice at cast time, tier-specific aether cost, only chosen damage button shown
- Add +1/−1 luck and grit controls to Token HUD
- Fix inconsistent indentation, remove duplicate i18n key, remove unused includesShield return
This commit is contained in:
2026-06-10 07:53:51 +02:00
parent b35b684d50
commit ce630feb51
17 changed files with 749 additions and 145 deletions
+35 -14
View File
@@ -285,13 +285,7 @@ export default class LethalFantasyRoll extends Roll {
let damageBonus = (options.rollTarget.weapon.system.applyStrengthDamageBonus) ? options.rollTarget.combat.damageModifier : 0
options.rollTarget.value = damageBonus + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.damageBonus
options.rollTarget.charModifier = damageBonus
if (options.rollType.includes("small")) {
options.damageSmall = true
dice = options.rollTarget.weapon.system.damage.damageS
} else {
options.damageMedium = true
dice = options.rollTarget.weapon.system.damage.damageM
}
dice = options.rollTarget.weapon.system.damage.damageM
if (/NE$/i.test(dice)) {
hasMaxValue = false
hasExplode = false
@@ -529,8 +523,6 @@ export default class LethalFantasyRoll extends Roll {
rollMode: rollContext.visibility,
hasTarget: options.hasTarget,
isDamage: options.isDamage,
damageSmall: options.damageSmall,
damageMedium: options.damageMedium,
pointBlank,
beyondSkill,
letItFly,
@@ -614,6 +606,11 @@ export default class LethalFantasyRoll extends Roll {
options.D30message = d30Message
}
// Show main roll before explosion dice so Dice So Nice animates in correct order
if (game?.dice3d && rollContext.favor !== "favor" && rollContext.favor !== "disfavor") {
await game.dice3d.showForRoll(rollBase, game.user, true)
}
let rollTotal = 0
let diceResults = []
let resultType
@@ -673,6 +670,10 @@ export default class LethalFantasyRoll extends Roll {
rollBase.options.defenderId = options.defenderId
rollBase.options.defenderTokenId = options.defenderTokenId
rollBase.options.extraShieldDr = options.extraShieldDr || 0
rollBase.options.damageTier = options.damageTier || "standard"
rollBase.options.d30Bleed = options.d30Bleed || false
rollBase.options.d30DamageMultiplier = options.d30DamageMultiplier || 1
rollBase.options.d30DrMultiplier = options.d30DrMultiplier || 1
/**
* A hook event that fires after the roll has been made.
@@ -903,6 +904,29 @@ export default class LethalFantasyRoll extends Roll {
actionItem.progressionCount = firstActionTaken ? 1 : (combatant.actor.system.combat?.combatProgressionStart ?? 1)
if (!firstActionTaken) await combatant.setFlag(SYSTEM.id, "firstActionTaken", true)
actionItem.rangedMode = rangedMode
// If this is a spell/miracle with multiple damage tiers, prompt tier choice
if (actionItem.system?.damageDice) {
const tiers = [
{ id: "standard", label: "Standard", dice: actionItem.system.damageDice },
{ id: "overpowered", label: "Overpowered", dice: actionItem.system.damageDiceOverpowered },
{ id: "overpowered2", label: "Overpowered 2", dice: actionItem.system.damageDiceOverpowered2 },
].filter(t => t.dice)
if (tiers.length > 1) {
const tierChoice = await foundry.applications.api.DialogV2.wait({
window: { title: "Choose Damage Tier" },
classes: ["lethalfantasy"],
content: `<div class="grit-luck-dialog"><p><strong>${selectedItem.name}</strong> has multiple damage tiers.</p><p>Choose which damage to use when the attack lands:</p></div>`,
buttons: tiers.map(t => ({
action: t.id,
label: `${t.label} (${t.dice.toUpperCase()})`,
icon: "fa-solid fa-wand-magic-sparkles",
callback: () => t.id
})),
rejectClose: false
})
actionItem.damageTier = tierChoice || "standard"
}
}
actionItem.castingTime = 1
actionItem.spellStatus = "castingTime"
// Set the flag on the combatant
@@ -1392,10 +1416,8 @@ export default class LethalFantasyRoll extends Roll {
return `${game.i18n.localize("LETHALFANTASY.Label.weapon-attack")}`
case "weapon-defense":
return `${game.i18n.localize("LETHALFANTASY.Label.weapon-defense")}`
case "weapon-damage-small":
return `${game.i18n.localize("LETHALFANTASY.Label.weapon-damage-small")}`
case "weapon-damage-medium":
return `${game.i18n.localize("LETHALFANTASY.Label.weapon-damage-medium")}`
case "weapon-damage":
return `${game.i18n.localize("LETHALFANTASY.Label.weapon-damage")}`
case "spell":
case "spell-attack":
case "spell-power":
@@ -1456,7 +1478,6 @@ export default class LethalFantasyRoll extends Roll {
weaponDamageOptions = {
weaponId: weapon._id || weapon.id,
weaponName: weapon.name,
damageS: weapon.system?.damage?.damageS,
damageM: weapon.system?.damage?.damageM
}
console.log("Weapon damage options:", weaponDamageOptions)