Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90fde23ebc | |||
| d17c9cba49 | |||
| bfbce9557b | |||
| 881cecf5b7 | |||
| de31c397fa | |||
| fe346f46bf | |||
| 96e5bd5b4d |
@@ -50,6 +50,14 @@ Fix Grit/Luck defense reaction dialog UX (stacking dialogs, multiple clicks, rev
|
|||||||
- **Import `hasD30Reroll` added** to `utils/combat.mjs`
|
- **Import `hasD30Reroll` added** to `utils/combat.mjs`
|
||||||
- **`bleed` top-level type handler added** to `processD30BonusDice` in `d30.mjs:79-81` — returns `specialEffect: "bleed"` same as combo path, so ranged attack bleed (values 5,10,15) creates reaction message and sets damage button bleed flag.
|
- **`bleed` top-level type handler added** to `processD30BonusDice` in `d30.mjs:79-81` — returns `specialEffect: "bleed"` same as combo path, so ranged attack bleed (values 5,10,15) creates reaction message and sets damage button bleed flag.
|
||||||
|
|
||||||
|
### Pass 7 — Cross-Client Attack D30 Pre-Processing
|
||||||
|
- **BUG FIX: D30 attack bonus processed at source (defense request time) instead of in defense handler** — `chat-reaction.mjs:136-148` now calls `processD30BonusDice` in the defense request button click handler, sends the boosted `attackRoll` and full `d30AttackEffects` result in the defense request. The `createChatMessage` handler at `chat-reaction.mjs:622-626` uses the pre-computed values instead of re-processing (avoids double dice roll and ensures both clients agree on attack value).
|
||||||
|
- **Fix ensures cross-client agreement**: Player and GM clients see the same boosted attack roll because it's computed once at attack time and sent via socket, not separately per client in the defense handler.
|
||||||
|
- **BUG FIX: `d30AttackEffects` not propagated through `_storeNextDefenseData`** — added `d30AttackEffects` to `nextDefenseData` in `combat.mjs:353-355` so the `createChatMessage` handler finds pre-computed values for same-client path (was silently falling back to legacy dice-roll, causing double boost).
|
||||||
|
- **BUG FIX: `d30AttackPrecomputedStale` flag for mulligan rerolls** — added `chat-reaction.mjs:464` flag and check at `chat-reaction.mjs:632-634`. After a mulligan reroll updates `attackD30message`, the stale pre-computed effects are bypassed and the new D30 message is processed fresh via `processD30BonusDice`.
|
||||||
|
- **BUG FIX: `_dice-breakdown.hbs` partial never registered** — `loadTemplates([path])` registers partials with the full file path as the Handlebars ID, but the template references it by the short name `chat/dice-breakdown`. Fixed by passing an object `{"chat/dice-breakdown": "path"}` which explicitly sets the partial ID to match the template reference.
|
||||||
|
- **SAFETY: do-while loop body wrapped in try/catch** — any exception from `createReactionMessage` calls now logs the error and continues safely instead of silently aborting the handler.
|
||||||
|
|
||||||
## Key Decisions
|
## Key Decisions
|
||||||
- **Auto-roll bonus dice without dialog** — matches existing D30=27 (d6E) flow
|
- **Auto-roll bonus dice without dialog** — matches existing D30=27 (d6E) flow
|
||||||
- **`buildDefenseReactionButtons` extracts only button-building** — defense while-loop structures differ between same-client and cross-client; merging loops risks behavioral divergence
|
- **`buildDefenseReactionButtons` extracts only button-building** — defense while-loop structures differ between same-client and cross-client; merging loops risks behavioral divergence
|
||||||
|
|||||||
+10
-2
@@ -108,11 +108,19 @@ function preLocalizeConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Hooks.once("ready", function () {
|
Hooks.once("ready", async function () {
|
||||||
console.info("LETHAL FANTASY | Ready")
|
console.info("LETHAL FANTASY | Ready")
|
||||||
|
|
||||||
// Initialiser la table des résultats D30
|
// Initialiser la table des résultats D30
|
||||||
documents.D30Roll.initialize()
|
await documents.D30Roll.initialize()
|
||||||
|
|
||||||
|
// Register Handlebars partials used by chat templates.
|
||||||
|
// The object key is the partial ID referenced in templates (e.g., {{> chat/dice-breakdown}}),
|
||||||
|
// the value is the file path. Using an array would register with the full path as the ID,
|
||||||
|
// which does NOT match the template reference.
|
||||||
|
await foundry.applications.handlebars.loadTemplates({
|
||||||
|
"chat/dice-breakdown": "systems/fvtt-lethal-fantasy/templates/chat/_dice-breakdown.hbs"
|
||||||
|
})
|
||||||
|
|
||||||
// Saignement piloté par le combat tracker
|
// Saignement piloté par le combat tracker
|
||||||
_registerBleedingHooks()
|
_registerBleedingHooks()
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => {
|
|||||||
btn.addEventListener("mouseleave", () => canvas.tokens.releaseAll())
|
btn.addEventListener("mouseleave", () => canvas.tokens.releaseAll())
|
||||||
|
|
||||||
// Gestionnaire pour les boutons de demande de défense
|
// Gestionnaire pour les boutons de demande de défense
|
||||||
btn.addEventListener("click", event => {
|
btn.addEventListener("click", async event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
@@ -140,13 +140,24 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => {
|
|||||||
|| (attackerWeapon?.system?.weaponType === "ranged")
|
|| (attackerWeapon?.system?.weaponType === "ranged")
|
||||||
|| (rollTargetOptions?.isRangedAttack === true)
|
|| (rollTargetOptions?.isRangedAttack === true)
|
||||||
|
|
||||||
|
// Process D30 attack bonus at source so both clients get the same boosted value.
|
||||||
|
// The d30BonusRoll chat message is created here (by _rollD30BonusDie inside processD30BonusDice).
|
||||||
|
let d30AttackEffects = null
|
||||||
|
let boostedAttackRoll = attackRoll
|
||||||
|
if (attackD30message && attacker) {
|
||||||
|
d30AttackEffects = await LethalFantasyUtils.processD30BonusDice(attackD30message, "attack", attackNaturalRoll, attacker, true)
|
||||||
|
if (d30AttackEffects.modifier) {
|
||||||
|
boostedAttackRoll = attackRoll + d30AttackEffects.modifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const defenseMsg = {
|
const defenseMsg = {
|
||||||
type: "requestDefense",
|
type: "requestDefense",
|
||||||
attackerName,
|
attackerName,
|
||||||
attackerId,
|
attackerId,
|
||||||
defenderName,
|
defenderName,
|
||||||
weaponName,
|
weaponName,
|
||||||
attackRoll,
|
attackRoll: boostedAttackRoll,
|
||||||
attackWeaponId,
|
attackWeaponId,
|
||||||
attackRollType,
|
attackRollType,
|
||||||
attackRollKey,
|
attackRollKey,
|
||||||
@@ -157,7 +168,8 @@ Hooks.on("renderChatMessageHTML", (message, html, data) => {
|
|||||||
damageTier,
|
damageTier,
|
||||||
combatantId,
|
combatantId,
|
||||||
tokenId,
|
tokenId,
|
||||||
isRanged: isRangedAttack
|
isRanged: isRangedAttack,
|
||||||
|
d30AttackEffects
|
||||||
}
|
}
|
||||||
|
|
||||||
// Envoyer le message socket à l'utilisateur contrôlant le combatant
|
// Envoyer le message socket à l'utilisateur contrôlant le combatant
|
||||||
@@ -448,8 +460,13 @@ Hooks.on("createChatMessage", async (message) => {
|
|||||||
let d30Bleed = false
|
let d30Bleed = false
|
||||||
let d30DamageMultiplier = 1
|
let d30DamageMultiplier = 1
|
||||||
let d30DrMultiplier = 1
|
let d30DrMultiplier = 1
|
||||||
|
// Tracks whether the pre-computed D30 effects are stale (invalidated by a mulligan reroll
|
||||||
|
// that produced a new D30 message). When a reroll updates attackD30message, the original
|
||||||
|
// d30AttackEffects from the defense request no longer apply — the new D30 must be processed.
|
||||||
|
let d30AttackPrecomputedStale = false
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
try {
|
||||||
mulliganRestart = false
|
mulliganRestart = false
|
||||||
defenderHandledBonus = false
|
defenderHandledBonus = false
|
||||||
attackerHandledBonus = false
|
attackerHandledBonus = false
|
||||||
@@ -535,16 +552,20 @@ Hooks.on("createChatMessage", async (message) => {
|
|||||||
canRerollDefense = false
|
canRerollDefense = false
|
||||||
if (!reroll) continue
|
if (!reroll) continue
|
||||||
defenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll
|
defenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll
|
||||||
await createReactionMessage(defender, {
|
try {
|
||||||
type: "mulligan",
|
await createReactionMessage(defender, {
|
||||||
actorName: defenderName,
|
type: "mulligan",
|
||||||
side: "defense",
|
actorName: defenderName,
|
||||||
oldRoll: oldDefenseRoll,
|
side: "defense",
|
||||||
newRoll: defenseRoll,
|
oldRoll: oldDefenseRoll,
|
||||||
diceResults: reroll.options?.diceResults || [],
|
newRoll: defenseRoll,
|
||||||
D30result: reroll.options?.D30result,
|
diceResults: reroll.options?.diceResults || [],
|
||||||
D30message: reroll.options?.D30message
|
D30result: reroll.options?.D30result,
|
||||||
})
|
D30message: reroll.options?.D30message
|
||||||
|
})
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Mulligan message creation failed (non-fatal):", e)
|
||||||
|
}
|
||||||
// Apply new D30 result on the restart
|
// Apply new D30 result on the restart
|
||||||
if (reroll.options?.D30message) {
|
if (reroll.options?.D30message) {
|
||||||
defenseD30message = reroll.options.D30message
|
defenseD30message = reroll.options.D30message
|
||||||
@@ -605,26 +626,42 @@ Hooks.on("createChatMessage", async (message) => {
|
|||||||
// ── D30 bonus dice (attack) — resolved before grit/luck ────────────────
|
// ── D30 bonus dice (attack) — resolved before grit/luck ────────────────
|
||||||
if (attackD30message && !attackD30Processed) {
|
if (attackD30message && !attackD30Processed) {
|
||||||
const preD30AttackRoll = attackRollFinal
|
const preD30AttackRoll = attackRollFinal
|
||||||
const canDialog = isPrimaryController(attacker)
|
const canShow = isPrimaryController(attacker)
|
||||||
const d30Result = await LethalFantasyUtils.processD30BonusDice(attackD30message, "attack", attackNaturalRoll, attacker, canDialog)
|
|
||||||
|
// Use pre-computed D30 effects (processed at attack time in the defense
|
||||||
|
// request handler) to avoid re-rolling dice on the defense handler.
|
||||||
|
// This ensures both clients agree on the boosted attack roll.
|
||||||
|
// When d30AttackPrecomputedStale is true (invalidated by a mulligan reroll),
|
||||||
|
// skip the pre-computed effects and process the new D30 message fresh.
|
||||||
|
const usePrecomputed = !!attackData.d30AttackEffects && !d30AttackPrecomputedStale
|
||||||
|
const d30Result = usePrecomputed
|
||||||
|
? attackData.d30AttackEffects
|
||||||
|
: await LethalFantasyUtils.processD30BonusDice(attackD30message, "attack", attackNaturalRoll, attacker, canShow)
|
||||||
|
d30AttackPrecomputedStale = false
|
||||||
|
|
||||||
if (d30Result.modifier) {
|
if (d30Result.modifier) {
|
||||||
attackRollFinal += d30Result.modifier
|
if (!usePrecomputed) {
|
||||||
if (d30Result.modifier > 0 && canDialog) {
|
// Legacy or post-reroll path: bonus was just rolled, apply now
|
||||||
|
attackRollFinal += d30Result.modifier
|
||||||
|
}
|
||||||
|
// d30BonusRoll message was created at attack time (pre-processed)
|
||||||
|
// or inside _rollD30BonusDie (legacy). Either way, show the application.
|
||||||
|
if (d30Result.modifier > 0 && canShow) {
|
||||||
await createReactionMessage(attacker, {type:"d30Bonus", actorName:attackerName, value:d30Result.modifier, side:"attack"})
|
await createReactionMessage(attacker, {type:"d30Bonus", actorName:attackerName, value:d30Result.modifier, side:"attack"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (d30Result.specialEffect === "flag" && canDialog) {
|
if (d30Result.specialEffect === "flag" && canShow) {
|
||||||
await createReactionMessage(attacker, {type:"d30Flag", actorName:attackerName, specialName:d30Result.specialName||"Special Effect"})
|
await createReactionMessage(attacker, {type:"d30Flag", actorName:attackerName, specialName:d30Result.specialName||"Special Effect"})
|
||||||
}
|
}
|
||||||
if (d30Result.specialEffect === "bleed") {
|
if (d30Result.specialEffect === "bleed") {
|
||||||
d30Bleed = true
|
d30Bleed = true
|
||||||
if (canDialog) {
|
if (canShow) {
|
||||||
await createReactionMessage(attacker, {type:"d30Bleed", actorName:attackerName})
|
await createReactionMessage(attacker, {type:"d30Bleed", actorName:attackerName})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (d30Result.specialEffect === "damageMultiplier") {
|
if (d30Result.specialEffect === "damageMultiplier") {
|
||||||
d30DamageMultiplier = d30Result.multiplier
|
d30DamageMultiplier = d30Result.multiplier
|
||||||
if (canDialog) {
|
if (canShow) {
|
||||||
await createReactionMessage(attacker, {type:"d30DamageMultiplier", actorName:attackerName, value:d30Result.multiplier})
|
await createReactionMessage(attacker, {type:"d30DamageMultiplier", actorName:attackerName, value:d30Result.multiplier})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -721,20 +758,26 @@ Hooks.on("createChatMessage", async (message) => {
|
|||||||
canRerollAttack = false
|
canRerollAttack = false
|
||||||
if (!reroll) continue
|
if (!reroll) continue
|
||||||
attackRollFinal = reroll.options?.rollTotal || reroll.total || oldAttackRoll
|
attackRollFinal = reroll.options?.rollTotal || reroll.total || oldAttackRoll
|
||||||
await createReactionMessage(attacker, {
|
try {
|
||||||
type: "mulligan",
|
await createReactionMessage(attacker, {
|
||||||
actorName: attackerName,
|
type: "mulligan",
|
||||||
side: "attack",
|
actorName: attackerName,
|
||||||
oldRoll: oldAttackRoll,
|
side: "attack",
|
||||||
newRoll: attackRollFinal,
|
oldRoll: oldAttackRoll,
|
||||||
diceResults: reroll.options?.diceResults || [],
|
newRoll: attackRollFinal,
|
||||||
D30result: reroll.options?.D30result,
|
diceResults: reroll.options?.diceResults || [],
|
||||||
D30message: reroll.options?.D30message
|
D30result: reroll.options?.D30result,
|
||||||
})
|
D30message: reroll.options?.D30message
|
||||||
|
})
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Mulligan message creation failed (non-fatal):", e)
|
||||||
|
}
|
||||||
// Apply new D30 result on the restart
|
// Apply new D30 result on the restart
|
||||||
if (reroll.options?.D30message) {
|
if (reroll.options?.D30message) {
|
||||||
attackD30message = reroll.options.D30message
|
attackD30message = reroll.options.D30message
|
||||||
attackD30Processed = false
|
attackD30Processed = false
|
||||||
|
// Invalidate pre-computed effects — the new D30 message must be processed fresh
|
||||||
|
d30AttackPrecomputedStale = true
|
||||||
}
|
}
|
||||||
// Restart the full comparison so both sides can react to the new roll
|
// Restart the full comparison so both sides can react to the new roll
|
||||||
mulliganRestart = true
|
mulliganRestart = true
|
||||||
@@ -787,6 +830,10 @@ Hooks.on("createChatMessage", async (message) => {
|
|||||||
mulliganRestart = true
|
mulliganRestart = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Defense handler loop error (non-fatal):", e)
|
||||||
|
mulliganRestart = false
|
||||||
|
}
|
||||||
} while (mulliganRestart)
|
} while (mulliganRestart)
|
||||||
|
|
||||||
const shieldDamageReduction = shieldBlocked ? shieldReaction?.damageReduction ?? 0 : 0
|
const shieldDamageReduction = shieldBlocked ? shieldReaction?.damageReduction ?? 0 : 0
|
||||||
|
|||||||
+17
-11
@@ -206,17 +206,21 @@ export async function handleAttackBoosted(msg) {
|
|||||||
if (!reroll) continue
|
if (!reroll) continue
|
||||||
updatedDefenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll
|
updatedDefenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll
|
||||||
let newD30message = reroll.options?.D30message || null
|
let newD30message = reroll.options?.D30message || null
|
||||||
const mulliganContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {
|
try {
|
||||||
type: "mulligan",
|
const mulliganContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {
|
||||||
actorName: defenderName,
|
type: "mulligan",
|
||||||
side: "defense",
|
actorName: defenderName,
|
||||||
oldRoll: oldDefenseRoll,
|
side: "defense",
|
||||||
newRoll: updatedDefenseRoll,
|
oldRoll: oldDefenseRoll,
|
||||||
diceResults: reroll.options?.diceResults || [],
|
newRoll: updatedDefenseRoll,
|
||||||
D30result: reroll.options?.D30result,
|
diceResults: reroll.options?.diceResults || [],
|
||||||
D30message: newD30message
|
D30result: reroll.options?.D30result,
|
||||||
})
|
D30message: newD30message
|
||||||
await ChatMessage.create({content: mulliganContent, speaker: ChatMessage.getSpeaker({actor: defender})})
|
})
|
||||||
|
await ChatMessage.create({content: mulliganContent, speaker: ChatMessage.getSpeaker({actor: defender})})
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Mulligan message creation failed (non-fatal):", e)
|
||||||
|
}
|
||||||
// Process new D30 bonus dice from the reroll
|
// Process new D30 bonus dice from the reroll
|
||||||
if (newD30message) {
|
if (newD30message) {
|
||||||
defenseD30message = newD30message
|
defenseD30message = newD30message
|
||||||
@@ -302,6 +306,7 @@ export async function showDefenseRequest(msg) {
|
|||||||
const attackD30result = msg.attackD30result
|
const attackD30result = msg.attackD30result
|
||||||
const attackD30message = msg.attackD30message
|
const attackD30message = msg.attackD30message
|
||||||
const attackRerollContext = msg.attackRerollContext
|
const attackRerollContext = msg.attackRerollContext
|
||||||
|
const d30AttackEffects = msg.d30AttackEffects
|
||||||
const combatantId = msg.combatantId
|
const combatantId = msg.combatantId
|
||||||
const tokenId = msg.tokenId
|
const tokenId = msg.tokenId
|
||||||
|
|
||||||
@@ -351,6 +356,7 @@ export async function showDefenseRequest(msg) {
|
|||||||
damageTier: msg.damageTier,
|
damageTier: msg.damageTier,
|
||||||
defenderId: defender.id, defenderTokenId,
|
defenderId: defender.id, defenderTokenId,
|
||||||
...(msg.attackNaturalRoll !== undefined && { attackNaturalRoll: msg.attackNaturalRoll }),
|
...(msg.attackNaturalRoll !== undefined && { attackNaturalRoll: msg.attackNaturalRoll }),
|
||||||
|
...(d30AttackEffects !== undefined && { d30AttackEffects }),
|
||||||
...(opts.isRanged !== undefined && { isRanged: opts.isRanged })
|
...(opts.isRanged !== undefined && { isRanged: opts.isRanged })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user