diff --git a/lethal-fantasy.mjs b/lethal-fantasy.mjs
index b265a1e..84a3cd8 100644
--- a/lethal-fantasy.mjs
+++ b/lethal-fantasy.mjs
@@ -250,6 +250,18 @@ Hooks.on(hookName, (message, html, data) => {
const attackWeaponId = message.rolls[0]?.rollTarget?.weapon?.id || message.rolls[0]?.rollTarget?.weapon?._id
const attackRollType = message.rolls[0]?.type
const attackRollKey = message.rolls[0]?.rollTarget?.rollKey
+ const attackD30result = message.rolls[0]?.options?.D30result || null
+ const attackD30message = message.rolls[0]?.options?.D30message || null
+ const attackRerollContext = {
+ rollType: message.rolls[0]?.options?.rollType,
+ rollTarget: foundry.utils.duplicate(message.rolls[0]?.options?.rollTarget ?? {}),
+ actorId: message.rolls[0]?.options?.actorId,
+ actorName: message.rolls[0]?.options?.actorName,
+ actorImage: message.rolls[0]?.options?.actorImage,
+ defenderId: combatant.actor?.id || null,
+ defenderTokenId: tokenId || combatant.token?.id || null,
+ rollContext: foundry.utils.duplicate(message.rolls[0]?.options?.rollData ?? {})
+ }
// Préparer le message de demande de défense
const defenseMsg = {
@@ -262,6 +274,9 @@ Hooks.on(hookName, (message, html, data) => {
attackWeaponId: attackWeaponId,
attackRollType: attackRollType,
attackRollKey: attackRollKey,
+ attackD30result: attackD30result,
+ attackD30message: attackD30message,
+ attackRerollContext: attackRerollContext,
combatantId: combatantId,
tokenId: tokenId
}
@@ -318,6 +333,7 @@ Hooks.on(hookName, (message, html, data) => {
let attackerId = button.data("attacker-id")
const defenderId = button.data("defender-id")
const defenderTokenId = button.data("defender-token-id") || null
+ const extraShieldDr = Number(button.data("extra-shield-dr") || 0)
const damageType = button.data("damage-type")
const damageFormula = button.data("damage-formula")
const damageModifier = button.data("damage-modifier")
@@ -332,7 +348,7 @@ Hooks.on(hookName, (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)
+ await actor.system.prepareMonsterRoll("monster-damage", attackKey, undefined, undefined, undefined, defenderId, defenderTokenId, extraShieldDr)
return
}
@@ -351,7 +367,7 @@ Hooks.on(hookName, (message, html, data) => {
// 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)
+ await actor.prepareRoll(rollType, weaponId, undefined, defenderId, defenderTokenId, extraShieldDr)
})
// Masquer les boutons de dommages dans les messages de résultat de combat si l'utilisateur n'est pas l'attaquant
@@ -410,8 +426,21 @@ Hooks.on("createChatMessage", async (message) => {
return
}
- const { attackerId, attackRoll, attackerName, defenderName, attackWeaponId, attackRollType, attackRollKey, defenderId, defenderTokenId } = attackData
+ const {
+ attackerId,
+ attackRoll,
+ attackerName,
+ defenderName,
+ attackWeaponId,
+ attackRollType,
+ attackRollKey,
+ attackD30message,
+ attackRerollContext,
+ defenderId,
+ defenderTokenId
+ } = attackData
let defenseRoll = message.rolls[0]?.options?.rollTotal || message.rolls[0]?.total || 0
+ const defenseD30message = message.rolls[0]?.options?.D30message || null
console.log("Processing defense:", { attackRoll, defenseRoll, attackerId, defenderId })
@@ -423,26 +452,165 @@ Hooks.on("createChatMessage", async (message) => {
// Récupérer le défenseur et l'attaquant
const defender = game.actors.get(defenderId)
const attacker = game.actors.get(attackerId)
+ const defenseRerollContext = {
+ rollType: message.rolls[0]?.options?.rollType,
+ rollTarget: foundry.utils.duplicate(message.rolls[0]?.options?.rollTarget ?? {}),
+ actorId: message.rolls[0]?.options?.actorId,
+ actorName: message.rolls[0]?.options?.actorName,
+ actorImage: message.rolls[0]?.options?.actorImage,
+ defenderId,
+ defenderTokenId,
+ rollContext: foundry.utils.duplicate(message.rolls[0]?.options?.rollData ?? {})
+ }
+
+ const isPrimaryController = actor => {
+ if (!actor) return false
+ const activePlayerOwners = game.users.filter(u => u.active && !u.isGM && actor.testUserPermission(u, "OWNER"))
+ if (activePlayerOwners.length > 0) {
+ return activePlayerOwners[0].id === game.user.id
+ }
+ return game.user.isGM
+ }
+
+ const createReactionMessage = async (actorDocument, content) => {
+ await ChatMessage.create({
+ content,
+ speaker: ChatMessage.getSpeaker({ actor: actorDocument })
+ })
+ }
// Si le défenseur est un personnage qui perd, proposer Grit/Luck (seulement s'il a des points)
// Seulement si l'utilisateur actuel est le propriétaire du défenseur
let defenderHandledBonus = false
- if (defender?.type === "character" && defenseRoll < attackRoll && !game.user.isGM && defender.isOwner) {
- const hasGritOrLuck = (defender.system.grit.current > 0) || (defender.system.luck.current > 0)
+ let shieldReaction = null
+ if (defender && defenseRoll < attackRoll && isPrimaryController(defender)) {
+ const shieldData = LethalFantasyUtils.getShieldReactionData(defender)
+ let canRerollDefense = LethalFantasyUtils.hasD30Reroll(defenseD30message)
+ let canShieldReact = !!shieldData
- if (hasGritOrLuck) {
- const bonusRoll = await LethalFantasyUtils.offerGritLuckBonus(
- defender,
- attackRoll,
- defenseRoll,
- attackerName,
- defenderName
- )
- if (bonusRoll > 0) {
+ while (defenseRoll < attackRoll) {
+ const currentGrit = Number(defender.system?.grit?.current) || 0
+ const currentLuck = Number(defender.system?.luck?.current) || 0
+ const buttons = []
+
+ if (currentGrit > 0) {
+ buttons.push({
+ action: "grit",
+ label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
+ icon: "fa-solid fa-fist-raised",
+ callback: () => "grit"
+ })
+ }
+
+ if (currentLuck > 0) {
+ buttons.push({
+ action: "luck",
+ label: `Spend 1 Luck (+1D6) [${currentLuck} left]`,
+ icon: "fa-solid fa-clover",
+ callback: () => "luck"
+ })
+ }
+
+ buttons.push({
+ action: "bonusDie",
+ label: "Add bonus die",
+ icon: "fa-solid fa-dice",
+ callback: () => "bonusDie"
+ })
+
+ if (canRerollDefense) {
+ buttons.push({
+ action: "rerollDefense",
+ label: "Re-roll defense",
+ icon: "fa-solid fa-rotate-right",
+ callback: () => "rerollDefense"
+ })
+ }
+
+ if (canShieldReact) {
+ buttons.push({
+ action: "shieldReact",
+ label: `Roll shield (${shieldData.label})`,
+ icon: "fa-solid fa-shield",
+ callback: () => "shieldReact"
+ })
+ }
+
+ buttons.push({
+ action: "continue",
+ label: "Continue (no defense bonus)",
+ icon: "fa-solid fa-forward",
+ callback: () => "continue"
+ })
+
+ const choice = await foundry.applications.api.DialogV2.wait({
+ window: { title: "Defense reactions" },
+ classes: ["lethalfantasy"],
+ content: `
+
+
+
${attackerName} rolled ${attackRoll}
+
${defenderName} currently has ${defenseRoll}
+ ${defenseD30message ? `
D30 special: ${defenseD30message}
` : ""}
+
+
Choose how to improve the defense before resolving the hit.
+
+ `,
+ buttons,
+ rejectClose: false
+ })
+
+ if (!choice || choice === "continue") break
+
+ defenderHandledBonus = true
+
+ if (choice === "grit") {
+ const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender, (total) => `${defenderName} spends 1 Grit and rolls ${total} for defense.
`)
defenseRoll += bonusRoll
+ await defender.update({ "system.grit.current": currentGrit - 1 })
+ continue
+ }
+
+ if (choice === "luck") {
+ const bonusRoll = await LethalFantasyUtils.rollBonusDie("1d6", defender, (total) => `${defenderName} spends 1 Luck and rolls ${total} for defense.
`)
+ defenseRoll += bonusRoll
+ await defender.update({ "system.luck.current": currentLuck - 1 })
+ continue
+ }
+
+ if (choice === "bonusDie") {
+ const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(defenderName, "attack", defenseRoll, attackRoll)
+ if (!bonusDie) continue
+ const bonusRoll = await LethalFantasyUtils.rollBonusDie(bonusDie, defender, (total, formula) => `${defenderName} adds ${formula.toUpperCase()} and rolls ${total} for defense.
`)
+ defenseRoll += bonusRoll
+ continue
+ }
+
+ if (choice === "rerollDefense" && canRerollDefense) {
+ const oldDefenseRoll = defenseRoll
+ const reroll = await LethalFantasyUtils.rerollConfiguredRoll(defenseRerollContext)
+ canRerollDefense = false
+ if (!reroll) continue
+ defenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll
+ await createReactionMessage(defender, `${defenderName} uses Mulligan and re-rolls defense: ${oldDefenseRoll} → ${defenseRoll} .
`)
+ continue
+ }
+
+ if (choice === "shieldReact" && canShieldReact) {
+ const shieldBonus = await LethalFantasyUtils.rollBonusDie(shieldData.formula, defender)
+ defenseRoll += shieldBonus
+ shieldReaction = {
+ damageReduction: shieldData.damageReduction,
+ label: shieldData.label,
+ bonus: shieldBonus
+ }
+ canShieldReact = false
+ await createReactionMessage(
+ defender,
+ `${defenderName} rolls ${shieldData.label} and adds ${shieldBonus} to defense.${defenseRoll < attackRoll ? ` The hit still lands, but shield DR ${shieldData.damageReduction} will reduce the damage.` : ""}
`
+ )
}
}
- defenderHandledBonus = true
}
let attackRollFinal = attackRoll
@@ -450,31 +618,98 @@ Hooks.on("createChatMessage", async (message) => {
// Si l'attaquant est un personnage qui perd et a du Grit
// Seulement si l'utilisateur actuel est le propriétaire de l'attaquant (pas le MJ)
- if (attacker?.type === "character" && attackRollFinal <= defenseRoll && attacker.system.grit.current > 0) {
- // Vérifier si l'utilisateur est un propriétaire non-GM de l'attaquant
- const isAttackerOwner = !game.user.isGM && attacker.testUserPermission(game.user, "OWNER")
+ if (!defenderHandledBonus && attacker && attackRollFinal <= defenseRoll && isPrimaryController(attacker)) {
+ let canRerollAttack = LethalFantasyUtils.hasD30Reroll(attackD30message)
- if (isAttackerOwner) {
- console.log("Offering Grit to attacker")
+ while (attackRollFinal <= defenseRoll) {
+ const currentGrit = Number(attacker.system?.grit?.current) || 0
+ const buttons = []
- const attackBonus = await LethalFantasyUtils.offerAttackerGritBonus(
- attacker,
- attackRollFinal,
- defenseRoll,
- attackerName,
- defenderName
- )
+ if (currentGrit > 0) {
+ buttons.push({
+ action: "grit",
+ label: `Spend 1 Grit (+1D6) [${currentGrit} left]`,
+ icon: "fa-solid fa-fist-raised",
+ callback: () => "grit"
+ })
+ }
+
+ buttons.push({
+ action: "bonusDie",
+ label: "Add bonus die",
+ icon: "fa-solid fa-dice",
+ callback: () => "bonusDie"
+ })
+
+ if (canRerollAttack && attackRerollContext) {
+ buttons.push({
+ action: "rerollAttack",
+ label: "Re-roll attack",
+ icon: "fa-solid fa-rotate-right",
+ callback: () => "rerollAttack"
+ })
+ }
+
+ buttons.push({
+ action: "continue",
+ label: "Continue (no attack bonus)",
+ icon: "fa-solid fa-forward",
+ callback: () => "continue"
+ })
+
+ const choice = await foundry.applications.api.DialogV2.wait({
+ window: { title: "Attack reactions" },
+ classes: ["lethalfantasy"],
+ content: `
+
+
+
${attackerName} currently has ${attackRollFinal}
+
${defenderName} rolled ${defenseRoll}
+ ${attackD30message ? `
D30 special: ${attackD30message}
` : ""}
+
+
Choose how to improve the attack before resolving the combat result.
+
+ `,
+ buttons,
+ rejectClose: false
+ })
+
+ if (!choice || choice === "continue") break
- attackRollFinal += attackBonus
attackerHandledBonus = true
- } else {
- console.log("Not attacker owner or is GM, skipping Grit offer")
+
+ if (choice === "grit") {
+ const attackBonus = await LethalFantasyUtils.rollBonusDie("1d6", attacker, (total) => `${attackerName} spends 1 Grit and rolls ${total} for attack.
`)
+ attackRollFinal += attackBonus
+ await attacker.update({ "system.grit.current": currentGrit - 1 })
+ continue
+ }
+
+ if (choice === "bonusDie") {
+ const bonusDie = await LethalFantasyUtils.promptCombatBonusDie(attackerName, "defense", attackRollFinal, defenseRoll)
+ if (!bonusDie) continue
+ const attackBonus = await LethalFantasyUtils.rollBonusDie(bonusDie, attacker, (total, formula) => `${attackerName} adds ${formula.toUpperCase()} and rolls ${total} for attack.
`)
+ attackRollFinal += attackBonus
+ continue
+ }
+
+ if (choice === "rerollAttack" && canRerollAttack && attackRerollContext) {
+ const oldAttackRoll = attackRollFinal
+ const reroll = await LethalFantasyUtils.rerollConfiguredRoll(attackRerollContext)
+ canRerollAttack = false
+ if (!reroll) continue
+ attackRollFinal = reroll.options?.rollTotal || reroll.total || oldAttackRoll
+ await createReactionMessage(attacker, `${attackerName} uses Mulligan and re-rolls attack: ${oldAttackRoll} → ${attackRollFinal} .
`)
+ }
}
}
+ const shieldDamageReduction = shieldReaction && attackRollFinal > defenseRoll ? shieldReaction.damageReduction : 0
+ const outcome = shieldDamageReduction > 0 ? "shielded-hit" : (attackRollFinal > defenseRoll ? "hit" : "miss")
+
// Créer le message de comparaison - uniquement par le client qui a géré le dernier bonus
// Priorité: attaquant si il a géré le bonus, sinon défenseur si il a géré le bonus, sinon défenseur
- const shouldCreateMessage = attackerHandledBonus || (!attackerHandledBonus && defenderHandledBonus) || (!attackerHandledBonus && !defenderHandledBonus && defender.isOwner)
+ const shouldCreateMessage = attackerHandledBonus || (!attackerHandledBonus && defenderHandledBonus) || (!attackerHandledBonus && !defenderHandledBonus && isPrimaryController(defender))
if (shouldCreateMessage) {
console.log("Creating comparison message", { attackerHandledBonus, defenderHandledBonus, isDefenderOwner: defender.isOwner })
@@ -489,7 +724,9 @@ Hooks.on("createChatMessage", async (message) => {
defenderName,
defenderId,
defenderTokenId,
- defenseRoll
+ defenseRoll,
+ outcome,
+ shieldDamageReduction
})
} else {
console.log("Skipping message creation", { attackerHandledBonus, defenderHandledBonus })
@@ -552,7 +789,9 @@ Hooks.on("createChatMessage", async (message) => {
// Calculer les DR
const armorDR = defender.computeDamageReduction() || 0
- const finalDamage = Math.max(0, damageTotal - armorDR)
+ const extraShieldDr = Number(message.rolls[0]?.options?.extraShieldDr) || 0
+ const totalDR = armorDR + extraShieldDr
+ const finalDamage = Math.max(0, damageTotal - totalDR)
// Prefer the token ID stored in roll options (set at attack time when the exact token is known).
// For unlinked tokens (default for monsters), this ensures we target the right instance even
@@ -584,7 +823,7 @@ Hooks.on("createChatMessage", async (message) => {
{
targetName: defender.name,
damage: finalDamage,
- drText: armorDR > 0 ? `Armor DR: ${armorDR}` : "",
+ drText: totalDR > 0 ? `Armor DR: ${armorDR}${extraShieldDr > 0 ? ` + Shield DR: ${extraShieldDr}` : ""}` : "",
weaponName: weaponName,
attackerName: attackerName,
rawDamage: damageTotal
@@ -624,4 +863,4 @@ async function registerWorldCount(registerKey) {
console.log("No usage log ")
}
}
-}
\ No newline at end of file
+}
diff --git a/module/applications/combat.mjs b/module/applications/combat.mjs
index 06285a6..cb7ec5e 100644
--- a/module/applications/combat.mjs
+++ b/module/applications/combat.mjs
@@ -100,6 +100,14 @@ export class LethalFantasyCombat extends Combat {
return this.turns = turns;
}
+ async startCombat() {
+ this._playCombatSound("startEncounter")
+ const updateData = { round: 0, turn: 0 }
+ Hooks.callAll("combatStart", this, updateData)
+ await this.update(updateData)
+ return this
+ }
+
async rollInitiative(ids, options) {
console.log("%%%%%%%%% Roll Initiative", ids, options);
@@ -110,13 +118,12 @@ export class LethalFantasyCombat extends Combat {
let updates = [];
for (let cId of ids) {
const c = this.combatants.get(cId);
- let user = game.users.find(u => u.active && u.character && u.character.id === c.actor.id);
- if (user?.hasPlayerOwner) {
+ const playerOwner = game.users.find(u => u.active && !u.isGM && u.character?.id === c.actor.id);
+ if (game.user.isGM && playerOwner) {
console.log("Rolling initiative for", c.actor.name);
- game.socket.emit(`system.${SYSTEM.id}`, { type: "rollInitiative", actorId: c.actor.id, combatId: this.id, combatantId: c.id });
+ game.socket.emit(`system.${SYSTEM.id}`, { type: "rollInitiative", userId: playerOwner.id, actorId: c.actor.id, combatId: this.id, combatantId: c.id });
} else {
- user = game.users.find(u => u.active && u.isGM);
- c.actor.system.rollInitiative(this.id, c.id);
+ await c.actor.system.rollInitiative(this.id, c.id);
}
}
@@ -196,12 +203,11 @@ export class LethalFantasyCombat extends Combat {
for (let c of this.combatants) {
if (nextRound >= c.initiative) {
- let user = game.users.find(u => u.active && u.character && u.character.id === c.actor.id);
- if (user?.hasPlayerOwner) {
- game.socket.emit(`system.${SYSTEM.id}`, { type: "rollProgressionDice", progressionCount: c.system.progressionCount + 1, actorId: c.actor.id, combatId: this.id, combatantId: c.id });
+ const playerOwner = game.users.find(u => u.active && !u.isGM && u.character?.id === c.actor.id);
+ if (game.user.isGM && playerOwner) {
+ game.socket.emit(`system.${SYSTEM.id}`, { type: "rollProgressionDice", userId: playerOwner.id, progressionCount: c.system.progressionCount + 1, actorId: c.actor.id, combatId: this.id, combatantId: c.id });
} else {
- user = game.users.find(u => u.active && u.isGM);
- c.actor.system.rollProgressionDice(this.id, c.id);
+ await c.actor.system.rollProgressionDice(this.id, c.id);
}
}
}
diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs
index 977aec8..a62fd8f 100644
--- a/module/documents/actor.mjs
+++ b/module/documents/actor.mjs
@@ -153,7 +153,7 @@ export default class LethalFantasyActor extends Actor {
}
/* *************************************************/
- async prepareRoll(rollType, rollKey, rollDice, defenderId, defenderTokenId) {
+ async prepareRoll(rollType, rollKey, rollDice, defenderId, defenderTokenId, extraShieldDr = 0) {
console.log("Preparing roll", rollType, rollKey, rollDice, defenderId)
let rollTarget
switch (rollType) {
@@ -268,7 +268,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)
+ await this.system.roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr)
}
}
diff --git a/module/documents/roll.mjs b/module/documents/roll.mjs
index f708ac7..d2b9e59 100644
--- a/module/documents/roll.mjs
+++ b/module/documents/roll.mjs
@@ -346,58 +346,73 @@ export default class LethalFantasyRoll extends Roll {
favor: "none",
targetName
}
- const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/roll-dialog.hbs", dialogContext)
+ let rollContext
+ if (options.rollContext) {
+ rollContext = foundry.utils.duplicate(options.rollContext)
+ hasGrantedDice = !!rollContext.hasGrantedDice
+ pointBlank = !!rollContext.pointBlank
+ beyondSkill = !!rollContext.beyondSkill
+ letItFly = !!rollContext.letItFly
+ saveSpell = !!rollContext.saveSpell
+ rollContext.visibility ||= rollContext.rollMode || game.settings.get("core", "rollMode")
+ rollContext.modifier ||= modifier
+ rollContext.favor ||= "none"
+ rollContext.changeDice ||= `${dice}`
+ rollContext.attackerAim ||= "0"
+ } else {
+ const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/roll-dialog.hbs", dialogContext)
- let position = game.user.getFlag(SYSTEM.id, "roll-dialog-pos") || { top: -1, left: -1 }
- const label = game.i18n.localize("LETHALFANTASY.Roll.roll")
- const rollContext = await foundry.applications.api.DialogV2.wait({
- window: { title: "Roll dialog" },
- classes: ["lethalfantasy"],
- content,
- position,
- buttons: [
- {
- label: label,
- callback: (event, button, dialog) => {
- console.log("Roll context", event, button, dialog)
- let position = dialog.position
- game.user.setFlag(SYSTEM.id, "roll-dialog-pos", foundry.utils.duplicate(position))
- const output = Array.from(button.form.elements).reduce((obj, input) => {
- if (input.name) obj[input.name] = input.value
- return obj
- }, {})
- return output
+ let position = game.user.getFlag(SYSTEM.id, "roll-dialog-pos") || { top: -1, left: -1 }
+ const label = game.i18n.localize("LETHALFANTASY.Roll.roll")
+ rollContext = await foundry.applications.api.DialogV2.wait({
+ window: { title: "Roll dialog" },
+ classes: ["lethalfantasy"],
+ content,
+ position,
+ buttons: [
+ {
+ label: label,
+ callback: (event, button, dialog) => {
+ console.log("Roll context", event, button, dialog)
+ let position = dialog.position
+ game.user.setFlag(SYSTEM.id, "roll-dialog-pos", foundry.utils.duplicate(position))
+ const output = Array.from(button.form.elements).reduce((obj, input) => {
+ if (input.name) obj[input.name] = input.value
+ return obj
+ }, {})
+ return output
+ },
},
- },
- ],
- actions: {
- "selectGranted": (event, button, dialog) => {
- hasGrantedDice = event.target.checked
- },
- "selectBeyondSkill": (event, button, dialog) => {
- beyondSkill = button.checked
- },
- "selectPointBlank": (event, button, dialog) => {
- pointBlank = button.checked
- },
- "selectLetItFly": (event, button, dialog) => {
- letItFly = button.checked
- },
- "saveSpellCheck": (event, button, dialog) => {
- saveSpell = button.checked
- },
- "gotoToken": (event, button, dialog) => {
- let tokenId = $(button).data("tokenId")
- let token = canvas.tokens?.get(tokenId)
- if (token) {
- canvas.animatePan({ x: token.x, y: token.y, duration: 200 })
- canvas.tokens.releaseAll();
- token.control({ releaseOthers: true });
+ ],
+ actions: {
+ "selectGranted": (event, button, dialog) => {
+ hasGrantedDice = event.target.checked
+ },
+ "selectBeyondSkill": (event, button, dialog) => {
+ beyondSkill = button.checked
+ },
+ "selectPointBlank": (event, button, dialog) => {
+ pointBlank = button.checked
+ },
+ "selectLetItFly": (event, button, dialog) => {
+ letItFly = button.checked
+ },
+ "saveSpellCheck": (event, button, dialog) => {
+ saveSpell = button.checked
+ },
+ "gotoToken": (event, button, dialog) => {
+ let tokenId = $(button).data("tokenId")
+ let token = canvas.tokens?.get(tokenId)
+ if (token) {
+ canvas.animatePan({ x: token.x, y: token.y, duration: 200 })
+ canvas.tokens.releaseAll()
+ token.control({ releaseOthers: true })
+ }
}
- }
- },
- rejectClose: false // Click on Close button will not launch an error
- })
+ },
+ rejectClose: false // Click on Close button will not launch an error
+ })
+ }
// If the user cancels the dialog, exit
if (rollContext === null) return
@@ -547,6 +562,10 @@ export default class LethalFantasyRoll extends Roll {
rollFavor = null
}
+ if (options.forceNoD30) {
+ hasD30 = false
+ }
+
if (hasD30) {
let rollD30 = await new Roll("1D30").evaluate()
if (game?.dice3d) {
@@ -621,6 +640,7 @@ export default class LethalFantasyRoll extends Roll {
rollBase.options.rollData = foundry.utils.duplicate(rollData)
rollBase.options.defenderId = options.defenderId
rollBase.options.defenderTokenId = options.defenderTokenId
+ rollBase.options.extraShieldDr = options.extraShieldDr || 0
/**
* A hook event that fires after the roll has been made.
@@ -1296,7 +1316,7 @@ export default class LethalFantasyRoll extends Roll {
* @returns {Promise} - A promise that resolves when the message is created.
*/
async toMessage(messageData = {}, { rollMode, create = true } = {}) {
- super.toMessage(
+ return await super.toMessage(
{
isSave: this.isSave,
isChallenge: this.isChallenge,
@@ -1313,7 +1333,7 @@ export default class LethalFantasyRoll extends Roll {
rollData: this.rollData,
...messageData,
},
- { rollMode: rollMode },
+ { rollMode, create },
)
}
diff --git a/module/models/character.mjs b/module/models/character.mjs
index f8dde99..86ac573 100644
--- a/module/models/character.mjs
+++ b/module/models/character.mjs
@@ -274,7 +274,7 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod
* @param {"="|"+"|"++"|"-"|"--"} rollAdvantage If there is an avantage (+), a disadvantage (-), a double advantage (++), a double disadvantage (--) or a normal roll (=).
* @returns {Promise} - A promise that resolves to null if the roll is cancelled.
*/
- async roll(rollType, rollTarget, defenderId, defenderTokenId) {
+ async roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr = 0) {
const hasTarget = false
let roll = await LethalFantasyRoll.prompt({
rollType,
@@ -285,7 +285,8 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod
hasTarget,
target: false,
defenderId,
- defenderTokenId
+ defenderTokenId,
+ extraShieldDr
})
if (!roll) return null
diff --git a/module/models/monster.mjs b/module/models/monster.mjs
index df80535..a478874 100644
--- a/module/models/monster.mjs
+++ b/module/models/monster.mjs
@@ -141,7 +141,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
* @param {"="|"+"|"++"|"-"|"--"} rollAdvantage If there is an avantage (+), a disadvantage (-), a double advantage (++), a double disadvantage (--) or a normal roll (=).
* @returns {Promise} - A promise that resolves to null if the roll is cancelled.
*/
- async roll(rollType, rollTarget, defenderId = undefined, defenderTokenId = undefined) {
+ async roll(rollType, rollTarget, defenderId = undefined, defenderTokenId = undefined, extraShieldDr = 0) {
const hasTarget = false
let roll = await LethalFantasyRoll.prompt({
rollType,
@@ -152,14 +152,15 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
hasTarget,
target: false,
defenderId,
- defenderTokenId
+ defenderTokenId,
+ extraShieldDr
})
if (!roll) return null
await roll.toMessage({}, { rollMode: roll.options.rollMode })
}
- async prepareMonsterRoll(rollType, rollKey, rollDice = undefined, tokenId = undefined, damageModifier = undefined, defenderId = undefined, defenderTokenId = undefined) {
+ async prepareMonsterRoll(rollType, rollKey, rollDice = undefined, tokenId = undefined, damageModifier = undefined, defenderId = undefined, defenderTokenId = undefined, extraShieldDr = 0) {
let rollTarget
switch (rollType) {
case "monster-attack":
@@ -255,7 +256,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
if (rollTarget) {
rollTarget.tokenId = tokenId
console.log(rollTarget)
- await this.roll(rollType, rollTarget, defenderId, defenderTokenId)
+ await this.roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr)
}
}
diff --git a/module/utils.mjs b/module/utils.mjs
index 0b2f9ce..6c4a381 100644
--- a/module/utils.mjs
+++ b/module/utils.mjs
@@ -124,10 +124,12 @@ export default class LethalFantasyUtils {
}
break
case "rollInitiative":
+ if (msg.userId && msg.userId !== game.user.id) break
actor = game.actors.get(msg.actorId)
actor.system.rollInitiative(msg.combatId, msg.combatantId)
break
case "rollProgressionDice":
+ if (msg.userId && msg.userId !== game.user.id) break
actor = game.actors.get(msg.actorId)
actor.system.rollProgressionDice(msg.combatId, msg.combatantId, msg.rollProgressionCount)
break
@@ -190,6 +192,9 @@ export default class LethalFantasyUtils {
const attackWeaponId = msg.attackWeaponId
const attackRollType = msg.attackRollType
const attackRollKey = msg.attackRollKey
+ const attackD30result = msg.attackD30result
+ const attackD30message = msg.attackD30message
+ const attackRerollContext = msg.attackRerollContext
const combatantId = msg.combatantId
const tokenId = msg.tokenId
@@ -289,6 +294,9 @@ export default class LethalFantasyUtils {
attackWeaponId,
attackRollType,
attackRollKey,
+ attackD30result,
+ attackD30message,
+ attackRerollContext,
defenderId: defender.id,
defenderTokenId
}
@@ -358,6 +366,9 @@ export default class LethalFantasyUtils {
attackWeaponId,
attackRollType,
attackRollKey,
+ attackD30result,
+ attackD30message,
+ attackRerollContext,
defenderId: defender.id,
defenderTokenId
}
@@ -368,6 +379,114 @@ export default class LethalFantasyUtils {
}
}
+ /* -------------------------------------------- */
+ static hasD30Reroll(d30Message) {
+ return /mulligan|re-?roll/i.test(d30Message || "")
+ }
+
+ /* -------------------------------------------- */
+ static getCombatBonusDiceChoices() {
+ return ["1d4", "1d6", "1d8", "1d10", "1d12", "1d20", "1d20e"]
+ }
+
+ /* -------------------------------------------- */
+ static getShieldReactionData(actor) {
+ if (!actor) return null
+ if (actor.type === "monster") {
+ const formula = actor.system.combat?.shieldDefenseDice
+ const damageReduction = actor.getShieldDR()
+ if (!formula || damageReduction <= 0) return null
+ return {
+ label: game.i18n.localize("LETHALFANTASY.Label.shieldDefenseDice"),
+ formula,
+ damageReduction
+ }
+ }
+
+ const equippedShields = actor.items.filter(item => item.type === "shield" && item.system.equipped)
+ if (equippedShields.length === 0) return null
+
+ const shield = equippedShields[0]
+ return {
+ label: shield.name,
+ formula: shield.system.defense,
+ damageReduction: actor.getShieldDR(),
+ shieldId: shield.id
+ }
+ }
+
+ /* -------------------------------------------- */
+ static async promptCombatBonusDie(actorName, sideLabel, currentRoll, opposingRoll) {
+ const choices = this.getCombatBonusDiceChoices()
+ const optionsHtml = choices.map(choice => `${choice.toUpperCase()} `).join("")
+ const content = `
+
+
+
${actorName} currently has ${currentRoll}
+
Opposing ${sideLabel} roll: ${opposingRoll}
+
+
+ Choose a bonus die:
+
+ ${optionsHtml}
+
+
+
+ `
+
+ return await foundry.applications.api.DialogV2.wait({
+ window: { title: "Add Bonus Die" },
+ classes: ["lethalfantasy"],
+ content,
+ buttons: [
+ {
+ label: "Roll Bonus Die",
+ icon: "fa-solid fa-dice",
+ callback: (event, button, dialog) => button.form.elements.bonusDie.value
+ },
+ {
+ label: "Cancel",
+ icon: "fa-solid fa-xmark",
+ callback: () => null
+ }
+ ],
+ rejectClose: false
+ })
+ }
+
+ /* -------------------------------------------- */
+ static async rollBonusDie(formula, actor, messageContent) {
+ const roll = new Roll(formula)
+ await roll.evaluate()
+ if (game?.dice3d) {
+ await game.dice3d.showForRoll(roll, game.user, true)
+ }
+ if (messageContent) {
+ await ChatMessage.create({
+ content: messageContent(roll.total, formula),
+ speaker: ChatMessage.getSpeaker({ actor })
+ })
+ }
+ return roll.total
+ }
+
+ /* -------------------------------------------- */
+ static async rerollConfiguredRoll(rerollContext = {}) {
+ const RollClass = CONFIG.Dice.rolls.find(r => r.name === "LethalFantasyRoll")
+ if (typeof RollClass?.prompt !== "function") {
+ ui.notifications.error("Lethal Fantasy roll class not available for reroll")
+ return null
+ }
+
+ return await RollClass.prompt({
+ ...foundry.utils.duplicate(rerollContext),
+ rollContext: foundry.utils.duplicate(rerollContext.rollContext || {}),
+ forceNoD30: true,
+ hasTarget: false,
+ target: false
+ })
+ }
+
/* -------------------------------------------- */
static async offerGritLuckBonus(defender, attackRoll, currentDefenseRoll, attackerName, defenderName) {
let totalBonus = 0
@@ -538,7 +657,8 @@ export default class LethalFantasyUtils {
/* -------------------------------------------- */
static async compareAttackDefense(data) {
console.log("compareAttackDefense called with:", data)
- const isAttackWin = data.attackRoll > data.defenseRoll
+ const outcome = data.outcome || (data.attackRoll > data.defenseRoll ? "hit" : "miss")
+ const isAttackWin = outcome !== "miss"
console.log("isAttackWin:", isAttackWin, "attackRoll:", data.attackRoll, "defenseRoll:", data.defenseRoll)
let damageButton = ""
@@ -548,10 +668,10 @@ export default class LethalFantasyUtils {
if (data.attackRollType === "weapon-attack") {
damageButton = `
-
+
Damage (Small)
-
+
Damage (Medium)
@@ -559,7 +679,7 @@ export default class LethalFantasyUtils {
} else if (data.attackRollType === "monster-attack") {
damageButton = `
-
+
Damage
@@ -588,9 +708,11 @@ export default class LethalFantasyUtils {
- ${isAttackWin ?
- ` ${data.attackerName} hits ${data.defenderName} !` :
- ` ${data.defenderName} parries the attack!`
+ ${outcome === "shielded-hit"
+ ? ` ${data.attackerName} still hits ${data.defenderName} , but shield DR ${data.shieldDamageReduction || 0} reduces the damage.`
+ : isAttackWin
+ ? ` ${data.attackerName} hits ${data.defenderName} !`
+ : ` ${data.defenderName} parries the attack!`
}
${damageButton}
diff --git a/packs-system/lf-equipment/000545.log b/packs-system/lf-equipment/000553.log
similarity index 100%
rename from packs-system/lf-equipment/000545.log
rename to packs-system/lf-equipment/000553.log
diff --git a/packs-system/lf-equipment/CURRENT b/packs-system/lf-equipment/CURRENT
index 19d4421..536f6fc 100644
--- a/packs-system/lf-equipment/CURRENT
+++ b/packs-system/lf-equipment/CURRENT
@@ -1 +1 @@
-MANIFEST-000543
+MANIFEST-000551
diff --git a/packs-system/lf-equipment/LOG b/packs-system/lf-equipment/LOG
index 62767a3..41dda52 100644
--- a/packs-system/lf-equipment/LOG
+++ b/packs-system/lf-equipment/LOG
@@ -1,8 +1,8 @@
-2026/04/07-20:41:02.007080 7f6820dfe6c0 Recovering log #541
-2026/04/07-20:41:02.061052 7f6820dfe6c0 Delete type=3 #539
-2026/04/07-20:41:02.061103 7f6820dfe6c0 Delete type=0 #541
-2026/04/07-20:44:04.752043 7f656afef6c0 Level-0 table #546: started
-2026/04/07-20:44:04.752063 7f656afef6c0 Level-0 table #546: 0 bytes OK
-2026/04/07-20:44:04.788614 7f656afef6c0 Delete type=0 #544
-2026/04/07-20:44:04.864197 7f656afef6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
-2026/04/07-20:44:04.864237 7f656afef6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
+2026/04/12-00:56:41.816652 7f20ecbfc6c0 Recovering log #549
+2026/04/12-00:56:41.832396 7f20ecbfc6c0 Delete type=3 #547
+2026/04/12-00:56:41.832446 7f20ecbfc6c0 Delete type=0 #549
+2026/04/12-01:07:05.182307 7f1e4ffff6c0 Level-0 table #554: started
+2026/04/12-01:07:05.182364 7f1e4ffff6c0 Level-0 table #554: 0 bytes OK
+2026/04/12-01:07:05.188459 7f1e4ffff6c0 Delete type=0 #552
+2026/04/12-01:07:05.207103 7f1e4ffff6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
+2026/04/12-01:07:05.207149 7f1e4ffff6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
diff --git a/packs-system/lf-equipment/LOG.old b/packs-system/lf-equipment/LOG.old
index 0a70b47..e7ebbf3 100644
--- a/packs-system/lf-equipment/LOG.old
+++ b/packs-system/lf-equipment/LOG.old
@@ -1,8 +1,8 @@
-2026/02/06-21:03:10.117032 7f71b6ffd6c0 Recovering log #537
-2026/02/06-21:03:10.127068 7f71b6ffd6c0 Delete type=3 #535
-2026/02/06-21:03:10.127128 7f71b6ffd6c0 Delete type=0 #537
-2026/02/06-21:51:23.216143 7f71b67fc6c0 Level-0 table #542: started
-2026/02/06-21:51:23.216190 7f71b67fc6c0 Level-0 table #542: 0 bytes OK
-2026/02/06-21:51:23.251149 7f71b67fc6c0 Delete type=0 #540
-2026/02/06-21:51:23.327713 7f71b67fc6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
-2026/02/06-21:51:23.327750 7f71b67fc6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
+2026/04/11-21:32:22.595743 7f20ecbfc6c0 Recovering log #545
+2026/04/11-21:32:22.605266 7f20ecbfc6c0 Delete type=3 #543
+2026/04/11-21:32:22.605313 7f20ecbfc6c0 Delete type=0 #545
+2026/04/12-00:19:41.485614 7f1e4ffff6c0 Level-0 table #550: started
+2026/04/12-00:19:41.485662 7f1e4ffff6c0 Level-0 table #550: 0 bytes OK
+2026/04/12-00:19:41.492344 7f1e4ffff6c0 Delete type=0 #548
+2026/04/12-00:19:41.513106 7f1e4ffff6c0 Manual compaction at level-0 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
+2026/04/12-00:19:41.513142 7f1e4ffff6c0 Manual compaction at level-1 from '!folders!ATr9wZhg5uTVTksM' @ 72057594037927935 : 1 .. '!items!zw9RQocTdz3HRjZK' @ 0 : 0; will stop at (end)
diff --git a/packs-system/lf-equipment/MANIFEST-000543 b/packs-system/lf-equipment/MANIFEST-000551
similarity index 71%
rename from packs-system/lf-equipment/MANIFEST-000543
rename to packs-system/lf-equipment/MANIFEST-000551
index a109daa..e7bb988 100644
Binary files a/packs-system/lf-equipment/MANIFEST-000543 and b/packs-system/lf-equipment/MANIFEST-000551 differ
diff --git a/packs-system/lf-gifts/000542.log b/packs-system/lf-gifts/000550.log
similarity index 100%
rename from packs-system/lf-gifts/000542.log
rename to packs-system/lf-gifts/000550.log
diff --git a/packs-system/lf-gifts/CURRENT b/packs-system/lf-gifts/CURRENT
index 50be3cb..bd72cba 100644
--- a/packs-system/lf-gifts/CURRENT
+++ b/packs-system/lf-gifts/CURRENT
@@ -1 +1 @@
-MANIFEST-000540
+MANIFEST-000548
diff --git a/packs-system/lf-gifts/LOG b/packs-system/lf-gifts/LOG
index e94e602..53867d9 100644
--- a/packs-system/lf-gifts/LOG
+++ b/packs-system/lf-gifts/LOG
@@ -1,8 +1,8 @@
-2026/04/07-20:41:02.075476 7f67ebfff6c0 Recovering log #538
-2026/04/07-20:41:02.123121 7f67ebfff6c0 Delete type=3 #536
-2026/04/07-20:41:02.123187 7f67ebfff6c0 Delete type=0 #538
-2026/04/07-20:44:04.788736 7f656afef6c0 Level-0 table #543: started
-2026/04/07-20:44:04.788765 7f656afef6c0 Level-0 table #543: 0 bytes OK
-2026/04/07-20:44:04.826947 7f656afef6c0 Delete type=0 #541
-2026/04/07-20:44:04.864207 7f656afef6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
-2026/04/07-20:44:04.864245 7f656afef6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
+2026/04/12-00:56:41.835571 7f20ee3ff6c0 Recovering log #546
+2026/04/12-00:56:41.850974 7f20ee3ff6c0 Delete type=3 #544
+2026/04/12-00:56:41.851033 7f20ee3ff6c0 Delete type=0 #546
+2026/04/12-01:07:05.194434 7f1e4ffff6c0 Level-0 table #551: started
+2026/04/12-01:07:05.194455 7f1e4ffff6c0 Level-0 table #551: 0 bytes OK
+2026/04/12-01:07:05.200694 7f1e4ffff6c0 Delete type=0 #549
+2026/04/12-01:07:05.207125 7f1e4ffff6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
+2026/04/12-01:07:05.207165 7f1e4ffff6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
diff --git a/packs-system/lf-gifts/LOG.old b/packs-system/lf-gifts/LOG.old
index 870a937..093d196 100644
--- a/packs-system/lf-gifts/LOG.old
+++ b/packs-system/lf-gifts/LOG.old
@@ -1,8 +1,8 @@
-2026/02/06-21:03:10.133019 7f71b7fff6c0 Recovering log #534
-2026/02/06-21:03:10.144336 7f71b7fff6c0 Delete type=3 #532
-2026/02/06-21:03:10.144427 7f71b7fff6c0 Delete type=0 #534
-2026/02/06-21:51:23.291316 7f71b67fc6c0 Level-0 table #539: started
-2026/02/06-21:51:23.291356 7f71b67fc6c0 Level-0 table #539: 0 bytes OK
-2026/02/06-21:51:23.327518 7f71b67fc6c0 Delete type=0 #537
-2026/02/06-21:51:23.327733 7f71b67fc6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
-2026/02/06-21:51:23.327757 7f71b67fc6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
+2026/04/11-21:32:22.610678 7f20ed3fd6c0 Recovering log #542
+2026/04/11-21:32:22.621586 7f20ed3fd6c0 Delete type=3 #540
+2026/04/11-21:32:22.621657 7f20ed3fd6c0 Delete type=0 #542
+2026/04/12-00:19:41.492538 7f1e4ffff6c0 Level-0 table #547: started
+2026/04/12-00:19:41.492574 7f1e4ffff6c0 Level-0 table #547: 0 bytes OK
+2026/04/12-00:19:41.499243 7f1e4ffff6c0 Delete type=0 #545
+2026/04/12-00:19:41.513118 7f1e4ffff6c0 Manual compaction at level-0 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
+2026/04/12-00:19:41.513149 7f1e4ffff6c0 Manual compaction at level-1 from '!folders!yPWGvxHJbDNHVSnY' @ 72057594037927935 : 1 .. '!items!x5gLtqlW4sdDmHTd' @ 0 : 0; will stop at (end)
diff --git a/packs-system/lf-gifts/MANIFEST-000540 b/packs-system/lf-gifts/MANIFEST-000548
similarity index 77%
rename from packs-system/lf-gifts/MANIFEST-000540
rename to packs-system/lf-gifts/MANIFEST-000548
index a760628..93af4d6 100644
Binary files a/packs-system/lf-gifts/MANIFEST-000540 and b/packs-system/lf-gifts/MANIFEST-000548 differ
diff --git a/packs-system/lf-skills/000547.log b/packs-system/lf-skills/000555.log
similarity index 100%
rename from packs-system/lf-skills/000547.log
rename to packs-system/lf-skills/000555.log
diff --git a/packs-system/lf-skills/CURRENT b/packs-system/lf-skills/CURRENT
index 7f02150..06420a4 100644
--- a/packs-system/lf-skills/CURRENT
+++ b/packs-system/lf-skills/CURRENT
@@ -1 +1 @@
-MANIFEST-000545
+MANIFEST-000553
diff --git a/packs-system/lf-skills/LOG b/packs-system/lf-skills/LOG
index eef68f6..598b9fd 100644
--- a/packs-system/lf-skills/LOG
+++ b/packs-system/lf-skills/LOG
@@ -1,8 +1,8 @@
-2026/04/07-20:41:01.946718 7f67eb7fe6c0 Recovering log #543
-2026/04/07-20:41:01.999435 7f67eb7fe6c0 Delete type=3 #541
-2026/04/07-20:41:01.999504 7f67eb7fe6c0 Delete type=0 #543
-2026/04/07-20:44:04.712609 7f656afef6c0 Level-0 table #548: started
-2026/04/07-20:44:04.712651 7f656afef6c0 Level-0 table #548: 0 bytes OK
-2026/04/07-20:44:04.751939 7f656afef6c0 Delete type=0 #546
-2026/04/07-20:44:04.864184 7f656afef6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
-2026/04/07-20:44:04.864223 7f656afef6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
+2026/04/12-00:56:41.797453 7f20ed3fd6c0 Recovering log #551
+2026/04/12-00:56:41.813315 7f20ed3fd6c0 Delete type=3 #549
+2026/04/12-00:56:41.813371 7f20ed3fd6c0 Delete type=0 #551
+2026/04/12-01:07:05.200810 7f1e4ffff6c0 Level-0 table #556: started
+2026/04/12-01:07:05.200837 7f1e4ffff6c0 Level-0 table #556: 0 bytes OK
+2026/04/12-01:07:05.207008 7f1e4ffff6c0 Delete type=0 #554
+2026/04/12-01:07:05.207134 7f1e4ffff6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
+2026/04/12-01:07:05.207157 7f1e4ffff6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
diff --git a/packs-system/lf-skills/LOG.old b/packs-system/lf-skills/LOG.old
index 08838ff..0db9311 100644
--- a/packs-system/lf-skills/LOG.old
+++ b/packs-system/lf-skills/LOG.old
@@ -1,8 +1,8 @@
-2026/02/06-21:03:10.101791 7f71b77fe6c0 Recovering log #539
-2026/02/06-21:03:10.111914 7f71b77fe6c0 Delete type=3 #537
-2026/02/06-21:03:10.111991 7f71b77fe6c0 Delete type=0 #539
-2026/02/06-21:51:23.251285 7f71b67fc6c0 Level-0 table #544: started
-2026/02/06-21:51:23.251325 7f71b67fc6c0 Level-0 table #544: 0 bytes OK
-2026/02/06-21:51:23.291180 7f71b67fc6c0 Delete type=0 #542
-2026/02/06-21:51:23.327724 7f71b67fc6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
-2026/02/06-21:51:23.327764 7f71b67fc6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
+2026/04/11-21:32:22.580345 7f20ee3ff6c0 Recovering log #547
+2026/04/11-21:32:22.590425 7f20ee3ff6c0 Delete type=3 #545
+2026/04/11-21:32:22.590495 7f20ee3ff6c0 Delete type=0 #547
+2026/04/12-00:19:41.520423 7f1e4ffff6c0 Level-0 table #552: started
+2026/04/12-00:19:41.520464 7f1e4ffff6c0 Level-0 table #552: 0 bytes OK
+2026/04/12-00:19:41.527028 7f1e4ffff6c0 Delete type=0 #550
+2026/04/12-00:19:41.543900 7f1e4ffff6c0 Manual compaction at level-0 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
+2026/04/12-00:19:41.554664 7f1e4ffff6c0 Manual compaction at level-1 from '!folders!7j8H7DbmBb9Uza2X' @ 72057594037927935 : 1 .. '!items!zt8s7564ep1La4XQ' @ 0 : 0; will stop at (end)
diff --git a/packs-system/lf-skills/MANIFEST-000545 b/packs-system/lf-skills/MANIFEST-000553
similarity index 71%
rename from packs-system/lf-skills/MANIFEST-000545
rename to packs-system/lf-skills/MANIFEST-000553
index 265df64..2d11c7b 100644
Binary files a/packs-system/lf-skills/MANIFEST-000545 and b/packs-system/lf-skills/MANIFEST-000553 differ
diff --git a/packs-system/lf-spells-miracles/000242.log b/packs-system/lf-spells-miracles/000250.log
similarity index 100%
rename from packs-system/lf-spells-miracles/000242.log
rename to packs-system/lf-spells-miracles/000250.log
diff --git a/packs-system/lf-spells-miracles/CURRENT b/packs-system/lf-spells-miracles/CURRENT
index 803ffe2..f95348a 100644
--- a/packs-system/lf-spells-miracles/CURRENT
+++ b/packs-system/lf-spells-miracles/CURRENT
@@ -1 +1 @@
-MANIFEST-000240
+MANIFEST-000248
diff --git a/packs-system/lf-spells-miracles/LOG b/packs-system/lf-spells-miracles/LOG
index 13a890e..059ecd7 100644
--- a/packs-system/lf-spells-miracles/LOG
+++ b/packs-system/lf-spells-miracles/LOG
@@ -1,8 +1,8 @@
-2026/04/07-20:41:02.201674 7f6820dfe6c0 Recovering log #238
-2026/04/07-20:41:02.254572 7f6820dfe6c0 Delete type=3 #236
-2026/04/07-20:41:02.254638 7f6820dfe6c0 Delete type=0 #238
-2026/04/07-20:44:04.827060 7f656afef6c0 Level-0 table #243: started
-2026/04/07-20:44:04.827088 7f656afef6c0 Level-0 table #243: 0 bytes OK
-2026/04/07-20:44:04.864062 7f656afef6c0 Delete type=0 #241
-2026/04/07-20:44:04.864215 7f656afef6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
-2026/04/07-20:44:04.864230 7f656afef6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
+2026/04/12-00:56:41.870438 7f20ecbfc6c0 Recovering log #246
+2026/04/12-00:56:41.885745 7f20ecbfc6c0 Delete type=3 #244
+2026/04/12-00:56:41.885813 7f20ecbfc6c0 Delete type=0 #246
+2026/04/12-01:07:05.229253 7f1e4ffff6c0 Level-0 table #251: started
+2026/04/12-01:07:05.229286 7f1e4ffff6c0 Level-0 table #251: 0 bytes OK
+2026/04/12-01:07:05.235261 7f1e4ffff6c0 Delete type=0 #249
+2026/04/12-01:07:05.235386 7f1e4ffff6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
+2026/04/12-01:07:05.245080 7f1e4ffff6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
diff --git a/packs-system/lf-spells-miracles/LOG.old b/packs-system/lf-spells-miracles/LOG.old
index fa08aab..e80364e 100644
--- a/packs-system/lf-spells-miracles/LOG.old
+++ b/packs-system/lf-spells-miracles/LOG.old
@@ -1,8 +1,8 @@
-2026/02/06-21:03:10.160494 7f71b6ffd6c0 Recovering log #234
-2026/02/06-21:03:10.170771 7f71b6ffd6c0 Delete type=3 #232
-2026/02/06-21:03:10.170846 7f71b6ffd6c0 Delete type=0 #234
-2026/02/06-21:51:23.435610 7f71b67fc6c0 Level-0 table #239: started
-2026/02/06-21:51:23.435649 7f71b67fc6c0 Level-0 table #239: 0 bytes OK
-2026/02/06-21:51:23.473038 7f71b67fc6c0 Delete type=0 #237
-2026/02/06-21:51:23.473211 7f71b67fc6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
-2026/02/06-21:51:23.473250 7f71b67fc6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
+2026/04/11-21:32:22.642207 7f20edbfe6c0 Recovering log #242
+2026/04/11-21:32:22.652005 7f20edbfe6c0 Delete type=3 #240
+2026/04/11-21:32:22.652058 7f20edbfe6c0 Delete type=0 #242
+2026/04/12-00:19:41.506566 7f1e4ffff6c0 Level-0 table #247: started
+2026/04/12-00:19:41.506602 7f1e4ffff6c0 Level-0 table #247: 0 bytes OK
+2026/04/12-00:19:41.512971 7f1e4ffff6c0 Delete type=0 #245
+2026/04/12-00:19:41.513136 7f1e4ffff6c0 Manual compaction at level-0 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
+2026/04/12-00:19:41.513162 7f1e4ffff6c0 Manual compaction at level-1 from '!folders!37mu4dxsSuftlnmP' @ 72057594037927935 : 1 .. '!items!zKOpU34oLziGJW6y' @ 0 : 0; will stop at (end)
diff --git a/packs-system/lf-spells-miracles/MANIFEST-000240 b/packs-system/lf-spells-miracles/MANIFEST-000248
similarity index 72%
rename from packs-system/lf-spells-miracles/MANIFEST-000240
rename to packs-system/lf-spells-miracles/MANIFEST-000248
index da96f66..219127b 100644
Binary files a/packs-system/lf-spells-miracles/MANIFEST-000240 and b/packs-system/lf-spells-miracles/MANIFEST-000248 differ
diff --git a/packs-system/lf-vulnerabilities/000541.log b/packs-system/lf-vulnerabilities/000549.log
similarity index 100%
rename from packs-system/lf-vulnerabilities/000541.log
rename to packs-system/lf-vulnerabilities/000549.log
diff --git a/packs-system/lf-vulnerabilities/CURRENT b/packs-system/lf-vulnerabilities/CURRENT
index c02f3d8..8ef3a46 100644
--- a/packs-system/lf-vulnerabilities/CURRENT
+++ b/packs-system/lf-vulnerabilities/CURRENT
@@ -1 +1 @@
-MANIFEST-000539
+MANIFEST-000547
diff --git a/packs-system/lf-vulnerabilities/LOG b/packs-system/lf-vulnerabilities/LOG
index 0412df1..ace657c 100644
--- a/packs-system/lf-vulnerabilities/LOG
+++ b/packs-system/lf-vulnerabilities/LOG
@@ -1,8 +1,8 @@
-2026/04/07-20:41:02.136268 7f67eb7fe6c0 Recovering log #537
-2026/04/07-20:41:02.189277 7f67eb7fe6c0 Delete type=3 #535
-2026/04/07-20:41:02.189349 7f67eb7fe6c0 Delete type=0 #537
-2026/04/07-20:44:04.864322 7f656afef6c0 Level-0 table #542: started
-2026/04/07-20:44:04.864350 7f656afef6c0 Level-0 table #542: 0 bytes OK
-2026/04/07-20:44:04.894847 7f656afef6c0 Delete type=0 #540
-2026/04/07-20:44:05.055029 7f656afef6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
-2026/04/07-20:44:05.055068 7f656afef6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
+2026/04/12-00:56:41.853085 7f20ed3fd6c0 Recovering log #545
+2026/04/12-00:56:41.867862 7f20ed3fd6c0 Delete type=3 #543
+2026/04/12-00:56:41.867925 7f20ed3fd6c0 Delete type=0 #545
+2026/04/12-01:07:05.188580 7f1e4ffff6c0 Level-0 table #550: started
+2026/04/12-01:07:05.188606 7f1e4ffff6c0 Level-0 table #550: 0 bytes OK
+2026/04/12-01:07:05.194367 7f1e4ffff6c0 Delete type=0 #548
+2026/04/12-01:07:05.207115 7f1e4ffff6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
+2026/04/12-01:07:05.207142 7f1e4ffff6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
diff --git a/packs-system/lf-vulnerabilities/LOG.old b/packs-system/lf-vulnerabilities/LOG.old
index dde7648..b11ba39 100644
--- a/packs-system/lf-vulnerabilities/LOG.old
+++ b/packs-system/lf-vulnerabilities/LOG.old
@@ -1,8 +1,8 @@
-2026/02/06-21:03:10.146976 7f71b77fe6c0 Recovering log #533
-2026/02/06-21:03:10.156549 7f71b77fe6c0 Delete type=3 #531
-2026/02/06-21:03:10.156610 7f71b77fe6c0 Delete type=0 #533
-2026/02/06-21:51:23.327858 7f71b67fc6c0 Level-0 table #538: started
-2026/02/06-21:51:23.327892 7f71b67fc6c0 Level-0 table #538: 0 bytes OK
-2026/02/06-21:51:23.359727 7f71b67fc6c0 Delete type=0 #536
-2026/02/06-21:51:23.473179 7f71b67fc6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
-2026/02/06-21:51:23.473221 7f71b67fc6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
+2026/04/11-21:32:22.628476 7f20ecbfc6c0 Recovering log #541
+2026/04/11-21:32:22.638519 7f20ecbfc6c0 Delete type=3 #539
+2026/04/11-21:32:22.638582 7f20ecbfc6c0 Delete type=0 #541
+2026/04/12-00:19:41.499517 7f1e4ffff6c0 Level-0 table #546: started
+2026/04/12-00:19:41.499556 7f1e4ffff6c0 Level-0 table #546: 0 bytes OK
+2026/04/12-00:19:41.506406 7f1e4ffff6c0 Delete type=0 #544
+2026/04/12-00:19:41.513127 7f1e4ffff6c0 Manual compaction at level-0 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
+2026/04/12-00:19:41.513155 7f1e4ffff6c0 Manual compaction at level-1 from '!folders!mnO9OzE7BEE2KDfh' @ 72057594037927935 : 1 .. '!items!zkK6ixtCsCw3RH9X' @ 0 : 0; will stop at (end)
diff --git a/packs-system/lf-vulnerabilities/MANIFEST-000539 b/packs-system/lf-vulnerabilities/MANIFEST-000547
similarity index 72%
rename from packs-system/lf-vulnerabilities/MANIFEST-000539
rename to packs-system/lf-vulnerabilities/MANIFEST-000547
index 34a0c5f..ca00070 100644
Binary files a/packs-system/lf-vulnerabilities/MANIFEST-000539 and b/packs-system/lf-vulnerabilities/MANIFEST-000547 differ
diff --git a/templates/chat-message.hbs b/templates/chat-message.hbs
index 73006c3..ca3e522 100644
--- a/templates/chat-message.hbs
+++ b/templates/chat-message.hbs
@@ -239,7 +239,7 @@
- Damage automatically applied to defender (with Armor DR)
+ Damage automatically applied to defender (with damage reduction)
{{else}}
@@ -288,4 +288,4 @@
{{/if}}
{{/if}}
-
\ No newline at end of file
+
diff --git a/templates/combat-tracker-footer-v2.hbs b/templates/combat-tracker-footer-v2.hbs
index 82406f3..6eb221e 100644
--- a/templates/combat-tracker-footer-v2.hbs
+++ b/templates/combat-tracker-footer-v2.hbs
@@ -3,7 +3,7 @@
{{!-- GM Controls --}}
{{#if user.isGM}}
- {{#if combat.round}}
+ {{#if (or combat.round (eq combat.turn 0))}}