This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# Lethal Fantasy FoundryVTT System — Session Context
|
# Lethal Fantasy FoundryVTT System — Session Context
|
||||||
|
|
||||||
## Current Goal
|
## Current Goal
|
||||||
Fix Grit/Luck defense reaction dialog UX (stacking dialogs, multiple clicks, revert on close) and cross-client sync of defense bonuses.
|
Fix Grit/Luck defense reaction dialog UX (stacking dialogs, multiple clicks, revert on close) and cross-client sync of defense bonuses. Fix monster defense mulligan reroll button missing cross-client.
|
||||||
|
|
||||||
## Accomplished
|
## Accomplished
|
||||||
|
|
||||||
@@ -24,15 +24,15 @@ Fix Grit/Luck defense reaction dialog UX (stacking dialogs, multiple clicks, rev
|
|||||||
### Pass 4 — D30 Dialog Removal & Dead Code Audit
|
### Pass 4 — D30 Dialog Removal & Dead Code Audit
|
||||||
- **D30 choice dialog removed** — auto-rolls bonus dice; special strike/defense reported as `specialEffect: "flag"` (informational)
|
- **D30 choice dialog removed** — auto-rolls bonus dice; special strike/defense reported as `specialEffect: "flag"` (informational)
|
||||||
- **Spell calamity choice restored** — catch-all for non-standard choices uses `specialEffect: "flag"`
|
- **Spell calamity choice restored** — catch-all for non-standard choices uses `specialEffect: "flag"`
|
||||||
- **Dead `specialEffect === "auto"` branches removed** from chat-reaction.mjs (×2), combat.mjs (×1), reaction-message.hbs
|
- **Dead `specialEffect === "auto"` branches removed** from `chat-reaction.mjs` (×2), `combat.mjs` (×1), `reaction-message.hbs`
|
||||||
- **Deleted `d30-special-choice.hbs` and `_buildSpecialLabel()`**
|
- **Deleted `d30-special-choice.hbs` and `_buildSpecialLabel()`**
|
||||||
- **Dead code audit** — 2 runtime bugs fixed, ~20 dead exports/methods, 33 unused i18n keys, 2 unused templates
|
- **Dead code audit** — 2 runtime bugs fixed, ~20 dead exports/methods, 33 unused i18n keys, 2 unused templates
|
||||||
- **3 critical bugs fixed**: SYSTEM.ROLL_TYPE, SYSTEM.EQUIPMENT_CATEGORIES, missing imports in combat.mjs
|
- **3 critical bugs fixed**: `SYSTEM.ROLL_TYPE`, `SYSTEM.EQUIPMENT_CATEGORIES`, missing imports in `applications/combat.mjs`
|
||||||
- **`isPrimaryController` consolidated** to local function
|
- **`isPrimaryController` consolidated** to local function
|
||||||
- **Aether/Grace deduction merged** via `_deductResourceOnCast()`
|
- **Aether/Grace deduction merged** via `_deductResourceOnCast()`
|
||||||
- **`nextDefenseData` deduped** via `_storeNextDefenseData()`
|
- **`nextDefenseData` deduped** via `_storeNextDefenseData()`
|
||||||
- **`buildDefenseReactionButtons` extracted** from combat.mjs; fixes stale Grit/Luck snapshots
|
- **`buildDefenseReactionButtons` extracted** from `combat.mjs`; fixes stale Grit/Luck snapshots
|
||||||
- **HP HUD toggling extracted** to helpers.mjs
|
- **HP HUD toggling extracted** to `helpers.mjs`
|
||||||
- **`node --check` passes all 55 `.mjs` files**
|
- **`node --check` passes all 55 `.mjs` files**
|
||||||
|
|
||||||
### Pass 5 — Live Verification
|
### Pass 5 — Live Verification
|
||||||
@@ -40,19 +40,27 @@ Fix Grit/Luck defense reaction dialog UX (stacking dialogs, multiple clicks, rev
|
|||||||
- **Defense request dialog verified** — Monster defense dialog with weapon dropdown
|
- **Defense request dialog verified** — Monster defense dialog with weapon dropdown
|
||||||
- **Defense reaction dialog verified** — Luck spent, bonus die added, combat result correct
|
- **Defense reaction dialog verified** — Luck spent, bonus die added, combat result correct
|
||||||
- **AZA→Monster attack flow tested end-to-end**: Club attack (D20=16, D30=6) → Monster defense (D20=1) → defense reactions (Continue) → D30 attack bonus processed (+2, total 18)
|
- **AZA→Monster attack flow tested end-to-end**: Club attack (D20=16, D30=6) → Monster defense (D20=1) → defense reactions (Continue) → D30 attack bonus processed (+2, total 18)
|
||||||
- **BUG FOUND & FIXED: `d30ChangedAttack` infinite loop** — chat-reaction.mjs:452-455 do-while reset block missing `d30ChangedAttack = false`; added at line 456
|
- **BUG FOUND & FIXED: `d30ChangedAttack` infinite loop** — `chat-reaction.mjs:452-455` do-while reset block missing `d30ChangedAttack = false`; added at line 456
|
||||||
- **BUG FIX CONFIRMED**: Re-tested full flow — AZA Club attack (13, D30=12) → defense dialog → Monster defense (2, D30=24) → reaction dialog (only 1 show!) → Continue → "AZA hits Monster!" combat result → damage roll (1d6=2, total 3) → Apply Damage button. No infinite loop. Full E2E success.
|
- **BUG FIX CONFIRMED**: Re-tested full flow — AZA Club attack (13, D30=12) → defense dialog → Monster defense (2, D30=24) → reaction dialog (only 1 show!) → Continue → "AZA hits Monster!" combat result → damage roll (1d6=2, total 3) → Apply Damage button. No infinite loop. Full E2E success.
|
||||||
|
|
||||||
|
### Pass 6 — Cross-Client Mulligan Reroll Fix
|
||||||
|
- **BUG FIX: `handleAttackBoosted` hardcoded `canRerollDefense: false`** — `utils/combat.mjs:142` now computes `canRerollDefense` from `defenseD30message` via `hasD30Reroll()`. Also passes `d30message` to dialog template (was `null`).
|
||||||
|
- **BUG FIX: Missing `defenseRerollContext` in socket data** — added `defenseRerollContext` to `attackBoosted` socket message at `chat-reaction.mjs:773`, and added `rerollDefense` handler in `handleAttackBoosted` at `utils/combat.mjs:203-240` so the mulligan reroll works cross-client.
|
||||||
|
- **BUG FIX: Cross-client mulligan reroll now processes new D30 bonus dice** — after reroll, calls `processD30BonusDice` on the new D30 message to apply bonus dice, flags, and DR multipliers (was silently ignored).
|
||||||
|
- **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.
|
||||||
|
|
||||||
## 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
|
||||||
- **Inline grit/luck deduction uses live actor values**
|
- **Inline grit/luck deduction uses live actor values**
|
||||||
- **Aether/Grace helper uses `costFn` parameter**
|
- **Aether/Grace helper uses `costFn` parameter**
|
||||||
|
- **Cross-client mulligan reroll sends full `defenseRerollContext`** via socket so the defender can re-roll the same configured roll on their client
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
1. Test defense request dialogs (character/monster/save) — more variants
|
1. Test defense request dialogs (character/monster/save) — more variants
|
||||||
2. Test all reaction message variants (shield block/fail, d30Bonus/Flag, grit, luck, etc.)
|
2. Test all reaction message variants (shield block/fail, d30Bonus/Flag, grit, luck, etc.)
|
||||||
3. Create Player user in Foundry for cross-client socket testing
|
3. Create Player user in Foundry for cross-client socket testing (includes mulligan reroll)
|
||||||
4. Prune dead code: unused exports (~20), unused i18n keys (33), unused templates (2)
|
4. Prune dead code: unused exports (~20), unused i18n keys (33), unused templates (2)
|
||||||
|
|
||||||
## Critical Context
|
## Critical Context
|
||||||
@@ -61,18 +69,19 @@ Fix Grit/Luck defense reaction dialog UX (stacking dialogs, multiple clicks, rev
|
|||||||
- **Clicking "Damage" directly bypasses defense** — rolls unapplied damage to chat
|
- **Clicking "Damage" directly bypasses defense** — rolls unapplied damage to chat
|
||||||
- **Same-owner guard** (`chat-reaction.mjs:180-182`) — skips defense when GM owns both, unless `!defenderIsMonster`
|
- **Same-owner guard** (`chat-reaction.mjs:180-182`) — skips defense when GM owns both, unless `!defenderIsMonster`
|
||||||
- **`d30ChangedAttack` infinite loop** — variable wasn't reset in do-while block; fix at `chat-reaction.mjs:456`
|
- **`d30ChangedAttack` infinite loop** — variable wasn't reset in do-while block; fix at `chat-reaction.mjs:456`
|
||||||
|
- **Cross-client mulligan fix**: `handleAttackBoosted` now shows reroll button and handles the reroll action. Requires `defenseRerollContext` in socket data (added).
|
||||||
- **Deserialized weapon object** — `weapon.name` works, `weapon.id` undefined, `weapon._id` works
|
- **Deserialized weapon object** — `weapon.name` works, `weapon.id` undefined, `weapon._id` works
|
||||||
- **Fvtt server**: port 31000, foundrydata-dev
|
- **Fvtt server**: port 31000, foundrydata-dev
|
||||||
- **No player user configured** — cannot test cross-client socket flow
|
- **No player user configured** — cannot test cross-client socket flow
|
||||||
|
|
||||||
## Relevant Files
|
## Relevant Files
|
||||||
- `module/hooks/chat-reaction.mjs` — all 7 hook registrations; defense do-while loop; **d30ChangedAttack fix (line 456)**
|
- `module/hooks/chat-reaction.mjs` — all 7 hook registrations; defense do-while loop; **d30ChangedAttack fix (line 456)**; socket data includes `defenseRerollContext` (line 773)
|
||||||
- `module/utils/combat.mjs` — `buildDefenseReactionButtons`, `_storeNextDefenseData`
|
- `module/utils/combat.mjs` — `buildDefenseReactionButtons`; **`handleAttackBoosted` mulligan fix (lines 142, 154, 227-249)**; imports `hasD30Reroll`
|
||||||
- `module/utils/d30.mjs` — `processD30BonusDice`: auto-roll, flag reporting, no dialog
|
- `module/utils/d30.mjs` — `processD30BonusDice`: auto-roll, flag reporting, no dialog; `hasD30Reroll` checks `type === "mulligan"`
|
||||||
- `module/utils/helpers.mjs` — `_toggleHudWraps`/`_disableHudWraps`
|
- `module/utils/helpers.mjs` — `_toggleHudWraps`/`_disableHudWraps`
|
||||||
- `module/utils.mjs` — barrel re-exporting 23 static methods
|
- `module/utils.mjs` — barrel re-exporting 23 static methods
|
||||||
- `module/models/equipment.mjs` — EQUIPMENT_CATEGORY fix
|
- `module/models/equipment.mjs` — `EQUIPMENT_CATEGORY` fix
|
||||||
- `module/applications/combat.mjs` — added imports
|
- `module/applications/combat.mjs` — added `import { SYSTEM }` and `import { log }`
|
||||||
- `templates/chat/reaction-message.hbs` — d30 removed
|
- `templates/chat/reaction-message.hbs` — `d30Flag` text changed; `d30Auto` branch removed
|
||||||
- `templates/dialogs/d30-special-choice.hbs` — deleted
|
- `templates/dialogs/d30-special-choice.hbs` — deleted
|
||||||
- `lang/en.json` — 33 unused i18n keys remain
|
- `lang/en.json` — 33 unused i18n keys remain
|
||||||
|
|||||||
@@ -771,6 +771,7 @@ Hooks.on("createChatMessage", async (message) => {
|
|||||||
damageTier: damageTier || "standard",
|
damageTier: damageTier || "standard",
|
||||||
attackD30message,
|
attackD30message,
|
||||||
defenseD30message,
|
defenseD30message,
|
||||||
|
defenseRerollContext,
|
||||||
hasShield: !!sData,
|
hasShield: !!sData,
|
||||||
shieldLabel: sData?.label || "",
|
shieldLabel: sData?.label || "",
|
||||||
shieldFormula: sData?.formula || "",
|
shieldFormula: sData?.formula || "",
|
||||||
|
|||||||
+42
-4
@@ -1,6 +1,6 @@
|
|||||||
import { SYSTEM } from "../config/system.mjs"
|
import { SYSTEM } from "../config/system.mjs"
|
||||||
import { log } from "./helpers.mjs"
|
import { log } from "./helpers.mjs"
|
||||||
import { processD30BonusDice } from "./d30.mjs"
|
import { processD30BonusDice, hasD30Reroll } from "./d30.mjs"
|
||||||
|
|
||||||
export async function handleSocketEvent(msg = {}) {
|
export async function handleSocketEvent(msg = {}) {
|
||||||
log(`handleSocketEvent !`, msg)
|
log(`handleSocketEvent !`, msg)
|
||||||
@@ -100,7 +100,7 @@ export async function handleAttackBoosted(msg) {
|
|||||||
attackerHandledBonus, attackRollFinal, defenseRoll, attackWeaponId, attackRollType, attackRollKey,
|
attackerHandledBonus, attackRollFinal, defenseRoll, attackWeaponId, attackRollType, attackRollKey,
|
||||||
shieldDamageReduction: initialShieldDR,
|
shieldDamageReduction: initialShieldDR,
|
||||||
d30Bleed, d30DamageMultiplier, d30DrMultiplier,
|
d30Bleed, d30DamageMultiplier, d30DrMultiplier,
|
||||||
damageTier, attackD30message, defenseD30message,
|
damageTier, attackD30message, defenseD30message, defenseRerollContext,
|
||||||
hasShield, shieldLabel, shieldFormula, shieldDr, canAdHocShield
|
hasShield, shieldLabel, shieldFormula, shieldDr, canAdHocShield
|
||||||
} = msg
|
} = msg
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ export async function handleAttackBoosted(msg) {
|
|||||||
if (defender) {
|
if (defender) {
|
||||||
while (updatedDefenseRoll < attackRollFinal) {
|
while (updatedDefenseRoll < attackRollFinal) {
|
||||||
const shieldData = canShieldReact ? { label: shieldLabel, formula: shieldFormula, damageReduction: shieldDr } : null
|
const shieldData = canShieldReact ? { label: shieldLabel, formula: shieldFormula, damageReduction: shieldDr } : null
|
||||||
const buttons = buildDefenseReactionButtons(defender, { canRerollDefense: false, shieldData, canShieldReact, canAdHocShield: canAdHoc })
|
const buttons = buildDefenseReactionButtons(defender, { canRerollDefense: hasD30Reroll(defenseD30message), shieldData, canShieldReact, canAdHocShield: canAdHoc })
|
||||||
|
|
||||||
const choice = await foundry.applications.api.DialogV2.wait({
|
const choice = await foundry.applications.api.DialogV2.wait({
|
||||||
window: { title: "Defense reactions — attack boosted" },
|
window: { title: "Defense reactions — attack boosted" },
|
||||||
@@ -151,7 +151,7 @@ export async function handleAttackBoosted(msg) {
|
|||||||
defenderName,
|
defenderName,
|
||||||
defenseRoll: updatedDefenseRoll,
|
defenseRoll: updatedDefenseRoll,
|
||||||
defenseStatus: "currently has",
|
defenseStatus: "currently has",
|
||||||
d30message: null,
|
d30message: defenseD30message || null,
|
||||||
offerText: "The attack was boosted! Choose how to improve the defense."
|
offerText: "The attack was boosted! Choose how to improve the defense."
|
||||||
}),
|
}),
|
||||||
buttons,
|
buttons,
|
||||||
@@ -200,6 +200,44 @@ export async function handleAttackBoosted(msg) {
|
|||||||
speaker: ChatMessage.getSpeaker({ actor: defender })
|
speaker: ChatMessage.getSpeaker({ actor: defender })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else if (choice === "rerollDefense" && defenseRerollContext) {
|
||||||
|
const oldDefenseRoll = updatedDefenseRoll
|
||||||
|
const reroll = await rerollConfiguredRoll(defenseRerollContext)
|
||||||
|
if (!reroll) continue
|
||||||
|
updatedDefenseRoll = reroll.options?.rollTotal || reroll.total || oldDefenseRoll
|
||||||
|
let newD30message = reroll.options?.D30message || null
|
||||||
|
const mulliganContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {
|
||||||
|
type: "mulligan",
|
||||||
|
actorName: defenderName,
|
||||||
|
side: "defense",
|
||||||
|
oldRoll: oldDefenseRoll,
|
||||||
|
newRoll: updatedDefenseRoll,
|
||||||
|
diceResults: reroll.options?.diceResults || [],
|
||||||
|
D30result: reroll.options?.D30result,
|
||||||
|
D30message: newD30message
|
||||||
|
})
|
||||||
|
await ChatMessage.create({content: mulliganContent, speaker: ChatMessage.getSpeaker({actor: defender})})
|
||||||
|
// Process new D30 bonus dice from the reroll
|
||||||
|
if (newD30message) {
|
||||||
|
defenseD30message = newD30message
|
||||||
|
const d30Result = await processD30BonusDice(defenseD30message, "defense", null, defender, true)
|
||||||
|
if (d30Result.modifier) {
|
||||||
|
updatedDefenseRoll += d30Result.modifier
|
||||||
|
if (d30Result.modifier > 0) {
|
||||||
|
const rmContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"d30Bonus", actorName:defenderName, value:d30Result.modifier, side:"defense"})
|
||||||
|
await ChatMessage.create({content: rmContent, speaker: ChatMessage.getSpeaker({actor: defender})})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (d30Result.specialEffect === "flag") {
|
||||||
|
const rmContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"d30Flag", actorName:defenderName, specialName:d30Result.specialName || "Special Effect"})
|
||||||
|
await ChatMessage.create({content: rmContent, speaker: ChatMessage.getSpeaker({actor: defender})})
|
||||||
|
}
|
||||||
|
if (d30Result.specialEffect === "drMultiplier") {
|
||||||
|
const rmContent = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/chat/reaction-message.hbs", {type:"d30DRMultiplier", actorName:defenderName, value:d30Result.multiplier})
|
||||||
|
await ChatMessage.create({content: rmContent, speaker: ChatMessage.getSpeaker({actor: defender})})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
} else if (choice === "adHocShield" && canAdHoc) {
|
} else if (choice === "adHocShield" && canAdHoc) {
|
||||||
const adHoc = await promptAdHocShield(defenderName, attackRollFinal, updatedDefenseRoll)
|
const adHoc = await promptAdHocShield(defenderName, attackRollFinal, updatedDefenseRoll)
|
||||||
if (adHoc) {
|
if (adHoc) {
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ export async function processD30BonusDice(d30Message, side, naturalRoll = null,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Bleed type (ranged attacks) — flag for wound creation, same as combo bleed
|
||||||
|
if (d30Message.type === "bleed") {
|
||||||
|
return { modifier: 0, specialEffect: "bleed", specialName: "Bleeding" }
|
||||||
|
}
|
||||||
|
|
||||||
// ── Damage multiplier type (2x/3x damage before DR)
|
// ── Damage multiplier type (2x/3x damage before DR)
|
||||||
if (d30Message.type === "damage_multiplier") {
|
if (d30Message.type === "damage_multiplier") {
|
||||||
return { modifier: 0, specialEffect: "damageMultiplier", specialName: `x${d30Message.multiplier} Damage`, multiplier: d30Message.multiplier }
|
return { modifier: 0, specialEffect: "damageMultiplier", specialName: `x${d30Message.multiplier} Damage`, multiplier: d30Message.multiplier }
|
||||||
|
|||||||
Reference in New Issue
Block a user