Compare commits

...

7 Commits

Author SHA1 Message Date
uberwald 5734f3a5e3 Fix layperson skills ate character creation
Release Creation / build (release) Successful in 53s
2026-07-07 08:30:23 +02:00
uberwald 7675c47ff9 fix: persist rollType/actorId on rollBase.options, re-evaluate canReroll after mulligan
Release Creation / build (release) Failing after 2m9s
- roll-prompt.mjs: explicitly save rollType, actorId, actorName, actorImage
  to rollBase.options so mulligan reroll context reads valid values
- chat-reaction.mjs: replace canRerollDefense/canRerollAttack = false with
  recomputation from updated D30 message — aligns same-client with
  cross-client behavior (new D30 always takes effect, mulligan chain loops)
2026-07-06 08:56:58 +02:00
uberwald 90fde23ebc docs: update AGENTS.md with partial registration and try/catch fixes
Release Creation / build (release) Successful in 45s
2026-07-05 21:12:20 +02:00
uberwald d17c9cba49 fix: wrap do-while loop body in try/catch to protect against exceptions
Any exception from createReactionMessage or renderTemplate inside the
defense/attack reaction loops would silently abort the handler, preventing
combat result creation. The try/catch logs the error and continues safely.
2026-07-05 20:55:29 +02:00
uberwald bfbce9557b fix: wrap mulligan message creation in try/catch to prevent handler abort
Even with the partial correctly registered, a template rendering failure
should not abort the entire reroll handler. Without try/catch, the
exception propagates out of the do-while loop and prevents combat result
creation. The reroll value was applied but nothing else happened.
2026-07-05 20:51:26 +02:00
uberwald 881cecf5b7 chore: remove DevTools session artifacts 2026-07-05 20:45:29 +02:00
uberwald de31c397fa fix: use object key for partial ID to match template reference
loadTemplates with an array registers the partial using the full file
path as its Handlebars ID (e.g.
'systems/fvtt-lethal-fantasy/templates/chat/_dice-breakdown.hbs'), but
the template references it by the short name 'chat/dice-breakdown'.
Using an object with explicit keys ensures the partial ID matches.
2026-07-05 20:45:24 +02:00
6 changed files with 71 additions and 44 deletions
+2
View File
@@ -55,6 +55,8 @@ Fix Grit/Luck defense reaction dialog UX (stacking dialogs, multiple clicks, rev
- **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
- **Auto-roll bonus dice without dialog** — matches existing D30=27 (d6E) flow
+7 -4
View File
@@ -114,10 +114,13 @@ Hooks.once("ready", async function () {
// Initialiser la table des résultats D30
await documents.D30Roll.initialize()
// Register Handlebars partials used by chat templates
await foundry.applications.handlebars.loadTemplates([
"systems/fvtt-lethal-fantasy/templates/chat/_dice-breakdown.hbs"
])
// 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
_registerBleedingHooks()
+4 -2
View File
@@ -7,8 +7,10 @@ export default class LethalFantasyActor extends Actor {
if (data instanceof Array) {
return super.create(data, options);
}
// If the created actor has items (only applicable to duplicated actors) bypass the new actor creation logic
if (data.items) {
// If the created actor already has items (duplicated or imported) bypass the new actor creation logic.
// Use .length check because Foundry V2 initializes data.items as an empty array by default
// (truthy), which would incorrectly skip the layperson skill auto-population.
if (data.items?.length > 0) {
let actor = super.create(data, options);
return actor;
}
+4
View File
@@ -570,6 +570,10 @@ export async function prompt(options = {}) {
rollBase.options.D30result = options.D30result
rollBase.options.D30message = options.D30message
rollBase.options.badResult = badResult
rollBase.options.rollType = options.rollType
rollBase.options.actorId = options.actorId
rollBase.options.actorName = options.actorName
rollBase.options.actorImage = options.actorImage
rollBase.options.rollData = foundry.utils.duplicate(rollData)
rollBase.options.defenderId = options.defenderId
rollBase.options.defenderTokenId = options.defenderTokenId
+19 -7
View File
@@ -466,6 +466,7 @@ Hooks.on("createChatMessage", async (message) => {
let d30AttackPrecomputedStale = false
do {
try {
mulliganRestart = false
defenderHandledBonus = false
attackerHandledBonus = false
@@ -548,9 +549,9 @@ Hooks.on("createChatMessage", async (message) => {
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
try {
await createReactionMessage(defender, {
type: "mulligan",
actorName: defenderName,
@@ -561,12 +562,16 @@ Hooks.on("createChatMessage", async (message) => {
D30result: reroll.options?.D30result,
D30message: reroll.options?.D30message
})
// Apply new D30 result on the restart
} catch(e) {
console.error("Mulligan message creation failed (non-fatal):", e)
}
// Apply new D30 result on the restart — the new D30 always takes effect,
// and if it's another mulligan the reroll button reappears.
if (reroll.options?.D30message) {
defenseD30message = reroll.options.D30message
defenseD30Processed = false
}
// Restart the full comparison so both sides can react to the new roll
canRerollDefense = LethalFantasyUtils.hasD30Reroll(defenseD30message)
mulliganRestart = true
break
}
@@ -750,9 +755,9 @@ Hooks.on("createChatMessage", async (message) => {
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
try {
await createReactionMessage(attacker, {
type: "mulligan",
actorName: attackerName,
@@ -763,14 +768,17 @@ Hooks.on("createChatMessage", async (message) => {
D30result: reroll.options?.D30result,
D30message: reroll.options?.D30message
})
// Apply new D30 result on the restart
} catch(e) {
console.error("Mulligan message creation failed (non-fatal):", e)
}
// Apply new D30 result on the restart — the new D30 always takes effect,
// and if it's another mulligan the reroll button reappears.
if (reroll.options?.D30message) {
attackD30message = reroll.options.D30message
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
canRerollAttack = LethalFantasyUtils.hasD30Reroll(attackD30message)
mulliganRestart = true
break
}
@@ -821,6 +829,10 @@ Hooks.on("createChatMessage", async (message) => {
mulliganRestart = true
}
}
} catch(e) {
console.error("Defense handler loop error (non-fatal):", e)
mulliganRestart = false
}
} while (mulliganRestart)
const shieldDamageReduction = shieldBlocked ? shieldReaction?.damageReduction ?? 0 : 0
+4
View File
@@ -206,6 +206,7 @@ export async function handleAttackBoosted(msg) {
if (!reroll) continue
updatedDefenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll
let newD30message = reroll.options?.D30message || null
try {
const mulliganContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {
type: "mulligan",
actorName: defenderName,
@@ -217,6 +218,9 @@ export async function handleAttackBoosted(msg) {
D30message: newD30message
})
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
if (newD30message) {
defenseD30message = newD30message