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)
|
const shieldData = LethalFantasyUtils.getShieldReactionData(defender)
|
||||||
let canRerollDefense = LethalFantasyUtils.hasD30Reroll(defenseD30message)
|
let canRerollDefense = LethalFantasyUtils.hasD30Reroll(defenseD30message)
|
||||||
let canShieldReact = !!shieldData
|
let canShieldReact = !!shieldData
|
||||||
|
let canAdHocShield = !shieldData
|
||||||
|
|
||||||
while (defenseRoll < attackRoll) {
|
while (defenseRoll < attackRoll) {
|
||||||
const currentGrit = Number(defender.system?.grit?.current) || 0
|
const currentGrit = Number(defender.system?.grit?.current) || 0
|
||||||
@@ -571,6 +572,14 @@ Hooks.on("createChatMessage", async (message) => {
|
|||||||
icon: "fa-solid fa-shield",
|
icon: "fa-solid fa-shield",
|
||||||
callback: () => "shieldReact"
|
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({
|
buttons.push({
|
||||||
@@ -640,7 +649,6 @@ Hooks.on("createChatMessage", async (message) => {
|
|||||||
canShieldReact = false
|
canShieldReact = false
|
||||||
|
|
||||||
if (newDefenseTotal >= attackRoll) {
|
if (newDefenseTotal >= attackRoll) {
|
||||||
// Shield roll tied or exceeded the attack — shield blocked
|
|
||||||
shieldBlocked = true
|
shieldBlocked = true
|
||||||
shieldReaction = {
|
shieldReaction = {
|
||||||
damageReduction: shieldData.damageReduction,
|
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>`
|
`<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 {
|
} else {
|
||||||
// Shield roll not enough — hit still lands, armor DR only
|
|
||||||
shieldReaction = null
|
shieldReaction = null
|
||||||
await createReactionMessage(
|
await createReactionMessage(
|
||||||
defender,
|
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",
|
"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_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",
|
"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": {
|
"29": {
|
||||||
"melee_attack": "Gain 1 Grit",
|
"melee_attack": "Gain 1 Grit",
|
||||||
@@ -28,9 +28,9 @@
|
|||||||
"melee_attack": "Granted D6 (1-6) Attack Modifier for This Melee Attack",
|
"melee_attack": "Granted D6 (1-6) Attack Modifier for This Melee Attack",
|
||||||
"ranged_attack": "Granted D6 (1-6) Attack Modifier for This Ranged 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",
|
"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",
|
"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": {
|
"26": {
|
||||||
"melee_attack": "Shield Destruction",
|
"melee_attack": "Shield Destruction",
|
||||||
@@ -41,9 +41,9 @@
|
|||||||
"skill_rolls": "empty"
|
"skill_rolls": "empty"
|
||||||
},
|
},
|
||||||
"25": {
|
"25": {
|
||||||
"melee_attack": "Bleed, Knock-Back on Hit",
|
"melee_attack": "empty",
|
||||||
"ranged_attack": "Bleed",
|
"ranged_attack": "empty",
|
||||||
"melee_defense": "Kick, Punch or Shield Bash",
|
"melee_defense": "empty",
|
||||||
"arcane_spell_attack": "empty",
|
"arcane_spell_attack": "empty",
|
||||||
"arcane_spell_defense": "empty",
|
"arcane_spell_defense": "empty",
|
||||||
"skill_rolls": "Add 1 to Skill Roll"
|
"skill_rolls": "Add 1 to Skill Roll"
|
||||||
@@ -54,14 +54,14 @@
|
|||||||
"melee_defense": "Defender Recovers or ignores any flash of pain",
|
"melee_defense": "Defender Recovers or ignores any flash of pain",
|
||||||
"arcane_spell_attack": "Magical Damage inflicts Flash of pain 1D6E seconds",
|
"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",
|
"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": {
|
"20": {
|
||||||
"melee_attack": "Possible Vicious Strike. Bleed, Knock-back on Hit",
|
"melee_attack": "Possible Vicious Strike or Add D12 to attack",
|
||||||
"ranged_attack": "Possible Vicious Strike. Bleeding wound inflicted on hit.",
|
"ranged_attack": "Possible Vicious Strike or add D12 to attack",
|
||||||
"melee_defense": "Possible 20/20 defense (avoids Any Attack Except a Lethal Strike). Grants a Kick, Punch or Shield Bash counter",
|
"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",
|
"arcane_spell_attack": "Possible Vicious Application of a Magical Attack or add D12 to attack",
|
||||||
"arcane_spell_defense": "Possible 20/20 Spell defense (Saves Against Any Magical Attack Except a Lethal Magical Strike)",
|
"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"
|
"skill_rolls": "20 Added to Skill Roll"
|
||||||
},
|
},
|
||||||
"15": {
|
"15": {
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
},
|
},
|
||||||
"7": {
|
"7": {
|
||||||
"melee_attack": "Flurry Attack on Hit or Miss",
|
"melee_attack": "Flurry Attack on Hit or Miss",
|
||||||
"ranged_attack": "Roll 2x Double Damage Dice",
|
"ranged_attack": "Roll 2x Damage Dice",
|
||||||
"melee_defense": "empty",
|
"melee_defense": "empty",
|
||||||
"arcane_spell_attack": "empty",
|
"arcane_spell_attack": "empty",
|
||||||
"arcane_spell_defense": "empty",
|
"arcane_spell_defense": "empty",
|
||||||
@@ -146,7 +146,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"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.",
|
"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": "Occurs only if damage exceeds the shields DR."
|
"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) {
|
static async rollBonusDie(formula, actor, messageContent) {
|
||||||
const roll = new Roll(formula)
|
const roll = new Roll(formula)
|
||||||
await roll.evaluate()
|
await roll.evaluate()
|
||||||
|
|||||||
Reference in New Issue
Block a user