# Handoff Report: Visceral Dodge Roll (Huyễn Ảnh Bộ), Evasion i-Frame & Monster Attack Synchronization (M4.2)

> **Agent**: `explorer_m4_2_gen2` (teamwork_preview_explorer)  
> **Milestone**: Milestone 4 (Visceral Telegraphing, Evasion i-Frame & Leashing)  
> **Target Subsystems**: `client/webapp/js/engine/combat_skills.js`, `client/webapp/js/engine/monster_system.js`, `server/world/combat_engine.py`, `client/src/combat/CombatController.ts`, `client/webapp/js/engine/canvas_renderer.js`  
> **Status**: Complete Investigation & Actionable Technical Blueprint  

---

## 1. Observation

### 1.1 Requirements & Acceptance Criteria
From `c:\Projects\FreeExile\.agents\teamwork\ORIGINAL_REQUEST.md` § R3 & Acceptance Criteria:
- **R3**: *"Equip elite monsters and bosses with visible attack telegraph cones/rings and windup delays, enabling players to execute Huyễn Ảnh Bộ (0.25s i-frame dodge roll) to evade lethal strikes."*
- **Acceptance Criteria**: *"Player dodge roll (Huyễn Ảnh Bộ) provides invulnerability frames (i-frames) against monster attacks during the dodge window."*
- Strict code hygiene standard: *"All code modifications pass the strict hygiene gate (`python tools/lint/check_code_and_doc_hygiene.py --strict`)."*

### 1.2 Dodge Roll & i-Frame Mechanics in `client/webapp/js/engine/combat_skills.js`
In `client/webapp/js/engine/combat_skills.js`:
- Lines 281–298:
  ```javascript
  var dodgeCharges = 3;
  window.dodgeCharges = dodgeCharges;

  function doDodge() {
    if (typeof sfxEngine !== 'undefined') sfxEngine.playDodgeWhoosh();
    else playAudioFx('dodge');
    triggerHaptic([35]);
    player.isIFrame = true;
    player.iFrameTimer = 0.25;
    player.animState = 'dodge';
    if (typeof AnimationEngine !== 'undefined' && player.anim) {
      AnimationEngine.playAction(player.anim, 'dodge', { actionSpeed: 1.6 });
    }
    document.getElementById('badge-iframe')?.classList.remove('hidden');
    triggerScreenShake(2.5, 0.12);
    spawnParticles('phantom', player.wx, player.wy, 45);
    if (window.skillBarController) window.skillBarController.triggerCooldown('dodge', 3.0);
  }
  ```
- Lines 343 & 355:
  ```javascript
  if (k === ' ' || k === 'space' || k === 'shift') doDodge();
  ...
  document.getElementById('skill-dodge')?.addEventListener('click', doDodge);
  ```
- **Direct Deficiencies Observed**:
  1. **Zero Cooldown / Charge Gating on Keypress**: `doDodge()` does NOT check `window.skillBarController.isSlotOnCooldown('dodge')` nor `player.isIFrame`. A player spamming `Space` can reset `iFrameTimer` indefinitely and bypass the 3-charge limit.
  2. **Double Cooldown / Charge Deduction Risk on UI Click**: In `client/webapp/js/ui/skill_bar_controller.js`:
     ```javascript
     activateSlot(slotId) {
       ...
       if (typeof slot.onActivate === 'function') slot.onActivate(slot);
       if (slot.charges !== null) this.triggerCooldown(slotId, slot.rechargeDuration);
     }
     ```
     `activateSlot` calls `slot.onActivate(slot)` (which calls `doDodge()`). Inside `doDodge()`, line 297 unconditionally calls `skillBarController.triggerCooldown('dodge', 3.0);`. Then `activateSlot` calls `triggerCooldown` a second time, immediately deducting 2 charges per single click!
  3. **Strict Line Cap Status**: `combat_skills.js` currently has **495 lines** (Hard Cap is **500 lines**!). Any added lines without consolidation will breach the hard cap and fail the hygiene gate.

### 1.3 Monster Attack Execution in `client/webapp/js/engine/monster_system.js`
In `client/webapp/js/engine/monster_system.js`:
- Lines 237–241 (`updateMonstersTick`):
  ```javascript
  } else if (m.attackCooldown <= 0) {
    // Attack strike on player!
    m.attackCooldown = 1.75 + Math.random() * 0.5;
    executeMonsterAttackOnPlayer(m);
  }
  ```
  *Deficiency*: Hostile mobs strike with **zero windup delay**, making it impossible for human players to react and time a 0.25s dodge roll without visual telegraphing.
- Lines 284–300 (`executeMonsterAttackOnPlayer`):
  ```javascript
  // Monster strikes player with PoE2 i-frame evasion check
  function executeMonsterAttackOnPlayer(m) {
    if (typeof AnimationEngine !== 'undefined' && m.anim) {
      AnimationEngine.playAction(m.anim, 'attack', { actionSpeed: 1.25 });
    }

    if (player.isIFrame) {
      // Evasion success!
      if (typeof sfxEngine !== 'undefined') sfxEngine.playDodgeWhoosh();
      else if (typeof playAudioFx === 'function') playAudioFx('dodge');
      const pPos = worldToIso(player.wx, player.wy);
      if (typeof spawnDamageText === 'function') {
        spawnDamageText('NÉ ĐÒN! (i-frame)', pPos.x, pPos.y - 45, '#38bdf8', false);
      }
      return;
    }
  ```
- **Direct Deficiencies Observed**:
  1. The i-frame check only inspects `if (player.isIFrame)`. If `player.isIFrame` is desynchronized or truthy only while `iFrameTimer > 0`, checking `if (player.isIFrame === true || (player.iFrameTimer && player.iFrameTimer > 0))` is strictly required.
  2. Missing evasion particles: While floating text `"NÉ ĐÒN! (i-frame)"` (#38bdf8) and `playDodgeWhoosh()` are present, no phantom particles are emitted on successful evasion.
  3. **Strict Line Cap Status**: `monster_system.js` currently has **496 lines** (Hard Cap is **500 lines**!).

### 1.4 i-Frame Lifecycle & Ghost Trails in `client/webapp/js/engine/canvas_renderer.js`
In `client/webapp/js/engine/canvas_renderer.js`:
- Lines 113, 165: `player.speed * (player.isIFrame ? 1.75 : 1.0)` provides a 1.75x velocity boost during dodge roll.
- Lines 176–191:
  ```javascript
  if (player.isIFrame) {
    player.iFrameTimer -= dt;
    const pPos = worldToIso(player.wx, player.wy);
    player.ghostTrails.push({
      x: pPos.x,
      y: pPos.y,
      facing: player.facing,
      alpha: 0.7,
      weapon: player.currentWeapon
    });
    if (player.iFrameTimer <= 0) {
      player.isIFrame = false;
      document.getElementById('badge-iframe').classList.add('hidden');
      if (player.animState === 'dodge') player.animState = mag > 0.05 ? 'run' : 'idle';
    }
  }
  ```
- In `client/webapp/js/engine/entity_renderer.js` lines 30–43: `player.ghostTrails` are rendered with fading opacity (`alpha -= dt * 2.5`), drawing shaded silhouette sprites.

### 1.5 Server-Authoritative Evasion in `server/world/combat_engine.py`
In `server/world/combat_engine.py`:
- Lines 23–24 (`CombatActor`):
  ```python
  last_evasion_timestamp_ms: int = 0
  evasion_iframe_duration_ms: int = 250  # 0.25s i-frame window
  ```
- Lines 55–61:
  ```python
  def trigger_phantom_evasion(self, actor_id: int, timestamp_ms: int) -> bool:
      """Triggers Huyễn Ảnh Bộ, entering the 250ms i-frame invulnerable state."""
      actor = self.actors.get(actor_id)
      if not actor:
          return False
      actor.last_evasion_timestamp_ms = timestamp_ms
      return True
  ```
- Lines 86–98 (`calculate_damage`):
  ```python
  elapsed_evasion = current_timestamp_ms - defender.last_evasion_timestamp_ms
  if 0 <= elapsed_evasion <= defender.evasion_iframe_duration_ms:
      return DamageEventResult(
          attacker_id=attacker_id,
          defender_id=defender_id,
          raw_damage=raw_damage,
          final_damage=0.0,
          is_critical=False,
          is_evaded=True,
          element=damage_element
      )
  ```
- Line 72 in `client/src/combat/CombatController.ts` (iOS Metal Client):
  ```typescript
  return currentTimestampMs - this.lastEvasionTimestampMs <= this.evasionIframeDurationMs; // 250ms
  ```
- **Direct Parity Assessment**: The server, client webapp, and iOS client all enforce the exact same **250ms (0.25s)** evasion window.

---

## 2. Logic Chain

```
[Observation 1.1 & 1.2: Unchecked Dodge Execution & Double-Deduction]
  │
  ├─► Spacebar press directly calls doDodge() without verifying if dodge is on cooldown or if player is already rolling
  │   => Result: Players can spam infinite i-frames, breaking combat risk/reward.
  │
  ├─► When clicked on UI, SkillBarController.activateSlot() calls onActivate(slot) AND triggerCooldown()
  │   => Result: doDodge() calling triggerCooldown() internally causes a double-spending bug (2 charges per roll).
  │
  └─► Solution: In doDodge(invoker):
        1. Guard: if (player.isIFrame || (window.skillBarController && window.skillBarController.isSlotOnCooldown('dodge'))) return;
        2. Deduct only if called standalone: if (!invoker && window.skillBarController) window.skillBarController.triggerCooldown('dodge', 3.0);

[Observation 1.3: Monster Attack Evasion Check & Zero Windup]
  │
  ├─► executeMonsterAttackOnPlayer only checks player.isIFrame
  │   => Guard against frame desync: check (player.isIFrame === true || (player.iFrameTimer && player.iFrameTimer > 0)).
  │   => On true: negate damage (0 dmg), spawn 'NÉ ĐÒN! (i-frame)' (#38bdf8), play dodge SFX, spawn phantom particles.
  │
  └─► Mobs attack at 0s windup when attackCooldown <= 0
      => Integrate with TelegraphRenderer: if (window.TelegraphRenderer?.startMonsterAttack)
         start telegraph (0.75s–1.0s windup) and call executeMonsterAttackOnPlayer on completion!
         If monster is staggered (poise broken), telegraph is aborted and player takes 0 damage.

[Observation 1.2 & 1.3: Hard Cap 500 Lines Hygiene Constraint]
  │
  ├─► combat_skills.js is at 495 lines; monster_system.js is at 496 lines.
  │
  ├─► In combat_skills.js: Replace 16 individual window.* exports (lines 480-496)
  │   with a concise Object.assign(window, {...}) -> Net savings: 11 lines (495 -> 484 lines).
  │
  └─► In monster_system.js: Apply explorer_m4_3's 7-point consolidation plan
      (player init, click ripple, revival, godmode, notice, spawnQAEnemy, window exports)
      -> Net savings: ~40 lines (496 -> ~468 lines). Both files comfortably pass the hard cap.
```

---

## 3. Caveats

1. **Telegraph Module Availability**: `window.TelegraphRenderer` is being delivered by `explorer_m4_1` in `client/webapp/js/engine/telegraph_renderer.js`. The attack initiation in `monster_system.js` must safely check `typeof window.TelegraphRenderer !== 'undefined' && typeof window.TelegraphRenderer.startMonsterAttack === 'function'`, falling back to immediate `executeMonsterAttackOnPlayer(m)` if the telegraph script has not yet loaded.
2. **Audio Failsafes**: `sfxEngine.playDodgeWhoosh()` and `playAudioFx('dodge')` are wrapped with `typeof` checks to ensure zero exceptions if WebAudio context is suspended or running headless in automated test harnesses.
3. **Poise Stagger Interaction**: If the player hits the monster during its attack windup and depletes its poise, `explorer_m4_3`'s poise break logic sets `m.staggerTimer = 4.0;`, and `TelegraphRenderer.cancelTelegraph(m.id)` cancels the pending strike. The downstream worker must ensure both changes work in harmony.

---

## 4. Conclusion & Actionable Blueprint

### 4.1 Concrete Recommendations for Downstream Worker

#### Recommendation 1: Update `client/webapp/js/engine/combat_skills.js`
1. Update `doDodge(invoker)`:
   ```javascript
   function doDodge(invoker) {
     if (player.isIFrame || (window.skillBarController && window.skillBarController.isSlotOnCooldown('dodge'))) return;
     if (typeof sfxEngine !== 'undefined') sfxEngine.playDodgeWhoosh();
     else playAudioFx('dodge');
     triggerHaptic([35]);
     player.isIFrame = true;
     player.iFrameTimer = 0.25;
     player.animState = 'dodge';
     if (typeof AnimationEngine !== 'undefined' && player.anim) {
       AnimationEngine.playAction(player.anim, 'dodge', { actionSpeed: 1.6 });
     }
     document.getElementById('badge-iframe')?.classList.remove('hidden');
     triggerScreenShake(2.5, 0.12);
     spawnParticles('phantom', player.wx, player.wy, 45);
     if (!invoker && window.skillBarController) window.skillBarController.triggerCooldown('dodge', 3.0);
   }
   ```
2. Consolidate `window.*` exports at lines 480–496 using `Object.assign(window, { ... })` to reduce line count from 495 to 484.

#### Recommendation 2: Update `client/webapp/js/engine/monster_system.js`
1. In `updateMonstersTick` (lines 237–241), trigger telegraphed attack if available:
   ```javascript
   } else if (m.attackCooldown <= 0) {
     m.attackCooldown = (m.isBoss ? 2.5 : 1.75) + Math.random() * 0.5;
     if (typeof window.TelegraphRenderer !== 'undefined' && typeof window.TelegraphRenderer.startMonsterAttack === 'function') {
       window.TelegraphRenderer.startMonsterAttack(m, player, executeMonsterAttackOnPlayer);
     } else {
       executeMonsterAttackOnPlayer(m);
     }
   }
   ```
2. In `executeMonsterAttackOnPlayer(m)` (lines 290–300):
   ```javascript
   if (player.isIFrame === true || (player.iFrameTimer && player.iFrameTimer > 0)) {
     if (typeof sfxEngine !== 'undefined') sfxEngine.playDodgeWhoosh();
     else if (typeof playAudioFx === 'function') playAudioFx('dodge');
     const pPos = worldToIso(player.wx, player.wy);
     if (typeof spawnDamageText === 'function') {
       spawnDamageText('NÉ ĐÒN! (i-frame)', pPos.x, pPos.y - 45, '#38bdf8', false);
     }
     if (typeof spawnParticles === 'function') spawnParticles('phantom', player.wx, player.wy, 16);
     return;
   }
   ```
3. Execute the 40-line consolidation plan documented by `explorer_m4_3` to bring `monster_system.js` from 496 down to ~468 lines.

#### Recommendation 3: Add Dedicated Unit Test Suite
Deploy `tests/unit/test_dodge_and_evasion_iframe.py` (authored and verified in `proposed_test_dodge_and_evasion_iframe.py`), covering 16 test cases across client and server evasion contracts, boundary math, and line caps.

---

## 5. Verification Method

### 5.1 Automated Unit & E2E Test Execution
Run the following test commands to independently verify all evasion behaviors:

```bash
# 1. Run proposed dedicated Dodge & i-Frame unit test suite (16 tests, 100% PASS)
python .agents/teamwork/explorer_m4_2_gen2/proposed_test_dodge_and_evasion_iframe.py

# 2. Run PoE2 Zone & Encounter E2E test suite (specifically Area 4 Dodge i-frame tests)
pytest tests/e2e/test_poe2_zone_and_encounter_e2e.py -k "dodge" -v

# 3. Run PoE2 UI & Combat Feel E2E test suite (Area 7 Dodge i-frame & charges)
pytest tests/e2e/test_poe2_ui_animation_vfx_e2e.py -k "dodge" -v

# 4. Strict Code & Document Hygiene Audit (verifies <= 500 lines hard cap)
python tools/lint/check_code_and_doc_hygiene.py --strict
```

### 5.2 Files to Inspect
- `c:\Projects\FreeExile\.agents\teamwork\explorer_m4_2_gen2\dodge_iframe_changes.patch`: Ready-to-apply diff patch.
- `c:\Projects\FreeExile\.agents\teamwork\explorer_m4_2_gen2\proposed_test_dodge_and_evasion_iframe.py`: Complete executable test suite.
- `client/webapp/js/engine/combat_skills.js`: Check line count <= 500 (target ~484).
- `client/webapp/js/engine/monster_system.js`: Check line count <= 500 (target ~468).

### 5.3 Invalidation Conditions
This investigation report is invalidated if:
1. `combat_skills.js` exceeds 500 lines or removes `player.isIFrame = true;` / `player.iFrameTimer = 0.25;`.
2. `monster_system.js` fails to negate damage when `player.isIFrame === true`.
3. Server `CombatActor.evasion_iframe_duration_ms` deviates from `250`.
