# HANDOFF REPORT — ARPG PC Input Controls, Engine Integration & Verification Strategy

**Agent**: `survey_explorer_3`  
**Milestone**: PC Desktop Client (`client/web_pc/`) Architecture Survey (Requirement R2 & R4)  
**Date**: 2026-10-02  
**Target Working Directory**: `c:\Projects\FreeExile\.agents\teamwork\survey_explorer_3\`

---

## 1. Observation

Direct observations from code inspection and tool verification:

### 1.1 Existing Movement Loop & Kinematics (`canvas_renderer.js`)
- **Key Inspection (`client/webapp/js/engine/canvas_renderer.js:42-51`)**:
  ```javascript
  let keyX = 0, keyY = 0;
  const isUp = !!(keys['w'] || keys['arrowup']);
  const isDown = !!(keys['s'] || keys['arrowdown']);
  const isLeft = !!(keys['a'] || keys['arrowleft']);
  const isRight = !!(keys['d'] || keys['arrowright']);
  if (isUp) { keyX -= 1; keyY -= 1; }
  if (isDown) { keyX += 1; keyY += 1; }
  if (isLeft) { keyX -= 1; keyY += 1; }
  if (isRight) { keyX += 1; keyY += 1; }
  ```
  `keys` is read as an undeclared global, resolving to `window.keys` (initialized in `joystick.js:152-163`). In `canvas_renderer.js:78-80`, `if (keyX !== 0 || keyY !== 0 || joystickActive) { if (player.destination) player.destination.active = false; }`.
- **Native Click-to-Move Kinematics (`canvas_renderer.js:84-95`)**:
  ```javascript
  if (mx === 0 && my === 0 && !joystickActive && player.destination && player.destination.active) {
    const destDx = player.destination.wx - player.wx;
    const destDy = player.destination.wy - player.wy;
    const destDist = Math.hypot(destDx, destDy);
    if (destDist > 0.22) {
      mx = destDx / destDist;
      my = destDy / destDist;
    } else {
      player.destination.active = false;
    }
  }
  ```
  When `mx === 0 && my === 0` and `player.destination.active = true`, the engine already computes normalized velocity towards the target, passes it to `resolveMovementWithSliding` (`canvas_renderer.js:137-144`), calculates 8-directional heading (`canvas_renderer.js:153-162`), sets `player.animState = 'run'`, and snaps to `idle` upon reaching within `0.22` world units.

### 1.2 Coordinate Transforms & Raycasting (`iso_math.js`)
- **Screen to World Transform (`client/webapp/js/engine/iso_math.js:154-167`)**:
  ```javascript
  function isoToWorld(screenX, screenY) {
    const cx = viewport.clientWidth / 2;
    const cy = viewport.clientHeight / 2;
    const camX = (typeof camera !== 'undefined' && camera) ? camera.wx : 0;
    const camY = (typeof camera !== 'undefined' && camera) ? camera.wy : 0;
    const sx = screenX - cx;
    const sy = screenY - cy;
    const relWx = (sx / (TILE_W / 2) + sy / (TILE_H / 2)) / 2;
    const relWy = (sy / (TILE_H / 2) - sx / (TILE_W / 2)) / 2;
    return { wx: relWx + camX, wy: relWy + camY };
  }
  ```
  `isoToWorld` directly converts canvas pixel offsets to continuous isometric world units accounting for camera lerp (`camX`, `camY`).
- **Player State Definition (`iso_math.js:201-228`)**: Player object contains `wx`, `wy`, `speed: 5.5`, `isIFrame: false`, `iFrameTimer: 0`, `ghostTrails: []`, `animState`, `facingAngle`, `hp`, `maxHp: 100`, `mana: 50`, `maxMana: 50`.

### 1.3 Combat Skills, Dodge & Potion (`combat_skills.js` & `monster_system.js`)
- **Dodge Roll / Huyễn Ảnh Bộ (`combat_skills.js:276-305`)**:
  - `player.isIFrame = true; player.iFrameTimer = 0.25;` (exactly 0.25s invulnerability window).
  - Animation cancel: cancels active attack windup (`AnimationEngine.cancelAction(player.anim)`).
  - Ghost trails generated in `canvas_renderer.js:207-217` while `player.isIFrame === true`.
  - Cooldown: 3 charges, 3.0s recharge via `window.skillBarController.triggerCooldown('dodge', 3.0)`.
- **Health Potion (`monster_system.js:403-415`)**:
  `drinkHealthPotion()` restores 450 HP, plays chime audio (`sfxEngine.playLootDropChime('rare')`), spawns damage text (`+450 HP` in green), emits particles, and triggers `updatePlayerHpUI()`.
- **Martial Skills (`combat_skills.js:225-270`)**:
  - `doFire()`: Infernal Slash projectile, animation `attack_slash`, fire particles.
  - `doThunder()`: Lightning lance, animation `attack_bolt`, thunder particles.
  - `doFrost()`: Glacial spikes fan, animation `attack_thrust`, frost particles.
  - `doPrimaryAttack()` (`combat_skills.js:307-330`): Melee slash, combo step 1-3, sound, screenshake.
- **Click-to-Move Single Click (`combat_skills.js:442-478`)**: Currently listens to canvas `click` event; sets `player.destination = { wx, wy, active: true }` and calls `spawnClickRipple(wx, wy)`. Lacks continuous mouse drag (Hold-to-Move).

### 1.4 Skill Bar & Cooldown Controller (`skill_bar_controller.js`)
- **Keybinding Conflict Observed (`skill_bar_controller.js:307`)**:
  `(slotKey && slotKey !== 'W' && slotKey === pressedKey)`
  `W` was specifically blocked from skill activation because mobile/web simulator mapped `W` to move-up! On PC ARPG, `W` must be mapped to Skill 2.

### 1.5 Viewport & Mobile Framework (`index.html`)
- `client/webapp/index.html:60`: `<div id="app-viewport" class="iphone-frame relative overflow-hidden flex flex-col bg-slate-950">`
- `client/webapp/index.html:18`: `<aside id="simulator-bar" class="hidden md:flex ...">`
- `client/webapp/index.html:130`: `<div id="joystick-zone" ...>`
- Script loading in `index.html:221-229`: `joystick.js` is loaded statically; all other modules are modular and decoupled.

### 1.6 Hygiene Verification
- Ran `python tools/lint/check_code_and_doc_hygiene.py`: **0 Hard Cap violations** across the entire codebase.

---

## 2. Logic Chain

```
[Observation 1.1: canvas_renderer.js has native Click-to-Move kinematics when keyX=0, keyY=0]
                                │
                                ▼
[Step 1: Omit joystick.js in client/web_pc/ -> replace with dedicated pc_input_controller.js]
                                │
                                ▼
[Step 2: Initialize window.keys = {} with enableWasdMovement = false]
                                │
                                ├─► Frees key 'W' from movement -> mapped to Martial Skill 2
                                └─► Ensures mx=0, my=0 from keyboard -> Click-to-Move drives 100% of movement
                                │
                                ▼
[Observation 1.2: isoToWorld converts screen coordinates to world coordinates]
                                │
                                ▼
[Step 3: Implement Pointer Events on #game-canvas in pc_input_controller.js]
                                │
                                ├─► LMB Down/Move/Up:
                                │     - Down: raycast to world -> set player.destination + spawnClickRipple
                                │     - Move (while isLmbHeld): continuously update player.destination (Hold-to-Move)
                                │     - Up: isLmbHeld = false
                                │
                                ├─► RMB Down:
                                │     - contextmenu e.preventDefault()
                                │     - Rotate player to face cursor: player.facingAngle = Math.atan2(dy, dx)
                                │     - Trigger Primary Martial Skill towards cursor
                                │
                                ├─► Spacebar:
                                │     - Calls doDodge() -> 0.25s i-frame, cancels attack windup, ghost trails
                                │
                                ├─► Q, W, E, R:
                                │     - Rotate player towards cursor, cast skill towards mouseWorld
                                │     - Trigger SkillBarController cooldown overlay
                                │
                                └─► 1, 2, 3, 4, 5 (Survival Flasks):
                                      - 1: Health Flask (+450 HP / instant heal)
                                      - 2: Mana Flask (+50 Qi / recovery)
                                      - 3: Quicksilver Flask (+35% move speed for 4s)
                                      - 4: Granite Flask (50% damage reduction for 4s)
                                      - 5: Diamond Flask (+30% crit chance / 1.5x damage for 4s)
                                │
                                ▼
[Observation 1.5: client/webapp reusability without modification]
                                │
                                ▼
[Step 4: client/web_pc/index.html loads shared engines via relative paths]
                                │
                                └─► ZERO mobile regression guaranteed; client/webapp remains 100% untouched
                                │
                                ▼
[Step 5: Verification Suite (Playwright Headless + Pytest Unit + DevTools MCP)]
                                │
                                └─► Validates 100vw/100vh layout, zero scrollbars, zero mobile elements,
                                    Click-to-Move, dodge i-frame, QWER, 1-5 flasks, modals, reactive i18n
```

---

## 3. Caveats

1. **Input Focus Exclusion**: When the user is typing in `<input id="chat-input">` or any modal input, all keyboard shortcuts (`Q`, `W`, `E`, `R`, `1-5`, `Space`, `I`, `C`, `Tab`, `Esc` closing) must be immediately bypassed to prevent accidental skill casting during text messaging.
2. **Browser Context Menu**: Browser native right-click menu must be suppressed on `#game-canvas` and the entire `#app-viewport` via `window.addEventListener('contextmenu', e => e.preventDefault())`.
3. **Complex Pathfinding**: `canvas_renderer.js` currently uses line-of-sight kinematics with `resolveMovementWithSliding` for wall sliding. For multi-room dungeon navigation around deep concavities, `grid_pathfinder.js` (`GridPathfinder`) is already available in the codebase and can be connected if multi-waypoint path following is desired.
4. **Resolution Scaling**: On 21:9 Ultrawide monitors (e.g. 3440×1440), `isoToWorld` and `updateCamera` smoothly scale because `iso_math.js` dynamically queries `viewport.clientWidth` and `viewport.clientHeight`.

---

## 4. Conclusion

1. **Dedicated PC Controller Design (`client/web_pc/js/pc_input_controller.js`)**:
   - Manages mouse tracking (`cursorScreen`, `cursorWorld`), continuous LMB hold-to-move, RMB primary attack, QWER skills, 1-5 survival flasks, Spacebar dodge, and UI toggles (`I`, `C`, `Esc`, `Tab`/`M`).
   - Replaces `joystick.js` completely in `client/web_pc/index.html`.
   - File length: strictly <= 320 lines (complies with <= 350 lines soft cap).
2. **PC Keybinding Matrix**:
   | Control | Action | Function / Target | Cooldown / Mechanism |
   |:---|:---|:---|:---|
   | **LMB (Click)** | Click-to-Move | `player.destination = {wx, wy, active: true}` | Direct Kinematics + `spawnClickRipple` |
   | **LMB (Hold)** | Hold-to-Move | Updates `player.destination` every mousemove | Continuous steering without click spam |
   | **RMB** | Primary Martial Skill | Faces cursor, triggers primary attack | Directional cast towards `cursorWorld` |
   | **Q** | Martial Skill 1 | Liệt Huyết Cuồng Trảm (`doFire()`) | 1.5s Cooldown sweep |
   | **W** | Martial Skill 2 | Hộ Thể Cương Khí / Lôi Kích (`doThunder()`) | 3.0s Cooldown sweep (WASD decoupled) |
   | **E** | Martial Skill 3 | Cửu Tiêu Lôi Kiếm / Hàn Độc (`doFrost()`) | 4.0s Cooldown sweep |
   | **R** | Martial Skill 4 | Ultimate Martial Strike | 5.0s Cooldown sweep |
   | **1** | Life Flask | `drinkHealthPotion()` | 450 HP instant recovery, 4.0s cd |
   | **2** | Mana Flask | `drinkManaPotion()` | +50 Qi restoration, 4.0s cd |
   | **3** | Quicksilver Flask | Speed Boost (`player.speed *= 1.35`) | +35% move speed for 4s, 6.0s cd |
   | **4** | Granite Flask | Iron Skin (`player.defenseBuff = 0.5`) | 50% damage reduction for 4s, 6.0s cd |
   | **5** | Diamond Flask | Crit Surge (`player.damageMultiplier = 1.5`) | +30% crit / 1.5x damage for 4s, 6.0s cd |
   | **Space** | Huyễn Ảnh Bộ | `doDodge()` | 0.25s i-frame, cancel attack, 3 charges |
   | **I / B** | Inventory Stash | Toggle `#modal-bag` | Modal open/close |
   | **C** | Character Sheet | Toggle `#modal-character` | Modal open/close |
   | **Tab / M** | World Map | Toggle `#modal-worldmap` | Modal open/close |
   | **Esc** | Pause / Settings | Closes open modals OR toggles `#overlay-pause` | `window.toggleGamePause()` |

3. **Zero Mobile Regression Guarantee**:
   All new files reside in `client/web_pc/`. No lines modified in `client/webapp/`.

---

## 5. Verification Method

### 5.1 Automated Pytest Unit Test Suite (`tests/unit/test_pc_input_controller.py`)
Run command:
```bash
python -m pytest tests/unit/test_pc_input_controller.py -v
```
**Test Coverage**:
- Verify key mapping structure (`QWER`, `1-5`, `Space`, `LMB`, `RMB`).
- Verify mathematical round-trip consistency: `isoToWorld(worldToIso(wx, wy)) == (wx, wy)`.
- Verify Click-to-Move destination kinematics distance formula and arrival threshold (`<= 0.22`).
- Verify Dodge properties: `isIFrame = true`, `iFrameTimer = 0.25`, attack cancel.
- Verify Flask properties: heal calculation, mana recovery, buff duration.
- Verify Code Hygiene: `< 350 lines` soft cap.

### 5.2 Automated Playwright E2E Test Suite (`tests/e2e/test_pc_desktop_client_e2e.py`)
Run command:
```bash
python -m pytest tests/e2e/test_pc_desktop_client_e2e.py -v
```
**Test Cases**:
1. `test_fullscreen_canvas_and_zero_mobile_elements`:
   - Checks client renders at 100vw × 100vh with `overflow: hidden` (zero scrollbars).
   - Confirms `.iphone-frame`, `#simulator-bar`, and `#joystick-zone` do not exist in DOM.
2. `test_click_to_move_mechanic`:
   - Dispatches mouse click at canvas `(cx + 80, cy + 40)`.
   - Asserts `player.destination.active === true` and player position moves towards target.
3. `test_hold_to_move_continuous_drag`:
   - Dispatches `pointerdown` -> `pointermove` across 3 points.
   - Verifies destination updates smoothly following cursor.
4. `test_spacebar_huyen_anh_bo_dodge`:
   - Simulates `Space` press.
   - Asserts `player.isIFrame === true`, `player.animState === 'dodge'`, and cooldown triggered.
5. `test_qwer_martial_skills_and_w_isolation`:
   - Simulates `W` press -> verifies skill activation and verifies `player.dirX === 0 && player.dirY === 0` (W does NOT cause movement).
   - Simulates `Q`, `E`, `R` -> verifies cooldown overlays.
6. `test_1_to_5_survival_flasks`:
   - Simulates keys 1 through 5 -> verifies HP, Mana, and buff states.
7. `test_ui_modal_hotkeys`:
   - Presses `I`, `C`, `Esc`, `Tab` -> verifies modal open/close transitions.
8. `test_reactive_i18n_without_page_reload`:
   - Changes locale via `FreeExileI18n.setLocale('en')` -> verifies HUD text changes in-place.
   - Asserts `window.__sessionNavCount === 1` (zero page reloads).
9. `test_zero_console_errors`:
   - Listens to `pageerror` and `console.error` -> asserts 0 unhandled errors.
10. `test_mobile_regression_guard`:
   - Loads `http://127.0.0.1:<port>/webapp/index.html` -> asserts mobile simulator remains green.

### 5.3 Invalidation Conditions
- Any code edit made to `client/webapp/` files.
- Pressing `W` causing the player character to walk upwards instead of casting a martial skill.
- Canvas introducing scrollbars or overflow on any desktop aspect ratio (16:9, 16:10, 21:9).
- Browser right-click spawning the native context menu over the game canvas.
