# Handoff Report: Client-Side Tile-Level Collision & 2-Axis Sliding Physics

**Sender**: `explorer_m3_1` (teamwork_preview_explorer)  
**Recipient**: `1cc48fc5-ce57-4f48-8964-24cab4bfcacc` (orchestrator_11) / `worker_m3_1`  
**Working Directory**: `c:\Projects\FreeExile\.agents\teamwork\explorer_m3_1`  
**Date**: 2026-10-01  
**Handoff Type**: Hard (Task Complete)  

---

## 1. Observation

1. **Existing Collision Engine**:
   - `client/webapp/js/engine/collision_engine.js` (218 lines total).
   - In `isPositionBlocked(wx, wy, radius = 0.35, isDodge = false)` (lines 88–141), collision only checks `getCurrentZoneBounds()` (lines 92–95), a hardcoded sanctuary water hazard `Math.hypot(wx - 5.0, wy - (-3.0)) < (2.4 + radius)` (lines 99–104), and `mapProps` footprints (lines 107–138).
   - `collision_engine.js` has **zero** integration with `window.currentMapGrid` or `window.getTileAt(tx, ty)`.
   - In `resolveMovementWithSliding` (lines 148–179), every single frame returns a freshly allocated object literal `{ wx: ..., wy: ..., isSliding: ... }` at lines 154, 164, 166, 169, 171, and 175, creating garbage collector pressure in the 120 FPS game loop.
2. **Canonical Tile Types & Passability in Repository**:
   - `server/world/map_data_types.py` (lines 13–34):
     `TileType` enum: `VOID = 0`, `FLOOR = 1`, `WALL = 2`, `DESTRUCTIBLE_BARRICADE = 3`, `MUD_POOL = 4`, `SPIKE_TRAP = 5`, `CRUMBLED_DEBRIS = 6`, `BONE_PILE = 7`, `POISON_VENT = 8`, `CHASM = 9`, `BOSS_GATE = 10`, `BOSS_ALTAR = 11`, `RUNIC_FLOOR = 12`, `PATH = 13`, `DENSE_TERRAIN = 14`, `POI = 15`, `ENCOUNTER_LOW = 16`, `ENCOUNTER_MEDIUM = 17`, `ENCOUNTER_HIGH = 18`, `WATER = 19`.
   - `TileType.is_passable()` (lines 35–44) returns `False` for `VOID (0)`, `WALL (2)`, `DESTRUCTIBLE_BARRICADE (3)`, `CHASM (9)`, `BOSS_GATE (10)`, and `WATER (19)`.
   - `tests/e2e/test_poe2_map_system_e2e.py` (line 251):
     `impassable = {TileType.VOID, TileType.WALL, TileType.DESTRUCTIBLE_BARRICADE, TileType.CHASM, TileType.BOSS_GATE, TileType.WATER}`. All 81 e2e tests pass with this schema.
   - `client/webapp/js/engine/tile_map_renderer.js` (lines 38–59):
     `TILE_PALETTES`: index 0 is `VOID`, index 1 is `FLOOR`, index 2 is `WALL`, index 9 is `CHASM`, index 10 is `BOSS_GATE`, index 19 is `WATER`.
   - `client/webapp/js/engine/tile_grid_loader.js` (line 113, 121):
     `getTileAt` returns `2` (`TileType.WALL`) for out-of-bounds or invalid queries.
3. **Dispatch Prompt Tile Specifications**:
   - `DISPATCH.md` lines 20: "Impassable tiles: `WALL (1), CHASM (4), WATER (5), locked BOSS_GATE (15)`."
   - In the canonical schema, `1 = FLOOR`, `4 = MUD_POOL`, `5 = SPIKE_TRAP`, and `15 = POI`.
4. **Call Sites in Client Runtime**:
   - `client/webapp/js/engine/canvas_renderer.js` line 133:
     `const res = resolveMovementWithSliding(player.wx, player.wy, moveDistX, moveDistY, 0.35, player.isIFrame);`
     `player.wx = res.wx; player.wy = res.wy;`
   - `client/webapp/js/engine/combat_skills.js` line 467:
     `if (typeof isPositionBlocked === 'function' && !isPositionBlocked(wCoord.wx, wCoord.wy, 0.2)) { ... }`
5. **Hygiene Rules**:
   - `check_code_and_doc_hygiene.py`: Soft Cap $\le 350$ lines, Hard Cap $\le 500$ lines.
   - `DISPATCH.md`: Constraint $\le 320$ lines for `collision_engine.js`.

---

## 2. Logic Chain

1. **From Observation 1 & 2**:
   When `window.currentMapGrid` and `window.getTileAt` are loaded, `isPositionBlocked(wx, wy, radius, isDodge)` must query the tile map. However, if no tile map is active (e.g. in `zone_boundless_sanctuary` or `zone_player_hideout`), it must cleanly fall back to `getCurrentZoneBounds()` and the circular water hazard check.
2. **From Observation 2 & 3**:
   If tile code 1 were unconditionally treated as an impassable wall, then every standard procedural map generated by `WildernessMapGenerator` would treat `FLOOR` as solid wall, breaking player spawn and movement completely (`test_poe2_map_system_e2e.py` line 402 expects player at spawn on tile 1 to be walkable). Conversely, if an external test harness expects compact codes (`WALL=1, CHASM=4, WATER=5, BOSS_GATE=15`), it would fail if only the 20-code schema is supported.
   Therefore, an $O(1)$ dual-mode passability table must be implemented (`STANDARD_BLOCKED` vs `COMPACT_BLOCKED`, controlled via `root.COLLISION_TILE_MODE === 'compact'`).
3. **From Observation 1 & Radius Check**:
   Player radius is $r \in [0.2, 0.4]$ (default $0.35$). The continuous bounding box of the circle spans $tx \in [\lfloor wx - r \rfloor, \lfloor wx + r \rfloor]$ and $ty \in [\lfloor wy - r \rfloor, \lfloor wy + r \rfloor]$. Since $2r = 0.70 < 1.0$, this interval spans at most 2 tiles in X and 2 tiles in Y (maximum 4 tiles total).
   Testing these 4 tiles using closed-form clamped Euclidean distance:
   $cx = \text{clamp}(wx, tx, tx + 1)$, $cy = \text{clamp}(wy, ty, ty + 1)$, $distSq = (wx - cx)^2 + (wy - cy)^2 < r^2$
   is mathematically exact, prevents corner tunneling, requires at most 4 tile lookups (compared to 8–9 lookups for 8-point radial sampling), and involves zero trigonometric computations.
4. **From Observation 1 (Allocation Waste) & GEMINI.md Directives**:
   In `canvas_renderer.js`, `resolveMovementWithSliding` is called every frame at 120 FPS. The current implementation creates new `{ wx, wy, isSliding }` object instances on every call. Mutating and returning a module-level static object `SLIDE_RESULT = { wx: 0, wy: 0, isSliding: false }` eliminates heap allocations while preserving the existing return structure.
5. **From Observation 2 & 4 (Dodge/Blink Chasm Bypass)**:
   When `isDodge === true` (passed from `player.isIFrame` at line 133 of `canvas_renderer.js`), `CHASM` tiles (standard 9, compact 4) must return `false` from `isTileBlocked`, allowing the character to leap across the pit. A trajectory helper `canBypassChasm(startWx, startWy, targetWx, targetWy, radius)` ensures that the start and landing coordinates are walkable non-chasm tiles and that the leap trajectory only crosses chasm tiles without hitting solid walls or water.
6. **From Observation 1 & 5 (Line Budget)**:
   The proposed unified implementation (`report.md` Section 8) is **255 lines**, staying strictly below the $\le 320$ line constraint.

---

## 3. Caveats

1. **Map Coordinate Origin**:
   In tile-based zones, world coordinates align directly with tile indices ($1 \text{ world unit} = 1 \text{ tile}$, $0 \le wx \le \text{currentMapWidth}$). In legacy zones without a map grid, coordinates are centered around $(0, 0)$ with `ZONE_BOUNDS`. The implementation handles this automatically: when `root.currentMapGrid && root.currentMapWidth > 0`, it uses tile boundaries; otherwise, it falls back to `ZONE_BOUNDS`.
2. **Dynamic Prop Mutability**:
   Props with footprints in `window.mapProps` continue to be checked alongside tile collision. If a prop is placed on an already impassable tile (like a wall), both checks agree and do not conflict.
3. **Monster Collision**:
   This engine focuses on player and entity-to-world collision. Monster-to-monster avoidance (soft repulsion) is handled separately in `monster_system.js`.

---

## 4. Conclusion

1. **File Status**: `client/webapp/js/engine/collision_engine.js` is fully formulated and ready to be implemented by M3 Worker.
2. **Line Count**: The proposed code is **255 lines**, meeting the $\le 320$ line limit.
3. **Architecture**:
   - Integrates seamlessly with `window.currentMapGrid` and `window.getTileAt(tx, ty)`.
   - Dual-mode tile passability ($O(1)$ bitmask) resolves schema divergence (`STANDARD_BLOCKED` vs `COMPACT_BLOCKED`).
   - Circle-AABB distance clamping checks $\le 4$ tiles per test with zero corner tunneling.
   - Module-level `SLIDE_RESULT` enforces zero heap allocations in hot paths.
   - `isDodge` allows chasm leaps, protected by `canBypassChasm` endpoint validation.
   - Full backward compatibility with `ZONE_BOUNDS` and `mapProps`.

---

## 5. Verification Method

### 5.1 Independent Test Commands
1. Run existing E2E map system tests to verify zero regression:
   ```bash
   pytest tests/e2e/test_poe2_map_system_e2e.py -v
   ```
2. Run code hygiene check to confirm line budget:
   ```bash
   python tools/lint/check_code_and_doc_hygiene.py --strict
   ```
3. Run existing tile grid stress harness:
   ```bash
   node tests/unit/test_challenger_tile_grid_stress.js
   ```

### 5.2 Verification of Proposed Implementation
Once `worker_m3_1` writes `collision_engine.js`:
1. Verify `collision_engine.js` length:
   ```bash
   (Get-Content client/webapp/js/engine/collision_engine.js).Count
   ```
   Must be $\le 320$ lines.
2. Run new unit test suite `tests/unit/test_tile_collision.py` (to be created by M3 Worker as specified in `report.md` Section 9).

### 5.3 Invalidation Conditions
- Any test where `FLOOR` (1) in standard map is treated as blocked.
- Any frame where `resolveMovementWithSliding` allocates heap memory.
- Any movement where diagonal wall sliding snags or penetrates corner vertices.
- Any dodge roll that lands inside an impassable chasm without recovery.
