refactor: remove D30 choice dialog, extract defense reaction buttons, fix bugs
Release Creation / build (release) Successful in 45s
Release Creation / build (release) Successful in 45s
- Remove D30 choice dialog — auto-roll bonus dice, flag special effects - Fix d30ChangedAttack infinite loop in defense do-while (missing reset) - Fix chat button dataset attributes (rollType/rollTarget/rollAvantage) - Extract buildDefenseReactionButtons from both defense loops - Merge Aether/Grace deduction via _deductResourceOnCast helper - Extract HP HUD toggling (_toggleHudWraps/_disableHudWraps) - Fix SYSTEM.EQUIPMENT_CATEGORIES typo in equipment model - Add missing imports to combat.mjs - Remove dead d30Auto branches, _buildSpecialLabel, d30-special-choice.hbs
This commit is contained in:
@@ -6,100 +6,73 @@ Fix Grit/Luck defense reaction dialog UX (stacking dialogs, multiple clicks, rev
|
||||
## Accomplished
|
||||
|
||||
### Pass 1 — Critical Issues
|
||||
- **Telemetry removed**: `ClassCounter`, `registerWorldCount`, orphaned `worldKey` setting deleted from system.json
|
||||
- **globalThis side effects**: `globalThis.SYSTEM`, `globalThis.pendingDefenses` moved from top-level to `init` hook
|
||||
- **console.log → log()**: All runtime console.log replaced with `log()` helper guarded by `lethalFantasy.debug` setting
|
||||
- **Stale Tenebris refs**: `macros.mjs` — `TENEBRIS.Label.jet` → `LETHALFANTASY.Label.jet`, `TENEBRIS.Manager.*` → `LETHALFANTASY.Label.*`, `tenebris.macro` flag → `lethalFantasy.macro`
|
||||
- **Telemetry removed** from system.json
|
||||
- **globalThis side effects** moved from top-level to `init` hook
|
||||
- **console.log → log()** helper guarded by setting
|
||||
- **Stale Tenebris refs** → LETHALFANTASY
|
||||
|
||||
### Pass 2 — V1/V2 Mixing, Fire-and-Forget
|
||||
- **V1 sheet registrations removed**: `foundry.appv1.sheets.*` in system.json
|
||||
- **V1 `activateListeners`/jQuery**: removed dead `defaultOptions`, V1 tab code from `combat.mjs`
|
||||
- **V2 API paths**: `FilePicker` → V2, `TextEditor.getDragEventData` → V2, `item.sheet.render(true)` → `render({force:true})`, `super._onRender()` → `super._onRender(context, options)`, `token._id` → `token.id`
|
||||
- **Fire-and-forget Promises**: All `actor.update()`, `ChatMessage.create()`, `prepareRoll()`, `prepareMonsterRoll()`, socket handler calls now awaited
|
||||
- **Misnamed class**: `LethalFantasySkill` → `LethalFantasyWeapon`; added missing `WEAPON_TYPE` import; fixed `weaponCategory`
|
||||
- V1 sheet registrations, activateListeners/jQuery, FilePicker paths fixed
|
||||
- Fire-and-forget Promises now awaited
|
||||
- Misnamed `LethalFantasySkill` → `LethalFantasyWeapon`
|
||||
|
||||
### Pass 3 — Code Review Fixes
|
||||
- **Duplicated dialogs**: Per-element `.rollable`/`.wound-data` bindings moved to `_onRender` (V2 destroys/recreates DOM each render); `_activateListeners` reverted
|
||||
- **renderChatMessage reverted**: V2 hook `renderChatMessage` passes jQuery html, `querySelectorAll` fails; kept `renderChatMessageHTML`
|
||||
- **Roll actions broken**: Fixed `async` base-actor-sheet methods; `_onRender` bindings for rollable elements restored
|
||||
- **Token HUD guard**: `html.querySelector()` → `html.find().length` (html is jQuery object)
|
||||
- **All review awaits confirmed**: `showDefenseRequest`/`socket` handlers all awaited
|
||||
- Duplicated dialogs fixed via `_onRender` bindings
|
||||
- renderChatMessage reverted to HTML hook
|
||||
- All review awaits confirmed
|
||||
|
||||
## Defense Dialog Investigation — Status
|
||||
### Pass 4 — D30 Dialog Removal & Dead Code Audit
|
||||
- **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"`
|
||||
- **Dead `specialEffect === "auto"` branches removed** from chat-reaction.mjs (×2), combat.mjs (×1), reaction-message.hbs
|
||||
- **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
|
||||
- **3 critical bugs fixed**: SYSTEM.ROLL_TYPE, SYSTEM.EQUIPMENT_CATEGORIES, missing imports in combat.mjs
|
||||
- **`isPrimaryController` consolidated** to local function
|
||||
- **Aether/Grace deduction merged** via `_deductResourceOnCast()`
|
||||
- **`nextDefenseData` deduped** via `_storeNextDefenseData()`
|
||||
- **`buildDefenseReactionButtons` extracted** from combat.mjs; fixes stale Grit/Luck snapshots
|
||||
- **HP HUD toggling extracted** to helpers.mjs
|
||||
- **`node --check` passes all 55 `.mjs` files**
|
||||
|
||||
### Symptom (user process)
|
||||
1. Monster (GM) attacks player — hits
|
||||
2. Player uses Grit/Luck to boost defense
|
||||
3. Defense now beats attack — reports new result
|
||||
4. Dialog **stays open** — Grit/Luck/bonus dice options still visible
|
||||
5. Closing dialog (Continue or X) causes "rolls vanish" — reverts to original result
|
||||
### Pass 5 — Live Verification
|
||||
- **D30=30 auto-roll verified** — Club attack shows D30=30 flag
|
||||
- **Defense request dialog verified** — Monster defense dialog with weapon dropdown
|
||||
- **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)
|
||||
- **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.
|
||||
|
||||
### Root Cause Found — Duplicate cross-client processing (FIXED)
|
||||
## Key Decisions
|
||||
- **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
|
||||
- **Inline grit/luck deduction uses live actor values**
|
||||
- **Aether/Grace helper uses `costFn` parameter**
|
||||
|
||||
When monster (GM) attacks player, the `createChatMessage` hook fires on **both** clients:
|
||||
## Next Steps
|
||||
1. Test defense request dialogs (character/monster/save) — more variants
|
||||
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
|
||||
4. Prune dead code: unused exports (~20), unused i18n keys (33), unused templates (2)
|
||||
|
||||
```
|
||||
Player's client: GM's client:
|
||||
defense msg created defense msg synced
|
||||
↓ ↓
|
||||
hook fires (line 557) hook fires (line 557)
|
||||
isPrimaryController(defender)=true isPrimaryController(defender)=false
|
||||
↓ ↓
|
||||
Defense dialog A shows Defense dialog skipped
|
||||
Player spends Grit Cross-client code (line 1009):
|
||||
defenseRoll=10→16 isPrimaryController(attacker)=true
|
||||
While loop exits defenderOwner=player (≠GM)
|
||||
Comparison: "miss" ↓
|
||||
**Sends attackBoosted with ORIGINAL
|
||||
defenseRoll=10 (stale!)**
|
||||
↓
|
||||
Player receives socket → handleAttackBoosted
|
||||
→ Defense dialog B shows with OLD values
|
||||
→ When closed, comparison: "hit" (overwrites!)
|
||||
```
|
||||
## Critical Context
|
||||
- **Chat buttons not interactive via DevTools snapshot** — need JS fallback: `document.querySelectorAll('button').forEach(b => { if (...) b.click(); })`
|
||||
- **Defense flow**: Attack card → target button (`.request-defense-btn`) → defense dialog → defense roll → defense reactions dialog → combat result card with Damage button → damage roll dialog → Apply Damage → HP application
|
||||
- **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`
|
||||
- **`d30ChangedAttack` infinite loop** — variable wasn't reset in do-while block; fix at `chat-reaction.mjs:456`
|
||||
- **Deserialized weapon object** — `weapon.name` works, `weapon.id` undefined, `weapon._id` works
|
||||
- **Fvtt server**: port 31000, foundrydata-dev
|
||||
- **No player user configured** — cannot test cross-client socket flow
|
||||
|
||||
Player sees **two** dialogs (A then B). Dialog B uses unboosted values, so closing/ignoring it produces a stale "hit" result that overwrites the correct "miss."
|
||||
|
||||
### Fix
|
||||
`lethal-fantasy.mjs:1016` — only send `attackBoosted` socket when `attackerHandledBonus || attackerHasNonGMOwner`. Guards against stale-socket overwrite for GM→player combat (where hook-based processing works without socket), while preserving socket delegation for PC→PC cross-client (where `attackerIsCrossClient` suppresses the hook-based processing on the defender's client).
|
||||
|
||||
Before:
|
||||
```js
|
||||
if (defenderOwner && defenderOwner.id !== game.user.id) {
|
||||
game.socket.emit(`system.${SYSTEM.id}`, { type: "attackBoosted", ... })
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
```js
|
||||
if (defenderOwner && defenderOwner.id !== game.user.id) {
|
||||
if (attackerHandledBonus || attackerHasNonGMOwner) {
|
||||
game.socket.emit(`system.${SYSTEM.id}`, { type: "attackBoosted", ... })
|
||||
}
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### Same-Client Path
|
||||
Code pattern is identical between attack and defense dialogs — both use `await DialogV2.wait({rejectClose:false})` in a while loop. Same-client defense works correctly because no duplicate socket messages arrive.
|
||||
|
||||
### Other Findings
|
||||
- `offerGritLuckBonus` (`utils.mjs:1121`) is dead code — never called
|
||||
- `promptCombatBonusDie` (`utils.mjs:975`) is correct — DialogV2 resolves to callback return value, not `action`
|
||||
- Cross-client `handleAttackBoosted` (`utils.mjs:291`) still uses `else if` chain without `continue` — functionally correct but differs from same-client pattern
|
||||
|
||||
### Code Paths
|
||||
| Flow | File | Line |
|
||||
|------|------|------|
|
||||
| Same-client attack | `lethal-fantasy.mjs` | 918-1004 |
|
||||
| Same-client defense | `lethal-fantasy.mjs` | 697-870 |
|
||||
| Cross-client defense | `module/utils.mjs` | 291-445 |
|
||||
| Cross-client socket guard | `lethal-fantasy.mjs` | 1006-1037 |
|
||||
| Attack Grit offer | `module/utils.mjs` | 1210-1290 |
|
||||
|
||||
### Key Files
|
||||
- `lethal-fantasy.mjs` — Main system hooks, same-client attack/defense reactions
|
||||
- `module/utils.mjs` — Cross-client defense flow, bonus dialogs, compareAttackDefense
|
||||
- `module/documents/actor.mjs` — `prepareRoll()` entry point
|
||||
- `module/documents/roll.mjs` — Roll resolution pipeline
|
||||
## Relevant Files
|
||||
- `module/hooks/chat-reaction.mjs` — all 7 hook registrations; defense do-while loop; **d30ChangedAttack fix (line 456)**
|
||||
- `module/utils/combat.mjs` — `buildDefenseReactionButtons`, `_storeNextDefenseData`
|
||||
- `module/utils/d30.mjs` — `processD30BonusDice`: auto-roll, flag reporting, no dialog
|
||||
- `module/utils/helpers.mjs` — `_toggleHudWraps`/`_disableHudWraps`
|
||||
- `module/utils.mjs` — barrel re-exporting 23 static methods
|
||||
- `module/models/equipment.mjs` — EQUIPMENT_CATEGORY fix
|
||||
- `module/applications/combat.mjs` — added imports
|
||||
- `templates/chat/reaction-message.hbs` — d30 removed
|
||||
- `templates/dialogs/d30-special-choice.hbs` — deleted
|
||||
- `lang/en.json` — 33 unused i18n keys remain
|
||||
|
||||
Reference in New Issue
Block a user