Update D30 descriptions and add simple shield option for NPCs
This commit is contained in:
BIN
assets/rules/dice_30_effects.xlsx
Normal file
BIN
assets/rules/dice_30_effects.xlsx
Normal file
Binary file not shown.
@@ -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) => {
|
||||
`<p><strong>${defenderName}</strong> rolls <strong>${shieldData.label}</strong> and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} ≥ ${attackRoll}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${shieldData.damageReduction}</strong> will apply to damage.</p>`
|
||||
)
|
||||
} 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,
|
||||
`<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} ≥ ${attackRoll}). <strong>Shield blocked the attack!</strong> Both armor DR and shield DR <strong>${adHoc.damageReduction}</strong> will apply to damage.</p>`
|
||||
)
|
||||
} else {
|
||||
shieldReaction = null
|
||||
await createReactionMessage(
|
||||
defender,
|
||||
`<p><strong>${defenderName}</strong> rolls <strong>${adHoc.formula.toUpperCase()}</strong> shield and adds <strong>${shieldBonus}</strong> to defense (${newDefenseTotal} < ${attackRoll}). Shield did not block — normal hit, armor DR only.</p>`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 => `<option value="${c}">${c.toUpperCase()}</option>`).join("")
|
||||
const content = `
|
||||
<div class="grit-luck-dialog">
|
||||
<div class="combat-status">
|
||||
<p><strong>${defenderName}</strong> uses a shield (not equipped)</p>
|
||||
<p>Attack: <strong>${attackRoll}</strong> — Current defense: <strong>${defenseRoll}</strong></p>
|
||||
</div>
|
||||
<div class="weapon-selection" style="margin-top:8px;">
|
||||
<label for="shield-dice">Shield dice:</label>
|
||||
<select id="shield-dice" name="shieldDice" style="width: 100%; margin-top: 4px;">
|
||||
${optionsHtml}
|
||||
</select>
|
||||
</div>
|
||||
<div class="weapon-selection" style="margin-top:8px;">
|
||||
<label for="shield-dr">Shield DR value:</label>
|
||||
<input id="shield-dr" name="shieldDR" type="number" min="0" value="0" style="width: 100%; margin-top: 4px;" />
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
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<number>}
|
||||
*/
|
||||
static async rollBonusDie(formula, actor, messageContent) {
|
||||
const roll = new Roll(formula)
|
||||
await roll.evaluate()
|
||||
|
||||
Reference in New Issue
Block a user