# HANDOFF REPORT — Root Cause & Mathematical Fix for UV Overflow Defect

**Author**: `explorer_rem_1` (Explorer Subagent)  
**Parent Agent**: `orchestrator_22` (`34037784-62e1-41f8-bfe6-912696fdec14`)  
**Milestone**: M1 Remediation: Monster & Character Pipeline UV Coordinate Overflow  
**Date**: 2026-10-04  
**Handoff Type**: Hard (Investigation Complete)  
**Artifact Link**: `c:\Projects\FreeExile\.agents\teamwork\explorer_rem_1\analysis.md`

---

## 1. Observation

Direct observations and evidence from static analysis and verification execution:

1. **Defect in `monster_character_pipeline_scaffold.py`**:
   - In `tools/asset_pipeline/monster_character_pipeline_scaffold.py:82-83` and `153-154`:
     ```python
     "textureWidth": 2048, "textureHeight": 2048,
     "texture_width": 2048, "texture_height": 2048,
     ```
   - In line 101 and line 172:
     ```python
     y = (row_idx + (f // 8)) * archetype["frame_height"]
     ```
   - In line 107 and line 178:
     ```python
     "uv": [x / 2048.0, y / 2048.0, (x + archetype["frame_width"]) / 2048.0, (y + archetype["frame_height"]) / 2048.0]
     ```
   - In both `scaffold_monsters` and `scaffold_characters`, `row_idx` starts at 0 and increments by 1 for every `(action, direction)` clip.

2. **Monster Archetype Metrics (10 Manifests)**:
   - File pattern: `client/cocos/assets/resources/monsters/archetypes/*/*_anim_manifest.json`
   - Actions: 6 actions (`idle`, `walk`, `attack`, `hurt`, `death`, `stun`) $\times$ 8 directions = 48 clips (rows $0$ to $47$).
   - Frame dimensions: $160 \times 160\text{ px}$.
   - Total height: $48 \times 160\text{ px} = 7,680\text{ px}$.
   - Max vertical coordinate: $y_{max} + H_f = 7,520 + 160 = 7,680\text{ px}$.
   - Resulting $V$: $v_1 = 7680 / 2048.0 = 3.750000$.
   - Out-of-bounds frame count: **160 out of 216 frames ($74.07\%$)** have $v_1 > 1.0$ (rows 12 to 47).

3. **Exile Character Metrics (6 Manifests)**:
   - File pattern: `client/cocos/assets/resources/characters/*/*_anim_manifest.json`
   - Actions: 10 actions (`idle`, `run`, 5 attacks, `whirlwind`, `dodge`, `hurt`, `death`) $\times$ 8 directions = 80 clips (rows $0$ to $79$).
   - Frame dimensions: $160 \times 192\text{ px}$.
   - Total height: $80 \times 192\text{ px} = 15,360\text{ px}$.
   - Max vertical coordinate: $y_{max} + H_f = 15,168 + 192 = 15,360\text{ px}$.
   - Resulting $V$: $v_1 = 15360 / 2048.0 = 7.500000$.
   - Out-of-bounds frame count: **400 out of 448 frames ($89.29\%$)** have $v_1 > 1.0$ (rows 10 to 79).

4. **Area Geometric Impossibility**:
   - Area of a $2048 \times 2048$ texture: $4,194,304\text{ px}^2$.
   - Area of 216 monster frames ($160 \times 160$): $5,529,600\text{ px}^2$ ($131.83\%$ of texture area).
   - Area of 448 character frames ($160 \times 192$): $13,762,560\text{ px}^2$ ($328.125\%$ of texture area).
   - Result: Fitting all frames into a single $2048 \times 2048$ sheet without overlapping or downscaling is geometrically impossible.

---

## 2. Logic Chain

1. **From Observation 1 and 4**: The scaffolding tool assumed all animations could be stacked 1-row-per-clip while fitting into a declared $2048 \times 2048$ texture. However, the geometric pixel area of the frames strictly exceeds the area of a single $2048 \times 2048$ texture.
2. **From Observation 2 and 3**: Because each clip advances `row_idx` by 1 without resetting within an archetype/character, $y$ reaches $7,520\text{ px}$ for monsters and $15,168\text{ px}$ for characters. Dividing these values by a hardcoded `2048.0` produces UV coordinates up to $3.75$ and $7.50$, causing texture sampling errors in any graphics engine.
3. **Evaluating Solution Approaches**:
   - *Option A (Multi-Sheet Pagination)*: Paginate into multiple $2048 \times 2048$ sheets (4 sheets for monsters, 8 sheets for characters). While each texture remains $\le 2048\text{ px}$, this breaks the single-manifest schema and requires modifying `SpriteAtlasRenderer.ts` in Cocos to handle multi-texture bindings.
   - *Option B (Dynamic Power-of-Two Dimensions)*: Calculate the smallest Power-of-Two height $H_{tex} \ge H_{required}$:
     $$H_{tex} = 2^{\lceil \log_2(N_{rows} \times H_f) \rceil}$$
     - Monsters: $48 \times 160 = 7680 \implies H_{tex} = 8192$ ($2048 \times 8192$).
     - Characters: $80 \times 192 = 15360 \implies H_{tex} = 16384$ ($2048 \times 16384$), or $4096 \times 8192$ with 16 columns.
     - Normalize coordinates dynamically: $u = x / W_{tex}, v = y / H_{tex}$.
     - Results: Max $u_1 \le 0.625 \le 1.0$, Max $v_1 \le 0.9375 \le 1.0$ across 100% of frames.
4. **Conclusion from Steps 1–3**: Option B is the cleanest, mathematically sound, zero-breaking-change fix. It satisfies all engine constraints, Cocos `SpriteAtlasRenderer.ts`, and automated tests without requiring renderer refactoring.

---

## 3. Caveats

- **Mobile WebGL 2.0 Max Texture Dimension**: $2048 \times 16384$ requires 16K texture support. Apple Metal and desktop GPUs support 16K textures natively. If older mobile WebGL devices capping `MAX_TEXTURE_SIZE` at 8192 must be supported, characters can be configured as $4096 \times 8192$ (16 columns, 2 directions per row).
- **Milestone 1 Scope**: Scaffolding manifests and directory templates are in scope for M1. Actual sprite rendering for monsters and characters will take place in subsequent milestones.

---

## 4. Conclusion

**Root Cause**: Hardcoded division by `2048.0` on frame rows that span up to $7,680\text{ px}$ (monsters) and $15,360\text{ px}$ (characters).

**Actionable Fix Plan for Implementer (`worker_rem_1`)**:
1. Add `next_power_of_two(n)` helper in `tools/asset_pipeline/monster_character_pipeline_scaffold.py`.
2. Update `scaffold_monsters()`:
   - Compute `tex_w = next_power_of_two(8 * frame_width)` (2048) and `tex_h = next_power_of_two(total_rows * frame_height)` (8192).
   - Set manifest `textureWidth: tex_w, textureHeight: tex_h`.
   - Calculate UVs dividing by `float(tex_w)` and `float(tex_h)`.
   - Update `pipeline_config.json` target resolution to `[tex_w, tex_h]`.
3. Update `scaffold_characters()`:
   - Compute `tex_w = 2048` and `tex_h = next_power_of_two(total_rows * frame_height)` (16384).
   - Set manifest `textureWidth: tex_w, textureHeight: tex_h`.
   - Calculate UVs dividing by `float(tex_w)` and `float(tex_h)`.
4. Run `python tools/asset_pipeline/monster_character_pipeline_scaffold.py` to regenerate all 16 manifests.
5. Add explicit UV boundedness assertions to `tests/e2e_cocos/test_vfx_texture_atlas_pipeline_e2e.py`.

---

## 5. Verification Method

To independently verify the resolution after implementing the fix:

1. **Automated UV Bounds Check across All 16 Manifests**:
   ```bash
   python -c "
   import json, pathlib
   total_bad = 0
   for cat in ['monsters/archetypes', 'characters']:
       for p in pathlib.Path(f'client/cocos/assets/resources/{cat}').glob('*/*_anim_manifest.json'):
           m = json.load(open(p, encoding='utf-8'))
           bad = [f['uv'] for f in m['frames'].values() if f['uv'][3] > 1.0 or f['uv'][2] > 1.0]
           max_v = max(f['uv'][3] for f in m['frames'].values())
           total_bad += len(bad)
           print(f'{p.parent.name:<25} Bad: {len(bad):<3} Max V: {max_v:.4f}')
   assert total_bad == 0, f'Defect remains: {total_bad} frames out of bounds'
   print('[PASS] All manifests have 0 frames out of bounds. Max V <= 1.0.')
   "
   ```
   *Pass Condition*: `total_bad == 0`, `Max V <= 0.9375`.

2. **Run Pipeline Tests**:
   ```bash
   pytest tests/unit/test_asset_pipeline_tools.py tests/e2e_cocos/test_vfx_texture_atlas_pipeline_e2e.py -v
   ```
   *Pass Condition*: 100% test pass with zero failures.
