From 81584ed5d65ff81ac129410476cc19ba7e57ac5d Mon Sep 17 00:00:00 2001 From: LeRatierBretonnier Date: Thu, 16 Apr 2026 23:42:24 +0200 Subject: [PATCH] Update D30 descriptions and add simple shield option for NPCs --- assets/rules/dice_30_effects.xlsx | Bin 0 -> 13254 bytes lethal-fantasy.mjs | 40 ++++++++++++++- module/config/d30_results_tables.json | 32 ++++++------ module/utils.mjs | 69 ++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 assets/rules/dice_30_effects.xlsx diff --git a/assets/rules/dice_30_effects.xlsx b/assets/rules/dice_30_effects.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e0bbba0046423ada0869feca514d2b47b4c0ada0 GIT binary patch literal 13254 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 PZy(aPFb=2x { const shieldData = LethalFantasyUtils.getShieldReactionData(defender) let canRerollDefense = LethalFantasyUtils.hasD30Reroll(defenseD30message) let canShieldReact = !!shieldData + let canAdHocShield = !shieldData while (defenseRoll < attackRoll) { const currentGrit = Number(defender.system?.grit?.current) || 0 @@ -571,6 +572,14 @@ Hooks.on("createChatMessage", async (message) => { icon: "fa-solid fa-shield", callback: () => "shieldReact" }) + } else if (canAdHocShield) { + // No pre-configured shield — offer ad-hoc shield option (useful for monsters) + buttons.push({ + action: "adHocShield", + label: "Roll ad-hoc shield (choose dice + DR)", + icon: "fa-solid fa-shield-halved", + callback: () => "adHocShield" + }) } buttons.push({ @@ -640,7 +649,6 @@ Hooks.on("createChatMessage", async (message) => { canShieldReact = false if (newDefenseTotal >= attackRoll) { - // Shield roll tied or exceeded the attack — shield blocked shieldBlocked = true shieldReaction = { damageReduction: shieldData.damageReduction, @@ -652,7 +660,6 @@ Hooks.on("createChatMessage", async (message) => { `

${defenderName} rolls ${shieldData.label} and adds ${shieldBonus} to defense (${newDefenseTotal} ≥ ${attackRoll}). Shield blocked the attack! Both armor DR and shield DR ${shieldData.damageReduction} will apply to damage.

` ) } else { - // Shield roll not enough — hit still lands, armor DR only shieldReaction = null await createReactionMessage( defender, @@ -660,6 +667,35 @@ Hooks.on("createChatMessage", async (message) => { ) } } + + if (choice === "adHocShield") { + const adHoc = await LethalFantasyUtils.promptAdHocShield(defenderName, attackRoll, defenseRoll) + if (!adHoc) continue + const shieldBonus = await LethalFantasyUtils.rollBonusDie(adHoc.formula, defender) + const newDefenseTotal = defenseRoll + shieldBonus + defenseRoll = newDefenseTotal + canShieldReact = false + canAdHocShield = false + + if (newDefenseTotal >= attackRoll) { + shieldBlocked = true + shieldReaction = { + damageReduction: adHoc.damageReduction, + label: `${adHoc.formula.toUpperCase()} shield`, + bonus: shieldBonus + } + await createReactionMessage( + defender, + `

${defenderName} rolls ${adHoc.formula.toUpperCase()} shield and adds ${shieldBonus} to defense (${newDefenseTotal} ≥ ${attackRoll}). Shield blocked the attack! Both armor DR and shield DR ${adHoc.damageReduction} will apply to damage.

` + ) + } else { + shieldReaction = null + await createReactionMessage( + defender, + `

${defenderName} rolls ${adHoc.formula.toUpperCase()} shield and adds ${shieldBonus} to defense (${newDefenseTotal} < ${attackRoll}). Shield did not block — normal hit, armor DR only.

` + ) + } + } } } diff --git a/module/config/d30_results_tables.json b/module/config/d30_results_tables.json index e94d0fa..632d79f 100644 --- a/module/config/d30_results_tables.json +++ b/module/config/d30_results_tables.json @@ -6,7 +6,7 @@ "melee_defense": "Possible Flawless or Legendary Defense or Add D20E to Defense", "arcane_spell_attack": "Possible Lethal or Vital Magical Strike or Add D20E to Spell Attack", "arcane_spell_defense": "Possible Spell Catastrophe or adds D20E to Spell Defense", - "skill_rolls": "Skill Succeeds Regardless of Opposing Roll / Success at highest level / Matching 30s cancel each other out" + "skill_rolls": "Skill Succeeds Regardless of Opposing Roll" }, "29": { "melee_attack": "Gain 1 Grit", @@ -28,9 +28,9 @@ "melee_attack": "Granted D6 (1-6) Attack Modifier for This Melee Attack", "ranged_attack": "Granted D6 (1-6) Attack Modifier for This Ranged Attack", "melee_defense": "Granted 1 Luck dice for Use in This Combat Only", - "arcane_spell_attack": "No Spell Lethargy (the Aether Approves)", + "arcane_spell_attack": "No Spell Lethargy the Aether Approves of Characters Efforts", "arcane_spell_defense": "Caster Suffers Severe pain and will be under a flash of pain for 1D6E seconds", - "skill_rolls": "Granted D6 (1-6) Skill Modifier for this Skill Attempt" + "skill_rolls": "empty" }, "26": { "melee_attack": "Shield Destruction", @@ -41,9 +41,9 @@ "skill_rolls": "empty" }, "25": { - "melee_attack": "Bleed, Knock-Back on Hit", - "ranged_attack": "Bleed", - "melee_defense": "Kick, Punch or Shield Bash", + "melee_attack": "empty", + "ranged_attack": "empty", + "melee_defense": "empty", "arcane_spell_attack": "empty", "arcane_spell_defense": "empty", "skill_rolls": "Add 1 to Skill Roll" @@ -54,14 +54,14 @@ "melee_defense": "Defender Recovers or ignores any flash of pain", "arcane_spell_attack": "Magical Damage inflicts Flash of pain 1D6E seconds", "arcane_spell_defense": "Caster Suffers Severe pain and will be under a flash of pain for 1D6E seconds", - "skill_rolls": "empty" + "skill_rolls": "Granted D6 (1-6) Skill Modifier for this Skill Attempt" }, "20": { - "melee_attack": "Possible Vicious Strike. Bleed, Knock-back on Hit", - "ranged_attack": "Possible Vicious Strike. Bleeding wound inflicted on hit.", - "melee_defense": "Possible 20/20 defense (avoids Any Attack Except a Lethal Strike). Grants a Kick, Punch or Shield Bash counter", - "arcane_spell_attack": "Possible Vicious Application of a Magical Attack", - "arcane_spell_defense": "Possible 20/20 Spell defense (Saves Against Any Magical Attack Except a Lethal Magical Strike)", + "melee_attack": "Possible Vicious Strike or Add D12 to attack", + "ranged_attack": "Possible Vicious Strike or add D12 to attack", + "melee_defense": "Possible 20/20 defense that avoids Any Attack Except a Lethal Strike or adds D12 to defense", + "arcane_spell_attack": "Possible Vicious Application of a Magical Attack or add D12 to attack", + "arcane_spell_defense": "Possible 20/20 Spell defense that Saves Against Any Magical Attack Except a Lethal Magical Strike or add D12 to defense", "skill_rolls": "20 Added to Skill Roll" }, "15": { @@ -106,7 +106,7 @@ }, "7": { "melee_attack": "Flurry Attack on Hit or Miss", - "ranged_attack": "Roll 2x Double Damage Dice", + "ranged_attack": "Roll 2x Damage Dice", "melee_defense": "empty", "arcane_spell_attack": "empty", "arcane_spell_defense": "empty", @@ -146,7 +146,9 @@ } }, "definitions": { - "flash_of_pain": "Causes the victim to defend with disfavor. They can only walk and cannot attack, cast spells, call miracles or perform skills.", - "shield_destruction_condition": "Occurs only if damage exceeds the shields DR." + "flash_of_pain": "Causes the victim to defend against melee and spell attacks with disfavor. They can only walk and cannot attack, cast spells, call miracles or perform skills.", + "shield_destruction_condition": "Shield destruction occurs only if damage exceeds the shields DR.", + "matching_30s": "Matching 30s on skill rolls cancel each other out and is resolved by the skill roll.", + "skill_roll_30": "A 30 on a skill roll indicates success at highest level of the skill involved." } } diff --git a/module/utils.mjs b/module/utils.mjs index 586485a..0b4476e 100644 --- a/module/utils.mjs +++ b/module/utils.mjs @@ -527,6 +527,75 @@ export default class LethalFantasyUtils { } /* -------------------------------------------- */ + /** + * Prompt the GM or player to choose an ad-hoc shield dice and DR value. + * Used when the defender has no pre-configured shield equipment. + * @param {string} defenderName + * @param {number} attackRoll + * @param {number} defenseRoll + * @returns {Promise<{formula: string, damageReduction: number}|null>} + */ + static async promptAdHocShield(defenderName, attackRoll, defenseRoll) { + const choices = this.getCombatBonusDiceChoices() + const optionsHtml = choices.map(c => ``).join("") + const content = ` +
+
+

${defenderName} uses a shield (not equipped)

+

Attack: ${attackRoll} — Current defense: ${defenseRoll}

+
+
+ + +
+
+ + +
+
+ ` + + const raw = await foundry.applications.api.DialogV2.wait({ + window: { title: "Ad-hoc Shield Roll" }, + classes: ["lethalfantasy"], + content, + buttons: [ + { + action: "roll", + label: "Roll Shield", + icon: "fa-solid fa-shield", + callback: (event, button) => { + const shieldDice = button.form?.elements?.shieldDice ?? button.closest("form")?.elements?.shieldDice + const shieldDR = button.form?.elements?.shieldDR ?? button.closest("form")?.elements?.shieldDR + return { + formula: shieldDice?.value ?? "1d6", + damageReduction: Number(shieldDR?.value) || 0 + } + } + }, + { + action: "cancel", + label: "Cancel", + icon: "fa-solid fa-xmark", + callback: () => null + } + ], + rejectClose: false + }) + + return raw ?? null + } + + /* -------------------------------------------- */ + /** + * Roll a bonus die formula, optionally showing Dice So Nice animation and posting a chat message. + * @param {string} formula + * @param {Actor} actor + * @param {Function} [messageContent] + * @returns {Promise} + */ static async rollBonusDie(formula, actor, messageContent) { const roll = new Roll(formula) await roll.evaluate()