# FreeExile Review Round 1: Animation Engine & Turn Inertia Adversarial Audit

> [!WARNING] **Skepticism Disclaimer**
> High confidence in the mathematical coordinate resolution, zero-velocity snap freeze, and headless Node/Canvas test harnesses; moderate confidence in full in-browser GPU presentation across heterogeneous mobile high-DPI displays.

## 1. What the prior attempt got wrong

1. **Out-of-Bounds Atlas Coordinates on Unmapped/Fallback Clips**
   - **Input:** `animState.currentClip = 'nonexistent_test'`
   - **Expected:** Fall back to `'idle'` and render idle frame 0 at `(sx: 0, sy: 0)`.
   - **Actual:** Rendered at `(sx: 160, sy: 768)` (completely outside sprite boundaries).
   - **Root cause:** `targetClipName` was not reassigned to `'idle'` when `manifest.animations[targetClipName]` was missing. The loop `for (const cName of allClips)` never matched `targetClipName`, summing all 33 frames of the entire manifest.

2. **Negative `frameIndex` Produced Negative Canvas Slicing Coordinates**
   - **Input:** `animState.frameIndex = -1`
   - **Expected:** Safe Euclidean modulo wrap to frame 3 (`sx = 480, sy = 0`).
   - **Actual:** `fIdx = -1`, resulting in `sx = -160, sy = -192` passed to `drawImage`, triggering Canvas `IndexSizeError`.
   - **Root cause:** JavaScript `%` operator preserves negative sign (`-1 % 4 === -1`) instead of performing `((x % n) + n) % n`.

3. **Monster Entity Atlas Fallback Leak to Hero Sprite**
   - **Input:** `m.anim.manifestKey = 'mob_feral_hellhound'` with only `hero_anim_atlas` loaded.
   - **Expected:** `drawEntityFrame` returns `false` to permit `entity_renderer.js` to fall back to static monster asset.
   - **Actual:** Fell back to `hero_anim_atlas`, drew human hero frames over monsters, and suppressed static fallback.
   - **Root cause:** `ASSETS[atlasKey] || ASSETS['hero_anim_atlas']` unconditionally fell back to hero atlas for all entity types.

4. **Unhandled TypeError Crash on Manifests Lacking `kinematics`**
   - **Input:** Custom manifest omitting `kinematics` dictionary.
   - **Expected:** Safe default to turn rate 18.0 and base speed 4.2.
   - **Actual:** `TypeError: Cannot read properties of undefined (reading 'turn_rate')`.
   - **Root cause:** Unguarded access `manifest.kinematics.turn_rate`.

5. **Mirrored / Inverted Debug Overlay Text When Facing West**
   - **Input:** `facingSign = -1` with `__DEBUG_ANIM__ = true`.
   - **Expected:** Upright, readable monospace debug telemetry.
   - **Actual:** Text rendered horizontally mirrored and backwards.
   - **Root cause:** Debug overlay rendered inside active `ctx.scale(-1, 1)` transform without compensating scale inversion.

6. **Soft Cap Line Budget Violation**
   - **Input:** `animation_engine.js` line count.
   - **Expected:** `<= 350` lines per `GEMINI.md`.
   - **Actual:** Exceeded soft cap at 351 lines.
   - **Root cause:** Uncompacted multi-line fallback and alias resolution branches.

7. **Missing Test Coverage for Edge Cases & Acceptance Criteria**
   - **Input:** Click-to-move arrival snap freeze, 120Hz joystick boundary alternation, negative indices, and empty frame lists.
   - **Expected:** Verified by unit tests.
   - **Actual:** Prior attempt had zero tests for these conditions.
   - **Root cause:** Shallow coverage limited to standard 6-class happy paths.

## 2. What I changed

- `client/webapp/js/engine/animation_engine.js`:
  - Fixed fallback resolution: resolved missing clip names to `'idle'` (or first available clip) so `targetClipName` matches in grid calculation.
  - Implemented iterative alias resolution (`while rawClip.atlas_clip ...`) to handle alias chains and skip aliased rows cleanly.
  - Replaced JavaScript remainder with Euclidean modulo: `((animState.frameIndex % frames.length) + frames.length) % frames.length`.
  - Added zero-length frames protection returning `false` early.
  - Restricted `hero_anim_atlas` fallback strictly to hero/character classes (`isHeroType`).
  - Added optional chaining / fallback for `manifest.kinematics`.
  - Corrected debug overlay text transform when `facingSign < 0` to prevent mirrored text.
  - Tightened velocity deadzone in `updateAnimation` to `actualVel >= 0.05` to prevent sub-deadzone micro-flutter.
  - Compacted file to exactly 344 lines (strictly `<= 350` soft cap).
- `tests/unit/test_animation_frame_mapping_and_inertia.py`:
  - Added tests for fallback clip coordinates, negative frameIndex wrap, mob atlas isolation, empty frames list, click-to-move destination snap-freeze, 120Hz threshold flutter stability, and missing kinematics safety.
  - Refactored test harness to eliminate duplication and keep total suite size at 286 lines (`<= 350` soft cap).

## 3. Verification Record

- **Deep Verification (ran actual tests):**
  - `pytest tests/unit/test_animation_frame_mapping_and_inertia.py -v`: 37/37 PASSED.
  - `pytest tests/unit/test_animation_pipeline_and_motion_matching.py tests/unit/test_character_animation_and_skills_vfx.py tests/unit/test_m2_animation_and_combat_feel.py tests/e2e/test_poe2_ui_animation_vfx_e2e.py tests/unit/test_challenger_m2_adversarial.py -v`: 167 PASSED, 9 XPASSED (0 failures).
  - `node tests/unit/harness_m2_empirical.mjs`: 5/5 PASSED.
  - `python tools/lint/check_code_and_doc_hygiene.py --strict`: PASSED (0 hard cap violations; `animation_engine.js` is 344 lines, `test_animation_frame_mapping_and_inertia.py` is 286 lines).
- **Shallow Verification (manual only):**
  - None; all edge cases were verified via automated Node.js runtime and pytest suites.
- **Unverified aspects:**
  - Visual fidelity of hardware-accelerated WebGL/Metal texture sampling on Apple Silicon ProMotion displays when devicePixelRatio is fractional (e.g. 2.625x on certain Android viewports).

## 4. Known Issues

- `Minor Robustness Risk`: The debug overlay uses a fixed 10px monospace font; if the canvas is styled via CSS with non-integer scaling, text may appear slightly blurred without high-DPI canvas backing store scaling.

## 5. Remaining risk & next step

- Task is complete. All 3 requirements (R1 Atlas Frame Mapping & Pivot, R2 Turn Inertia Snap Freeze & Click-to-Move, R3 Toggleable Debug Overlay default hidden) are implemented, verified by 37 dedicated unit tests, and fully conformant to 2026 engineering standards and line caps.
