# HANDOFF REPORT — DOM & Runtime Exception Remediation Plan

**Agent**: `explorer_18_2` (DOM & Runtime Exception Remediation Explorer)  
**Role**: `investigator`, `planner`  
**Target Recipient**: `orchestrator_18` (`75463099-5538-440a-8ff9-91c183526f7a`) / `worker_18_2`  
**Date**: 2026-10-02T07:11:00Z  
**Type**: Hard Handoff (Investigation & Remediation Formulation Complete)  

---

## 1. Observation

### O1. Defect 1: Uncaught `TypeError` in `canvas_renderer.js:219` upon Dodge i-Frame Expiration
- **Trigger**: User presses `Spacebar` (Huyễn Ảnh Bộ / Dodge Roll), setting `player.isIFrame = true` and `player.iFrameTimer = 0.25`. Exactly 250ms later, `player.iFrameTimer <= 0`.
- **Target File & Code**: `client/webapp/js/engine/canvas_renderer.js`, lines 217–221:
  ```javascript
  217:         if (player.iFrameTimer <= 0) {
  218:           player.isIFrame = false;
  219:           document.getElementById('badge-iframe').classList.add('hidden');
  220:           if (player.animState === 'dodge') player.animState = mag > 0.05 ? 'run' : 'idle';
  221:         }
  ```
- **DOM Inspection**:
  - In `client/webapp/index.html:85`:
    `<div id="badge-iframe" class="hidden px-1 py-0.2 rounded bg-stone-800 text-stone-300 border border-stone-600 font-mono text-[8px]">I-FRAME</div>`
  - In `client/web_pc/index.html`: Element `#badge-iframe` is completely missing.
- **Empirical Failure Verification**:
  - Command: `python .agents/teamwork/challenger_18_1/test_targeted_findings.py`
  - Output verbatim:
    ```
    [TEST W STANDSTILL DRIFT]: 0.0
    [INIT DODGE CHARGES]: 3
    [CHARGES AFTER 1 SPACE PRESS]: 1
    [PAGE ERRORS AFTER IFRAME EXPIRY]: ["Cannot read properties of null (reading 'classList')"]
    ```

### O2. Defect 2: `TypeError: sfxEngine.playWeaponSlash is not a function` in `combat_skills.js:309`
- **Trigger**: User triggers Primary Attack via Key `R` or Right Mouse Button (`RMB`).
- **Target File & Code**: `client/webapp/js/engine/combat_skills.js`, lines 307–310:
  ```javascript
  307:     function doPrimaryAttack() {
  308:       if (player.isDead || player.hp <= 0) return;
  309:       if (typeof sfxEngine !== 'undefined') sfxEngine.playWeaponSlash();
  310:       else if (typeof playAudioFx === 'function') playAudioFx('slash');
  ```
- **Audio Engine Inspection**:
  - In `client/webapp/js/audio/sfx_engine.js:195`:
    The visceral slashing method is declared as `playBladeSlash()`, NOT `playWeaponSlash()`.
  - In `client/webapp/js/audio/sfx_engine.js:405`:
    `window.sfxEngine = sfxEngine;` exports the instance globally.
  - Because `window.sfxEngine` is truthy, `typeof sfxEngine !== 'undefined'` evaluates to `true`, and calling `sfxEngine.playWeaponSlash()` throws an uncaught `TypeError`.
- **Empirical Failure Verification**:
  - Command: `python .agents/teamwork/reviewer_18_2/test_do_primary.py`
  - Output verbatim:
    ```
    doPrimaryAttack evaluate result: TypeError: sfxEngine.playWeaponSlash is not a function
    ```

### O3. Defect 3 (Auxiliary): Double Decrement of Dodge Charges on Spacebar
- **Target File & Code**:
  - `client/webapp/js/engine/combat_skills.js:304`:
    `if (!invoker && window.skillBarController) window.skillBarController.triggerCooldown('dodge', 3.0);`
    `window.doDodge()` decrements `dodge` charges by 1.
  - `client/web_pc/js/pc_input_controller.js:186-196`:
    ```javascript
    if (e.code === 'Space') {
      e.preventDefault();
      if (typeof window.doDodge === 'function') {
        window.doDodge();
      }
      this.triggerUiCooldown('dodge', 3.0);
      return;
    }
    ```
    `this.triggerUiCooldown('dodge', 3.0)` calls `skillBarController.triggerCooldown('dodge', 3.0)` a second time, dropping charges from 3 directly to 1.

---

## 2. Logic Chain

1. **Root Cause 1 (Missing DOM Element in PC Client)**:
   - `canvas_renderer.js` was originally authored for `client/webapp/index.html`, where line 85 declared `<div id="badge-iframe" ...>`.
   - `canvas_renderer.js:219` unconditionally calls `document.getElementById('badge-iframe').classList.add('hidden')` without optional chaining (`?.`).
   - `client/web_pc/index.html` omitted this element. When `renderLoop()` runs 250ms after dodge, `document.getElementById('badge-iframe')` returns `null`.
   - Calling `.classList.add('hidden')` on `null` throws an uncaught `TypeError`, violating the 0 console error requirement and stalling the render loop.
   - **Remediation**: Inserting `<div id="badge-iframe" class="hidden ...">I-FRAME</div>` inside `#hud-char-status-card` in `client/web_pc/index.html` provides the DOM node. When hidden, it occupies 0px; when active, it displays a crisp glowing i-frame indicator.

2. **Root Cause 2 (API Naming Inconsistency Between Shared Modules)**:
   - `combat_skills.js:309` expects `sfxEngine.playWeaponSlash()`.
   - `sfx_engine.js:195` implemented `playBladeSlash()`.
   - Because `client/webapp/` is strictly read-only per Project Directives (to preserve 100% mobile webapp zero-regression), we must NOT touch `combat_skills.js` or `sfx_engine.js`.
   - `client/web_pc/js/pc_main.js` is the dedicated orchestration entry point for the PC desktop client.
   - **Remediation**: Aliasing `playWeaponSlash` on `SFXEngine.prototype`, `sfxEngine`, and `window.sfxEngine` in `client/web_pc/js/pc_main.js` delegates `playWeaponSlash` directly to `this.playBladeSlash()`. This resolves the exception at 0ms cost with zero impact on `client/webapp/`.

3. **Auxiliary Cause 3 (Redundant Cooldown Trigger)**:
   - `window.doDodge()` in `combat_skills.js` already handles `triggerCooldown('dodge', 3.0)`.
   - Calling `triggerUiCooldown('dodge', 3.0)` unconditionally in `pc_input_controller.js:195` consumes an extra charge.
   - **Remediation**: Restricting `this.triggerUiCooldown('dodge', 3.0)` to the fallback branch (`else if (window.player)`) ensures exactly 1 charge is consumed per roll.

---

## 3. Caveats

- **Read-Only Scope Compliance**: As an Explorer agent, no changes were committed to project files. Remediation must be executed by `worker_18_2`.
- **Base HREF & Script Order**: In `client/web_pc/index.html`, `<base href="../webapp/">` is declared in `<head>`. Module scripts (`sfx_engine.js`, `pc_main.js`) execute in document order. The prototype and instance aliasing in `pc_main.js` must execute at module evaluation time so that direct evaluations (like `window.doPrimaryAttack()`) succeed even before any user clicks occur.

---

## 4. Conclusion & Concrete Implementation Diffs

### Remediation Step 1: Add `#badge-iframe` to `client/web_pc/index.html`
**Target File**: `client/web_pc/index.html` (lines 61–65)  
**Location**: Inside `#hud-char-status-card`, within `<div class="flex items-center gap-1.5">` next to `#txt-char-level`.

```diff
--- a/client/web_pc/index.html
+++ b/client/web_pc/index.html
@@ -61,6 +61,7 @@
           <div class="flex items-center gap-1.5">
             <span id="txt-char-name" class="font-sans text-xs font-semibold text-stone-200 tracking-normal" data-i18n="char_name">Lưu Đày</span>
             <span id="txt-char-level" class="text-[10px] font-mono text-amber-400/90 font-bold">Lv.1</span>
+            <div id="badge-iframe" class="hidden px-1.5 py-0.2 rounded bg-amber-500/20 text-amber-300 border border-amber-400/40 font-mono text-[8px] animate-pulse">I-FRAME</div>
           </div>
           <div class="flex items-center gap-1.5 mt-0.5">
```

---

### Remediation Step 2: Establish `sfxEngine.playWeaponSlash` Alias in `client/web_pc/js/pc_main.js`
**Target File**: `client/web_pc/js/pc_main.js`  
**Description**: Import `sfxEngine` and `SFXEngine`, establish prototype and instance alias at module load, and call `this.initAudioAliasing()` inside `PcMain.init()`.

```diff
--- a/client/web_pc/js/pc_main.js
+++ b/client/web_pc/js/pc_main.js
@@ -6,9 +6,23 @@
 import { showTooltip, hideTooltip } from '../../webapp/js/ui/tooltips.js';
 import { initChatUI } from '../../webapp/js/ui/chat_ui.js';
 import { PcHudController } from './pc_hud_controller.js';
+import { sfxEngine, SFXEngine } from '../../webapp/js/audio/sfx_engine.js';
+
+// Setup SFX alias: playWeaponSlash -> playBladeSlash for combat_skills.js primary attack compatibility
+if (typeof SFXEngine !== 'undefined' && SFXEngine.prototype && !SFXEngine.prototype.playWeaponSlash) {
+  SFXEngine.prototype.playWeaponSlash = function(...args) {
+    return this.playBladeSlash(...args);
+  };
+}
+if (sfxEngine && typeof sfxEngine.playWeaponSlash !== 'function') {
+  sfxEngine.playWeaponSlash = function(...args) {
+    return this.playBladeSlash(...args);
+  };
+}
 
 export const PcMain = {
   init() {
+    this.initAudioAliasing();
     this.initPcHud();
     this.initTooltips();
     this.initNavButtons();
@@ -153,6 +167,20 @@
     }
   },
 
+  initAudioAliasing() {
+    if (window.sfxEngine && typeof window.sfxEngine.playWeaponSlash !== 'function') {
+      window.sfxEngine.playWeaponSlash = function(...args) {
+        return (this.playBladeSlash || window.sfxEngine.playBladeSlash).apply(this, args);
+      };
+    }
+    if (window.SFXEngine && window.SFXEngine.prototype && typeof window.SFXEngine.prototype.playWeaponSlash !== 'function') {
+      window.SFXEngine.prototype.playWeaponSlash = function(...args) {
+        return this.playBladeSlash.apply(this, args);
+      };
+    }
+  },
+
   initAudioUnlock() {
     const unlock = () => {
```

---

### Remediation Step 3 (Auxiliary): Fix Dodge Charge Double Decrement in `client/web_pc/js/pc_input_controller.js`
**Target File**: `client/web_pc/js/pc_input_controller.js` (lines 186–197)

```diff
--- a/client/web_pc/js/pc_input_controller.js
+++ b/client/web_pc/js/pc_input_controller.js
@@ -186,13 +186,13 @@
       if (e.code === 'Space') {
         e.preventDefault();
         if (typeof window.doDodge === 'function') {
           window.doDodge();
         } else if (window.player) {
           window.player.isIFrame = true;
           window.player.iFrameTimer = 0.25;
           window.player.animState = 'dodge';
+          this.triggerUiCooldown('dodge', 3.0);
         }
-        this.triggerUiCooldown('dodge', 3.0);
         return;
       }
```

---

### Remediation Step 4: Unit Test Assertion Hardening
**Target File**: `tests/unit/test_pc_web_client.py`  
In `test_index_html_action_hud_and_dual_orbs`, add:
```python
self.assertIn('id="badge-iframe"', content, "Must contain badge-iframe element to prevent canvas_renderer.js null dereference")
```

---

## 5. Verification Method

Once `worker_18_2` applies the changes, run the following verification commands:

```bash
# 1. Verify Primary Attack sfxEngine.playWeaponSlash evaluates cleanly to 'OK':
python .agents/teamwork/reviewer_18_2/test_do_primary.py

# 2. Verify Spacebar dodge i-frame expiry (0 classList errors) and charge retention (leaves 2 charges):
python .agents/teamwork/challenger_18_1/test_targeted_findings.py

# 3. Verify all 10 individual input actions execute with 0 page errors:
python .agents/teamwork/reviewer_18_2/test_each_action.py

# 4. Verify full multi-resolution and gameplay resilience harness:
python .agents/teamwork/challenger_18_2/trace_errors.py

# 5. Run full unit and E2E test suites:
pytest tests/unit/test_pc_input_controller.py tests/unit/test_pc_web_client.py tests/unit/test_pc_web_client_layout.py tests/unit/test_mobile_webapp_config.py -v
pytest tests/e2e/test_pc_desktop_client_e2e.py -v

# 6. Verify Code Hygiene & Strict i18n:
python tools/lint/check_code_and_doc_hygiene.py --strict
python tools/lint/check_i18n_hygiene.py --strict
```

### Invalidation Conditions (Fix Success Criteria):
- `test_do_primary.py` outputs: `doPrimaryAttack evaluate result: OK` (0 TypeErrors).
- `test_targeted_findings.py` outputs: `[PAGE ERRORS AFTER IFRAME EXPIRY]: []` and `[CHARGES AFTER 1 SPACE PRESS]: 2`.
- `test_each_action.py` outputs: `0 errors` across all 10 actions (`click`, `KeyQ`, `KeyW`, `KeyE`, `KeyR`, `Key1`, `Space`, `KeyC`, `KeyI`, `Escape`).
- Zero modifications inside `client/webapp/`.
