diff --git a/assets/rules/dice_30_effects.xlsx b/assets/rules/dice_30_effects.xlsx new file mode 100644 index 0000000..e0bbba0 Binary files /dev/null and b/assets/rules/dice_30_effects.xlsx differ diff --git a/lethal-fantasy.mjs b/lethal-fantasy.mjs index d8d7466..f284eb2 100644 --- a/lethal-fantasy.mjs +++ b/lethal-fantasy.mjs @@ -524,6 +524,7 @@ Hooks.on("createChatMessage", async (message) => { 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}
+