diff --git a/css/fvtt-lethal-fantasy.css b/css/fvtt-lethal-fantasy.css index d7af336..4ba2a24 100644 --- a/css/fvtt-lethal-fantasy.css +++ b/css/fvtt-lethal-fantasy.css @@ -4139,6 +4139,59 @@ i.lethalfantasy { padding-left: 8px; font-size: 0.9rem; } +/* Luck/Grit Styles */ +#token-hud .luck-grit-wrap { + position: absolute; + left: 75px; + display: none; + top: 50%; + width: 80px; + text-align: start; + background: rgba(0, 0, 0, 0.8); + border: 1px solid rgba(139, 69, 19, 0.5); + border-radius: 4px; + padding: 4px 6px; + transform: translate(-100%, -50%); +} +#token-hud .luck-grit-hud-active { + display: block; +} +#token-hud .luck-grit-hud-disabled { + display: none; +} +#token-hud .luck-grit-row { + display: flex; + align-items: center; + gap: 4px; + margin: 2px 0; +} +#token-hud .luck-grit-label { + flex: 1; + color: #c9b896; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; +} +#token-hud .luck-grit-btn { + width: 28px; + height: 22px; + padding: 0; + background: rgba(139, 69, 19, 0.25); + border: 1px solid rgba(139, 69, 19, 0.4); + border-radius: 3px; + color: #d4c5a9; + font-size: 12px; + font-weight: 700; + cursor: pointer; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; +} +#token-hud .luck-grit-btn:hover { + background: rgba(139, 69, 19, 0.5); + border-color: rgba(139, 69, 19, 0.7); +} /* -------------------------------------------------- */ /* Dice Tray — injected into the Foundry chat sidebar */ /* -------------------------------------------------- */ diff --git a/lang/en.json b/lang/en.json index 9571b7c..67c28da 100644 --- a/lang/en.json +++ b/lang/en.json @@ -505,8 +505,6 @@ "monster-defense": "Monster defense", "weapons": "Weapons", "wis": "WIS", - "weapon-damage-medium": "Weapon damage medium", - "weapon-damage-small": "Weapon damage small", "combatProgressionStart": "Combat start threshold", "miracle": "Miracle", "titleStandard": "Standard Roll", @@ -828,6 +826,12 @@ "cost": { "label": "Cost" }, + "costOverpowered": { + "label": "Cost (Overpowered)" + }, + "costOverpowered2": { + "label": "Cost (Overpowered 2)" + }, "description": { "label": "Description" }, diff --git a/lethal-fantasy.mjs b/lethal-fantasy.mjs index 1d4a145..c182f37 100644 --- a/lethal-fantasy.mjs +++ b/lethal-fantasy.mjs @@ -242,6 +242,9 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => { console.log(`[LF] request-defense-btn | attackRollType=${attackRollType} defender=${defenderName} defenderType=${combatant.actor?.type}`) const attackD30result = message.rolls[0]?.options?.D30result || null const attackD30message = message.rolls[0]?.options?.D30message || null + const attackDiceResults = message.rolls[0]?.options?.diceResults || null + const attackNaturalRoll = attackDiceResults?.[0]?.value || null + const damageTier = message.rolls[0]?.options?.damageTier || "standard" const attackRerollContext = { rollType: message.rolls[0]?.options?.rollType, rollTarget: foundry.utils.duplicate(message.rolls[0]?.options?.rollTarget ?? {}), @@ -277,6 +280,8 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => { attackD30result, attackD30message, attackRerollContext, + attackNaturalRoll, + damageTier, combatantId, tokenId, isRanged: isRangedAttack @@ -338,6 +343,9 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => { const damageFormula = btn.dataset.damageFormula const damageModifier = btn.dataset.damageModifier const isMonster = btn.dataset.isMonster + const d30Bleed = btn.dataset.d30Bleed === "true" + const d30DamageMultiplier = Number(btn.dataset.d30DamageMult) || 1 + const d30DrMultiplier = Number(btn.dataset.d30DrMult) || 1 // Récupérer l'acteur (soit depuis le message, soit depuis attackerId) const actor = attackerId ? game.actors.get(attackerId) : game.actors.get(message.rolls[0]?.actorId) @@ -392,7 +400,10 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => { defenderTokenId, actorId: actor.id, actorName: actor.name, - actorImage: actor.img + actorImage: actor.img, + d30Bleed, + d30DamageMultiplier, + d30DrMultiplier } await documents.LethalFantasyRoll.rollSpellDamageToMessage(damageFormula, rollOpts) return @@ -400,13 +411,13 @@ Hooks.on("renderChatMessageHTML", (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, extraShieldDr) + await actor.system.prepareMonsterRoll("monster-damage", attackKey, undefined, undefined, undefined, defenderId, defenderTokenId, extraShieldDr, { d30Bleed, d30DamageMultiplier, d30DrMultiplier }) return } // Pour les monstres, utiliser prepareMonsterRoll if (isMonster === "true" || actor.type === "monster") { - await actor.system.prepareMonsterRoll("monster-damage", weaponId, undefined, undefined, damageModifier) + await actor.system.prepareMonsterRoll("monster-damage", weaponId, undefined, undefined, damageModifier, defenderId, defenderTokenId, extraShieldDr, { d30Bleed, d30DamageMultiplier, d30DrMultiplier }) return } @@ -417,9 +428,9 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => { return } - // 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, extraShieldDr) + // Lancer les dégâts + const rollType = "weapon-damage" + await actor.prepareRoll(rollType, weaponId, undefined, defenderId, defenderTokenId, extraShieldDr, { d30Bleed, d30DamageMultiplier, d30DrMultiplier }) }) } @@ -483,6 +494,8 @@ Hooks.on("createChatMessage", async (message) => { attackRollKey, attackD30message, attackRerollContext, + attackNaturalRoll, + damageTier, defenderId, defenderTokenId } = attackData @@ -542,12 +555,50 @@ Hooks.on("createChatMessage", async (message) => { let attackRollFinal = attackRoll let canRerollAttack = LethalFantasyUtils.hasD30Reroll(attackD30message) let mulliganRestart = false + // These persist across mulligan restarts (D30 bonus only applied once) + let defenseD30Processed = false + let attackD30Processed = false + // D30 combat effects for damage application + let d30Bleed = false + let d30DamageMultiplier = 1 + let d30DrMultiplier = 1 do { mulliganRestart = false defenderHandledBonus = false attackerHandledBonus = false + // ── D30 bonus dice (defense) — resolved before grit/luck/shield ─────── + if (defenseD30message && !defenseD30Processed && isPrimaryController(defender)) { + const d30Result = await LethalFantasyUtils.processD30BonusDice(defenseD30message, "defense", null, defender) + if (d30Result.modifier) { + defenseRoll += d30Result.modifier + if (d30Result.modifier > 0) { + await createReactionMessage(defender, + `
${defenderName} gains +${d30Result.modifier} from D30 bonus die for defense.
` + ) + } + } + if (d30Result.specialEffect === "auto") { + defenseRoll = attackRollFinal + 1 // auto-block + await createReactionMessage(defender, + `${defenderName} uses ${d30Result.specialName || "Special Defense"} from D30 — defense automatically succeeds!
` + ) + } + if (d30Result.specialEffect === "flag") { + await createReactionMessage(defender, + `D30 — ${d30Result.specialName || "Special Effect"} triggered for ${defenderName}!
` + ) + } + if (d30Result.specialEffect === "drMultiplier") { + d30DrMultiplier = d30Result.multiplier + await createReactionMessage(defender, + `D30 — Defense grants x${d30Result.multiplier} DR (choose which DR types to multiply when damage is applied)
` + ) + } + defenseD30Processed = true + } + // ── Defense reaction loop ────────────────────────────────────────────── if (defender && defenseRoll < attackRollFinal && isPrimaryController(defender) && !isSpellOrMiracle) { while (defenseRoll < attackRollFinal) { @@ -726,6 +777,43 @@ Hooks.on("createChatMessage", async (message) => { if (mulliganRestart) continue + // ── D30 bonus dice (attack) — resolved before grit/luck ──────────────── + if (attackD30message && !attackD30Processed && isPrimaryController(attacker)) { + const d30Result = await LethalFantasyUtils.processD30BonusDice(attackD30message, "attack", attackNaturalRoll, attacker) + if (d30Result.modifier) { + attackRollFinal += d30Result.modifier + if (d30Result.modifier > 0) { + await createReactionMessage(attacker, + `${attackerName} gains +${d30Result.modifier} from D30 bonus die for attack.
` + ) + } + } + if (d30Result.specialEffect === "auto") { + attackRollFinal = defenseRoll + 1 // auto-hit + await createReactionMessage(attacker, + `${attackerName} uses ${d30Result.specialName || "Special Strike"} from D30 — attack automatically hits!
` + ) + } + if (d30Result.specialEffect === "flag") { + await createReactionMessage(attacker, + `D30 — ${d30Result.specialName || "Special Effect"} triggered for ${attackerName}!
` + ) + } + if (d30Result.specialEffect === "bleed") { + d30Bleed = true + await createReactionMessage(attacker, + `D30 — Bleeding/Internal Injury on hit! Damage past DR will cause a bleeding wound.
` + ) + } + if (d30Result.specialEffect === "damageMultiplier") { + d30DamageMultiplier = d30Result.multiplier + await createReactionMessage(attacker, + `D30 — x${d30Result.multiplier} damage before damage reduction!
` + ) + } + attackD30Processed = true + } + // ── Attack reaction loop ─────────────────────────────────────────────── if (!defenderHandledBonus && attacker && attackRollFinal <= defenseRoll && isPrimaryController(attacker)) { while (attackRollFinal <= defenseRoll) { @@ -837,7 +925,11 @@ Hooks.on("createChatMessage", async (message) => { defenderTokenId, defenseRoll, outcome, - shieldDamageReduction + shieldDamageReduction, + d30Bleed: d30Bleed ? "true" : "", + d30DamageMultiplier: d30DamageMultiplier, + d30DrMultiplier: d30DrMultiplier, + damageTier: damageTier || "standard" }) } else { console.log("Skipping message creation", { attackerHandledBonus, defenderHandledBonus }) @@ -865,15 +957,19 @@ Hooks.on("createChatMessage", async (message) => { const spell = spellId ? actor.items.get(spellId) : null if (!spell || spell.type !== "spell") return - const cost = Number(spell.system?.cost) || 0 + 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 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 tierLabel = damageTier === "standard" ? "" : ` (${damageTier})` await ChatMessage.create({ - content: `🔮 ${actor.name} casts ${spell.name} — spends ${cost} Aether (${currentAether} → ${newAether}).
`, + content: `🔮 ${actor.name} casts ${spell.name}${tierLabel} — spends ${cost} Aether (${currentAether} → ${newAether}).
`, speaker: ChatMessage.getSpeaker({ actor }) }) }) @@ -966,13 +1062,94 @@ Hooks.on("createChatMessage", async (message) => { const attackerName = message.rolls[0]?.options?.actorName || "Unknown Attacker" const rollType = message.rolls[0]?.options?.rollType + // Lire les effets D30 + const d30Bleed = message.rolls[0]?.options?.d30Bleed || false + const d30DamageMultiplier = message.rolls[0]?.options?.d30DamageMultiplier || 1 + const d30DrMultiplier = message.rolls[0]?.options?.d30DrMultiplier || 1 + + // Appliquer le multiplicateur de dégâts D30 au total AVANT DR + const rawDamage = damageTotal * d30DamageMultiplier + // Calculer les DR — les sorts utilisent une DR manuelle saisie par l'utilisateur const isSpellDamage = rollType === "spell-damage" const manualDR = message.rolls[0]?.options?.manualDR ?? 0 const extraShieldDr = Number(message.rolls[0]?.options?.extraShieldDr) || 0 - const armorDR = isSpellDamage ? manualDR : (defender.computeDamageReduction() || 0) - const totalDR = isSpellDamage ? manualDR : armorDR + extraShieldDr - const finalDamage = Math.max(0, damageTotal - totalDR) + + // Décomposer les DR en composants + let baseDR = 0 + let shieldDR = 0 + let magicDR = 0 + + if (isSpellDamage) { + baseDR = manualDR + } else { + const totalDefDR = defender.computeDamageReduction() || 0 + magicDR = defender.getMagicDR() || 0 + baseDR = totalDefDR - magicDR // naturalDR + armorDR (ou hpDR + combatDR pour les monstres) + shieldDR = extraShieldDr + } + + // Appliquer le multiplicateur de DR D30 si actif — boîte de dialogue + let appliedBaseDR = baseDR + let appliedShieldDR = shieldDR + let appliedMagicDR = magicDR + + if (d30DrMultiplier > 1) { + const drResult = await (async () => { + const checks = { + base: true, + shield: shieldDR > 0, + magic: magicDR > 0 + } + const html = ` +D30 DR Multiplier ×${d30DrMultiplier}
+Choose which DR types to multiply:
+ + + +Bleeding: Wound of ${finalDamage} HP for ${finalDamage} seconds.
` + } + await ChatMessage.create({ - content: messageContent, + content: messageContent + bleedContent, speaker: ChatMessage.getSpeaker({ actor: defender }), whisper: ChatMessage.getWhisperRecipients("GM") }) diff --git a/module/config/d30_results_tables.json b/module/config/d30_results_tables.json index afc93ca..8d04f70 100644 --- a/module/config/d30_results_tables.json +++ b/module/config/d30_results_tables.json @@ -346,7 +346,7 @@ "target": "spell_defense" } ], - "description": "Possible 20/20 Spell defense that Saves Against Any Magical Attack Except a Lethal Magical Strike or add D12 to defense" + "description": "Possible 20/20 Spell defense that Saves Against Any Magical Attack Except a Lethal Magical Strike or add D12 to spell defense" }, "skill_rolls": { "type": "bonus_flat", @@ -386,10 +386,10 @@ "type": "bleed" }, { - "type": "knockback" + "type": "internal_injury" } ], - "description": "Bleed, Knock-back on Hit" + "description": "Bleed, Internal Injury on Hit" }, "ranged_attack": { "type": "bleed", @@ -399,10 +399,9 @@ "type": "counter_attack", "options": [ "kick", - "punch", - "shield_bash" + "punch" ], - "description": "Kick, Punch or Shield Bash" + "description": "Kick or Punch" }, "skill_rolls": { "type": "bonus_flat", @@ -414,10 +413,9 @@ "type": "counter_attack", "options": [ "kick", - "punch", - "shield_bash" + "punch" ], - "description": "Kick, Punch or Shield Bash" + "description": "Kick or Punch" }, "saving_throws": { "type": "bonus_flat", @@ -446,10 +444,10 @@ "type": "bleed" }, { - "type": "knockback" + "type": "internal_injury" } ], - "description": "Bleed, Knock-back on Hit" + "description": "Bleed, Internal Injury on Hit" }, "ranged_attack": { "type": "bleed", @@ -459,10 +457,9 @@ "type": "counter_attack", "options": [ "kick", - "punch", - "shield_bash" + "punch" ], - "description": "Kick, Punch or Shield Bash" + "description": "Kick or Punch" }, "skill_rolls": { "type": "bonus_flat", @@ -474,10 +471,9 @@ "type": "counter_attack", "options": [ "kick", - "punch", - "shield_bash" + "punch" ], - "description": "Kick, Punch or Shield Bash" + "description": "Kick or Punch" }, "saving_throws": { "type": "bonus_flat", @@ -539,10 +535,10 @@ "type": "bleed" }, { - "type": "knockback" + "type": "internal_injury" } ], - "description": "Bleed, Knock-back on Hit" + "description": "Bleed, Internal Injury on Hit" }, "ranged_attack": { "type": "bleed", @@ -552,10 +548,9 @@ "type": "counter_attack", "options": [ "kick", - "punch", - "shield_bash" + "punch" ], - "description": "Kick, Punch, or Shield Bash" + "description": "Kick or Punch" }, "skill_rolls": { "type": "bonus_flat", @@ -567,10 +562,9 @@ "type": "counter_attack", "options": [ "kick", - "punch", - "shield_bash" + "punch" ], - "description": "Kick, Punch, or Shield Bash" + "description": "Kick or Punch" }, "saving_throws": { "type": "bonus_flat", diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 2355b09..77ff631 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -79,16 +79,28 @@ export default class LethalFantasyActor extends Actor { } /* *************************************************/ - computeDamageReduction() { - // Pour les monstres, utiliser hp.damageResistance et combat.damageReduction + getNaturalDR() { if (this.type === "monster") { - let hpDR = Number(this.system.hp?.damageResistance) || 0 + return Number(this.system.hp?.damageResistance) || 0 + } + return Number(this.system.biodata?.naturalDR) || 0 + } + + /* *************************************************/ + getMagicDR() { + if (this.type === "monster") return 0 + return Number(this.system.biodata?.magicDR) || 0 + } + + /* *************************************************/ + computeDamageReduction() { + if (this.type === "monster") { + let hpDR = this.getNaturalDR() let combatDR = Number(this.system.combat?.damageReduction) || 0 return hpDR + combatDR } - // Pour les personnages, utiliser biodata et items - let naturalDR = Number(this.system.biodata?.naturalDR) || 0 - let magicDR = Number(this.system.biodata?.magicDR) || 0 + let naturalDR = this.getNaturalDR() + let magicDR = this.getMagicDR() let armorDR = this.getArmorDR() return naturalDR + magicDR + armorDR } @@ -153,7 +165,7 @@ export default class LethalFantasyActor extends Actor { } /* *************************************************/ - async prepareRoll(rollType, rollKey, rollDice, defenderId, defenderTokenId, extraShieldDr = 0) { + async prepareRoll(rollType, rollKey, rollDice, defenderId, defenderTokenId, extraShieldDr = 0, d30Effects = {}) { console.log("Preparing roll", rollType, rollKey, rollDice, defenderId) let rollTarget switch (rollType) { @@ -198,8 +210,15 @@ export default class LethalFantasyActor extends Actor { case "miracle-power": rollTarget = this.items.find((i) => (i.type === "miracle" || i.type == "spell") && i.id === rollKey) rollTarget.rollKey = rollKey + // Read damage tier from combatant currentAction if available + const activeCombatant = game.combat?.combatants?.find(c => c.actorId === this.id) + const currentAction = activeCombatant?.getFlag(SYSTEM.id, "currentAction") + const damageTier = currentAction?.damageTier || "standard" + rollTarget.damageTier = damageTier if (rollType === "spell-attack" || rollType === "spell-power") { - const cost = Number(rollTarget.system?.cost) || 0 + const tierCostMap = { standard: "cost", overpowered: "costOverpowered", overpowered2: "costOverpowered2" } + const costField = tierCostMap[damageTier] || "cost" + const cost = Number(rollTarget.system?.[costField]) || 0 const currentAether = Number(this.system.aetherPoints?.value) || 0 if (cost > currentAether) { ui.notifications.warn(`${this.name} cannot cast ${rollTarget.name}: insufficient Aether (needs ${cost}, has ${currentAether}).`) @@ -222,8 +241,7 @@ export default class LethalFantasyActor extends Actor { rollTarget.rollKey = rollKey } break; - case "weapon-damage-small": - case "weapon-damage-medium": + case "weapon-damage": case "weapon-attack": case "weapon-defense": { let weapon = this.items.find((i) => i.type === "weapon" && i.id === rollKey) @@ -264,7 +282,7 @@ export default class LethalFantasyActor extends Actor { combat: foundry.utils.duplicate(this.system.combat), isRangedAttack: weapon.system.weaponType === "ranged" } - if (rollType === "weapon-damage-small" || rollType === "weapon-damage-medium") { + if (rollType === "weapon-damage") { rollTarget.grantedDice = this.system.granted.damageDice } if (rollType === "weapon-attack") { @@ -287,7 +305,7 @@ export default class LethalFantasyActor extends Actor { rollTarget.magicUser = this.system.biodata.magicUser rollTarget.actorModifiers = foundry.utils.duplicate(this.system.modifiers) rollTarget.actorLevel = this.system.biodata.level - await this.system.roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr) + await this.system.roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr, d30Effects) } } diff --git a/module/documents/roll.mjs b/module/documents/roll.mjs index 0d84bf4..6280c45 100644 --- a/module/documents/roll.mjs +++ b/module/documents/roll.mjs @@ -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: `${selectedItem.name} has multiple damage tiers.
Choose which damage to use when the attack lands:
D30 result: ${d30Message.description}
+Choose how to use this result:
+D30 bonus: rolled ${cleaned.toUpperCase()} = ${roll.total}
`, + speaker: ChatMessage.getSpeaker({ actor }) + }) + 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 + */ + static _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 + * @param {number|null} naturalRoll The natural D20 roll + * @returns {string} The special effect name + */ + static _buildSpecialName(specialChoice, naturalRoll) { + if (specialChoice.type === "special_strike") { + if (specialChoice.options.includes("lethal")) { + if (naturalRoll === 20) return "Lethal Strike" + if (naturalRoll >= 16 && naturalRoll <= 19) return "Vital Strike" + return "Lethal/Vital Strike" + } + if (specialChoice.options.includes("vicious")) return "Vicious Strike" + return "Special Strike" + } + if (specialChoice.type === "special_defense") { + if (specialChoice.options.includes("perfect_spell")) return "Perfect Spell Defense" + if (specialChoice.options.includes("flawless")) return "Flawless Defense" + if (specialChoice.options.includes("legendary")) return "Legendary Defense" + if (specialChoice.options.includes("perfect")) return "Perfect Defense" + return "Special Defense" + } + return "Special Effect" + } + /* -------------------------------------------- */ static getCombatBonusDiceChoices() { return ["1d4", "1d6", "1d8", "1d10", "1d12", "1d20", "1d20e"] @@ -843,19 +1068,16 @@ export default class LethalFantasyUtils { // Déterminer le type de dégâts à lancer if (data.attackRollType === "weapon-attack") { damageButton = ` -