# FreeExile Client Map Architecture & Tile Grid Survey Report

**Author**: Client Map Explorer  
**Date**: 2026-10-01  
**Scope**: Client world renderer, isometric math, canvas renderer, collision engine, fog of war, minimap HUD, monster encounters, and mobile performance optimization for FreeExile WebApp.  
**Authoritative Reference**: `ORIGINAL_REQUEST.md` (section `## 2026-10-01T19:19:13Z`)

---

## 1. Executive Summary

FreeExile is currently rendering its world terrain via synthetic mathematical formulas (`Math.abs(x - y) <= 1` for stone, `Math.hypot(x - 5, y - (-3)) < 3.2` for water) combined with a static 1440×960 painted backdrop centered at `(0, 0)`. There is no persistent tile grid data on the client, and collision is limited to outer bounding boxes (`ZONE_BOUNDS`) and static prop footprints.

To realize the PoE2-style procedural wilderness vision, the client must transition to a **server-authoritative, tile-based pipeline**:
1. **Compact Data Delivery**: Receive a compact `Uint8Array` (1 byte per tile, 60×45 to 120×90) from the server upon entering a zone.
2. **True Isometric Tile Rendering**: Replace the hardcoded math loop in `world_renderer.js` with view-frustum culled tile drawing and 16×16 chunk pre-rendering.
3. **Tile-Level Collision & Wall Sliding**: Upgrade `isPositionBlocked(wx, wy)` in `collision_engine.js` to sample tile grid data via `(tx, ty) = (floor(wx), floor(wy))` while preserving smooth 2-axis wall sliding.
4. **Fog of War & Minimap HUD**: Implement a `Uint8Array` fog grid (`UNEXPLORED=0`, `EXPLORED_FOGGED=1`, `VISIBLE=2`), persistent per zone+seed in `localStorage`, paired with a dedicated 120×80px 2D Minimap canvas in the HUD.
5. **Encounter Zone Monster Anchoring**: Anchor monster packs to procedural encounter zones rather than hardcoded coordinates, wire collision-aware movement into monster AI, and enforce boss gate progression locks.
6. **Mobile Performance Protection**: Implement an LRU-bounded chunk cache pool (3–4 active `OffscreenCanvas` instances) to strictly guarantee the **≤ 8 MB RAM budget** and reduce per-frame canvas draw calls by over 98%.

---

## 2. Component-by-Component Investigation & Findings

### 2.1. World Renderer (`client/webapp/js/engine/world_renderer.js`)

#### Current State (Lines 1–461)
- **Math-based Tile Generation (Lines 12–28)**:
  ```javascript
  const tileRadius = Math.ceil(Math.max(viewport.clientWidth, viewport.clientHeight) / (TILE_H * 2)) + 2;
  for (let x = camWx - tileRadius; x <= camWx + tileRadius; x++) {
    for (let y = camWy - tileRadius; y <= camWy + tileRadius; y++) {
      const pt = worldToIso(x, y);
      if (pt.x < -TILE_W || pt.x > viewport.clientWidth + TILE_W || pt.y < -TILE_H || pt.y > viewport.clientHeight + TILE_H) continue;
      let tileImg = ASSETS['tile_grass'];
      if (Math.abs(x - y) <= 1) tileImg = ASSETS['tile_stone'];
      else if (Math.hypot(x - 5, y - (-3)) < 3.2) tileImg = ASSETS['tile_water'];
      if (tileImg && tileImg.complete) {
        ctx.drawImage(tileImg, pt.x - TILE_W / 2, pt.y - TILE_H / 2, TILE_W, TILE_H);
      }
    }
  }
  ```
  - **Issues**:
    1. On a standard 1280×720 viewport, `tileRadius` is ~22. The nested loop executes $(45 \times 45) = 2,025$ iterations every frame.
    2. Calls `worldToIso(x, y)` 2,025 times per frame, creating high GC pressure from transient `{x, y}` object allocations.
    3. The terrain is completely synthetic and identical everywhere; there are no actual rooms, corridors, or obstacles.
- **Fixed Painted Backdrop (Lines 30–40)**:
  - Draws `vltk1_terrain` (1440×960) at `worldToIso(0, 0)`. In maps expanded to 1600×1600 or larger, walking outside this box drops the player into the generic math pattern.
- **Interactive Props & Portals (Lines 42–458)**:
  - Ground loot beams, secret chamber vortex (lines 93–132), proximity interaction prompts (lines 134–186), Map Device with 6 orbiting portals (lines 188–287), World Gate, Quest Gate, and Waypoints with glowing safe zone indicators (lines 380–458).
  - *Recommendation*: Preserve all interactive props, Map Device, and Waypoint safe zone runes, but replace lines 12–40 with the unified tile grid renderer.

#### Proposed Architecture: Tile Grid Rendering
- **Grid Storage**:
  - `window.currentMapGrid`: `Uint8Array` of size `width * height` stored in row-major order (`index = ty * width + tx`).
  - Helper functions:
    ```javascript
    function getTileAt(tx, ty) {
      if (tx < 0 || tx >= window.mapWidth || ty < 0 || ty >= window.mapHeight) return 0; // VOID
      return window.currentMapGrid[ty * window.mapWidth + tx];
    }
    ```
- **Tile Types Supported**:
  - `0`: `VOID` (unwalkable, abyss)
  - `1`: `FLOOR` (standard walkable terrain)
  - `2`: `WALL` (solid barrier, blocks movement and vision)
  - `3`: `DESTRUCTIBLE_BARRICADE` (destructible wall)
  - `4`: `MUD_POOL` / `DENSE_TERRAIN` (walkable, -30% movement speed penalty)
  - `5`: `SPIKE_TRAP` (walkable, hazard damage)
  - `6`: `CRUMBLED_DEBRIS` (rubble obstacle)
  - `7`: `BONE_PILE` (bone obstacle)
  - `8`: `POISON_VENT` (toxic hazard)
  - `9`: `CHASM` / `WATER` (impassable fluid/pit)
  - `10`: `BOSS_GATE` (locked barrier until zone packs cleared)
  - `11`: `BOSS_ALTAR` (boss chamber center)
  - `12`: `RUNIC_FLOOR` / `PATH` (walkable, +20% movement speed bonus)
  - `13`: `POI` (scripted encounter / reward tile)
  - `14`: `ENCOUNTER_HIGH` / `15`: `ENCOUNTER_MEDIUM` / `16`: `ENCOUNTER_LOW` (monster pack spawn regions)

---

### 2.2. Isometric Math & Coordinate Spaces (`client/webapp/js/engine/iso_math.js`)

#### Current State (Lines 94–153)
- Constants: `TILE_W = 64`, `TILE_H = 32` (standard 2:1 isometric ratio).
- **Coordinate Transformations**:
  ```javascript
  function worldToIso(wx, wy, wz = 0) {
    const cx = viewport.clientWidth / 2;
    const cy = viewport.clientHeight / 2;
    const relWx = wx - camera.wx;
    const relWy = wy - camera.wy;
    return {
      x: (relWx - relWy) * (TILE_W / 2) + cx,
      y: (relWx + relWy) * (TILE_H / 2) + cy - wz * 24
    };
  }

  function isoToWorld(screenX, screenY) {
    const cx = viewport.clientWidth / 2;
    const cy = viewport.clientHeight / 2;
    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 + camera.wx,
      wy: relWy + camera.wy
    };
  }
  ```
- **Camera Tracking (Lines 98–125)**:
  - `updateCamera(targetWx, targetWy, dt)`:
    - `followSpeed = 6.5`, `deadzone = 0.35`.
    - Exponential smoothing: `factor = 1.0 - Math.exp(-camera.followSpeed * dt)`.
    - Clamped against `bounds.minCamWx .. bounds.maxCamWx`, `minCamWy .. bounds.maxCamWy`.

#### Coordinate Mapping Analysis
- In FreeExile, **1 world unit = 1 tile**.
- Tile integer indices `(tx, ty)` map directly to world coordinates:
  $$\text{tx} = \lfloor \text{wx} \rfloor, \quad \text{ty} = \lfloor \text{wy} \rfloor$$
- When a tile at `(tx, ty)` is rendered, `worldToIso(tx, ty)` returns the top/center anchor of the diamond:
  - Diamond vertices relative to `(x, y)`:
    - Top: $(x, y - 16)$
    - Right: $(x + 32, y)$
    - Bottom: $(x, y + 16)$
    - Left: $(x - 32, y)$
- This direct mapping eliminates floating-point coordinate offsets between the server `ProceduralMapEngine` and client render/collision loops.

#### Frustum Bounding Box Derivation
To calculate the visible tile range without looping all tiles:
Given screen corners $S_1 = (0, 0)$, $S_2 = (W, 0)$, $S_3 = (W, H)$, $S_4 = (0, H)$:
Convert each to world coordinates via `isoToWorld`:
$$W_i = \text{isoToWorld}(S_i.x, S_i.y)$$
The visible tile bounding box is:
$$\text{minTx} = \max\left(0, \lfloor \min_{i} W_i.\text{wx} \rfloor - 2\right), \quad \text{maxTx} = \min\left(\text{mapWidth} - 1, \lceil \max_{i} W_i.\text{wx} \rceil + 2\right)$$
$$\text{minTy} = \max\left(0, \lfloor \min_{i} W_i.\text{wy} \rfloor - 2\right), \quad \text{maxTy} = \min\left(\text{mapHeight} - 1, \lceil \max_{i} W_i.\text{wy} \rceil + 2\right)$$
This yields approximately $(W / 64 + 5) \times (H / 32 + 5)$ tiles, satisfying the culling bound in Requirement R2.

---

### 2.3. Canvas Renderer & Entity Layering (`canvas_renderer.js` & `entity_renderer.js`)

#### Render Loop Sequence (`canvas_renderer.js` lines 290–324)
1. `ctx.fillRect(0, 0, viewport.clientWidth, viewport.clientHeight)` (Dark slate background `#020617`).
2. Camera screen shake transformation (`ctx.translate(shkX, shkY)`).
3. **Subsystems in Strict Visual Layer Order**:
   - `renderWorldEnvironment(ctx, now, renderDt)`: Base tiles, loot beams, portal runes, waypoints.
   - `renderTelegraphs(ctx)`: Ground warning decals (cones, rings).
   - `renderVfxProjectiles(ctx, renderDt)`: Flying projectiles.
   - `renderEntities(ctx, now, renderDt)`: Props, monsters, NPCs, and Player sorted by depth.
   - `weaponSwingRenderer.render(ctx, player)`: Melee arcs.
   - `renderVfxParticlesAndOverlay(ctx, renderDt, now)`: Ambient ash/embers, slash impact particles.
   - `combatFeelEngine.renderDamageNumbers(ctx)`: Floating combat text.
4. `ctx.restore()` (Restores screen shake).
5. `HudOrbs.update(...)`: Native HTML/Canvas overlay (vitality and mana orbs).

#### Depth Sorting (`entity_renderer.js` lines 50–105)
- All entities (props, monsters, NPCs, player) are collected and assigned depth:
  $$\text{depth} = \text{wx} + \text{wy}$$
- Sorted ascending: `renderEntities.sort((a, b) => a.depth - b.depth);`.
- In isometric 2:1 projection where $y_{\text{screen}} \propto (\text{wx} + \text{wy}) \cdot \frac{\text{TILE\_H}}{2}$, sorting by $\text{wx} + \text{wy}$ correctly prevents z-fighting and ensures proper occlusion.

#### Fog of War Integration with Entity Rendering
- **Critical Requirement from R4**: *"Sau khi player đi qua, tiles chuyển EXPLORED_FOGGED (overlay mờ, không có quái)"*.
- In `entity_renderer.js`:
  ```javascript
  const mobTx = Math.floor(mob.wx);
  const mobTy = Math.floor(mob.wy);
  const fogState = window.getFogAt ? window.getFogAt(mobTx, mobTy) : 2;
  // If monster is in UNEXPLORED (0) or EXPLORED_FOGGED (1), suppress rendering
  if (fogState !== 2 && !mob.isBoss) {
    return; // Masked by War-Fog
  }
  ```
- This ensures enemies in fog cannot be seen by the player, preventing maphacks and honoring the PoE2 fog specification.

---

### 2.4. Collision Engine & Wall Sliding (`client/webapp/js/engine/collision_engine.js`)

#### Current State (Lines 88–141)
- Currently checks:
  1. Hard boundary clamp: `ZONE_BOUNDS` for 10 zones.
  2. Hardcoded sanctuary water distance: `Math.hypot(wx - 5.0, wy - (-3.0)) < 2.4 + radius`.
  3. Static `mapProps` solid footprints (circles, ellipses, rects).
- **Movement Resolution with Sliding (Lines 148–179)**:
  - If direct movement to `(targetX, targetY)` is blocked:
    - Tests X-axis only: `curWx + moveDistX, curWy`.
    - Tests Y-axis only: `curWx, curWy + moveDistY`.
    - If one axis is unblocked, glides character smoothly along that wall without corner-glitching.

#### Integrating Tile-Level Collision
- In `isPositionBlocked(wx, wy, radius = 0.35, isDodge = false)`:
  - Compute tile coordinate: `tx = Math.floor(wx)`, `ty = Math.floor(wy)`.
  - Check bounding footprint: To prevent characters with radius 0.35 from clipping into wall corners, sample center and 4 radial points:
    $$(wx \pm radius, wy), \quad (wx, wy \pm radius)$$
  - For each point $(px, py)$:
    - $cx = \lfloor px \rfloor, cy = \lfloor py \rfloor$.
    - If $cx < 0$ or $cx \ge \text{mapWidth}$ or $cy < 0$ or $cy \ge \text{mapHeight}$: return `true` (out-of-bounds void).
    - Query `tileType = getTileAt(cx, cy)`.
    - Impassable tiles:
      - `TileType.WALL` (2) $\rightarrow$ return `true`.
      - `TileType.CHASM` / `WATER` (9) $\rightarrow$ return `true`.
      - `TileType.VOID` (0) $\rightarrow$ return `true`.
      - `TileType.BOSS_GATE` (10) $\rightarrow$ blocked if `!isBossGateUnlocked(currentZoneId)`; walkable once unlocked.
      - `TileType.DESTRUCTIBLE_BARRICADE` (3) $\rightarrow$ blocked until broken.
  - If all tile samples are clear, proceed to check prop footprints and boundary clamping.
  - Because `resolveMovementWithSliding` delegates entirely to `isPositionBlocked`, upgrading `isPositionBlocked` instantly enables full 2-axis wall sliding along procedural walls, corridors, and boss gates!

---

### 2.5. Monster System & Encounter Architecture (`monster_system.js` & `wilderness_zone_packs.js`)

#### Current State
- `wilderness_zone_packs.js` defines hardcoded `(wx, wy)` for packs (e.g. `pack_tang_kiem_alpha` at `wx: 9.5, wy: -4.5`).
- `monster_system.js` line 68 initializes packs directly from these static coordinates.
- In `monster_system.js` lines 255–259:
  ```javascript
  const step = dt * 1.8;
  m.wx += ((player.wx - m.wx) / distToPlayer) * step;
  m.wy += ((player.wy - m.wy) / distToPlayer) * step;
  ```
  Monsters move directly in a straight line toward the player without checking tile collisions, causing them to phase through walls.

#### Proposed Encounter Architecture (R5)
1. **Procedural Encounter Anchoring**:
   - The server procedural map engine designates 2–3 encounter zones (`ENCOUNTER_HIGH`, `ENCOUNTER_MEDIUM`, `ENCOUNTER_LOW`) along the main path.
   - When `initZoneMonsters(zoneId)` executes:
     - Filter map tiles matching the encounter zone type.
     - Select a central walkable floor tile for the pack leader.
     - Place minions on adjacent walkable floor tiles within $\pm 2$ tiles.
     - Set `m.originWx` and `m.originWy` to these procedural coordinates.
2. **Collision-Aware Monster AI**:
   - In `updateMonstersTick(dt)`:
     - Calculate desired step vector: $(dx, dy) = \frac{\text{player} - m}{\text{dist}} \cdot \text{step}$.
     - Use `resolveMovementWithSliding(m.wx, m.wy, dx, dy, 0.40)` so monsters respect walls and obstacles.
     - If blocked, perform local BFS (4-way cardinal) to navigate around corridor bends.
3. **Encounter Progress Tracker & Boss Gate Unlock**:
   - State tracker:
     ```javascript
     window.zoneEncounterProgress = window.zoneEncounterProgress || {};
     window.zoneEncounterProgress[zoneId] = {
       packsKilled: 0,
       packsRequired: 2,
       bossGateUnlocked: false
     };
     ```
   - In `combat_skills.js` (line 206, when monster dies):
     - Check if all members of the pack are dead (`activeMonsters.every(m => m.packId !== packId || m.hp <= 0)`).
     - If pack eliminated, increment `packsKilled`.
     - When `packsKilled >= packsRequired`:
       - Set `bossGateUnlocked = true`.
       - Update tile at `bossGate` coordinates from `BOSS_GATE` (10) to `FLOOR` (1).
       - Mark chunk dirty flag to trigger OffscreenCanvas re-render.
       - Display floating notification: *"Cổng Phong Ấn Cổ Đao đã được khai mở!"*.
4. **Scripted POI Encounters**:
   - When player enters within 1.2 tiles of a `TileType.POI` (13):
     - If POI not cleared: trigger an ambush wave of 3–4 elite monsters.
     - When wave is defeated: mark POI as cleared, change POI visual (rune turns golden), and spawn a bonus currency cache.

---

### 2.6. HUD Structure, Minimap, and Boss Gate Lore Popup (`index.html`)

#### Layout Analysis
- In `client/webapp/index.html`:
  - Viewport container: `#app-viewport` (lines 27–178).
  - Main Canvas: `#game-canvas` (line 29, z-0).
  - Top header: Left status card (lines 33–47), Center telemetry (lines 48–61), Right icon bar (lines 62–87).
  - Joystick: Bottom-left `#joystick-zone` (lines 95–107, z-20).
  - Action Bar & Orbs: Bottom `#hud-orbs-container` & `#hud-skill-bar` (lines 110–141, z-30).
  - Chat dock: Bottom-left `#chat-dock` (lines 150–163, z-30).
- **Available Clear Real Estate**:
  - The **top-right area directly below the header icon bar** (`top-12 right-3`) is completely clear of all HUD elements in both landscape and mobile simulator views!

#### Recommended Minimap Implementation
```html
<!-- 120x80 PROCEDURAL MINIMAP HUD (R4) -->
<div id="hud-minimap-container" class="absolute top-12 right-3 z-30 pointer-events-auto bg-stone-950/85 backdrop-blur-md border border-amber-900/40 rounded-lg p-1 shadow-2xl flex flex-col items-center select-none">
  <div class="flex items-center justify-between w-full px-1 pb-0.5 text-[8.5px] font-mono text-stone-400">
    <span id="minimap-zone-title" class="truncate max-w-[70px] text-amber-400 font-semibold">Tàn Kiếm</span>
    <span id="minimap-coords" class="text-[8px] text-stone-500">0, 0</span>
  </div>
  <canvas id="minimap-canvas" width="120" height="80" class="rounded bg-black/90 border border-stone-800/80 block"></canvas>
</div>
```
- **Minimap Rendering Rules**:
  - Unexplored tiles: solid black (`#000000`).
  - Explored tiles: Biome color (e.g. `#3b362e` Bleached Bone, `#1e3823` Mangrove).
  - Player dot: White `#ffffff` with pulsing ring.
  - Active Waypoints: Emerald green `#22c55e`.
  - Boss Gate: Crimson red `#ef4444`.
  - POIs: Amber yellow `#fbbf24`.
  - Re-render frequency: Only when fog matrix updates or player crosses tile boundary (dirty flag check).

#### Recommended Boss Gate Lore Popup
```html
<!-- BOSS GATE PROXIMITY LORE POPUP (R5) -->
<div id="hud-boss-gate-popup" class="hidden pointer-events-auto absolute top-16 left-1/2 -translate-x-1/2 z-30 max-w-xs w-11/12 bg-stone-950/95 border border-purple-600/70 rounded-xl p-3 shadow-2xl backdrop-blur text-center select-none animate-in fade-in zoom-in-95">
  <div class="flex items-center justify-between border-b border-stone-800 pb-1 mb-1">
    <div class="flex items-center gap-1.5">
      <span class="text-sm">⛩️</span>
      <span class="font-bold text-xs text-purple-300 uppercase tracking-wide">Cổng Phong Ấn Cổ Đao</span>
    </div>
    <span id="txt-boss-gate-status" class="text-[8.5px] font-mono px-1.5 py-0.2 rounded bg-purple-950 text-purple-300 border border-purple-800">Khóa</span>
  </div>
  <p id="txt-boss-gate-lore" class="text-[10px] text-stone-300 leading-snug mb-2">Huyết khí ma trận ngăn cản bước chân. Trảm diệt yêu ma để khai mở kết giới.</p>
  <div class="flex items-center justify-between text-[9.5px] font-mono bg-black/50 rounded px-2 py-1 border border-stone-800">
    <span class="text-stone-400">Tiến độ tiêu diệt:</span>
    <span id="txt-boss-gate-req" class="text-amber-400 font-bold">0 / 2 bầy quái</span>
  </div>
</div>
```
- Toggle logic: Displays automatically when $\text{hypot}(\text{player} - \text{bossGate}) \le 3.0$ tiles. Hides when $> 3.5$ tiles.

---

### 2.7. Mobile Performance, 16×16 Chunk OffscreenCanvas Caching & RAM Budget

#### Mathematical Proof of the RAM Budget
1. **Grid Memory**:
   - Max zone dimension: $120 \times 90$ tiles = 10,800 tiles.
   - Tile grid: `Uint8Array` of 10,800 bytes = **10.5 KB**.
   - Fog grid: `Uint8Array` of 10,800 bytes = **10.5 KB**.
   - Combined data structures: **~21 KB**.
2. **Chunk Memory & The 8 MB Limit**:
   - Each chunk is $16 \times 16$ tiles.
   - Screen bounding box of a $16 \times 16$ isometric chunk:
     $$\text{Width} = (16 + 16) \times \frac{\text{TILE\_W}}{2} = 32 \times 32 = 1,024 \text{ px}$$
     $$\text{Height} = (16 + 16) \times \frac{\text{TILE\_H}}{2} = 32 \times 16 = 512 \text{ px}$$
   - Memory of one $1,024 \times 512$ 32-bit RGBA bitmap:
     $$1,024 \times 512 \times 4 \text{ bytes} = 2,097,152 \text{ bytes} \approx 2.0 \text{ MB}$$
   - If all 48 chunks of a $120 \times 90$ map were cached simultaneously:
     $$48 \times 2.0 \text{ MB} = 96 \text{ MB} \quad (\textbf{VIOLATES the }\le 8 \text{ MB budget!})$$
3. **The Solution: LRU Bounded Active Chunk Pool**:
   - On mobile viewports (e.g. iPhone $844 \times 390$ or $1280 \times 720$), at most **2 to 4 chunks** intersect the viewport frustum at any given time.
   - Maintain an **LRU Cache Pool of maximum 3 to 4 `OffscreenCanvas` instances**:
     $$\text{Total Chunk RAM} = 3.5 \times 2.0 \text{ MB} \approx 7.0 \text{ MB} \le 8.0 \text{ MB}$$
   - When the camera pans to a new chunk, the oldest off-screen chunk is evicted/re-used.
   - **Dirty Flag Discipline**:
     - `chunk.isDirty = false` after rendering.
     - Only re-render when a tile state inside the chunk changes (boss gate breached, barricade broken).
     - Clean chunks are rendered with a single `ctx.drawImage(chunkCanvas, ...)` call.
4. **Draw Call Reduction**:
   - Current math loop: ~300 to 500 `drawImage` calls per frame.
   - With 16×16 chunk caching: **2 to 4 `drawImage` calls per frame**.
   - **Over 98% reduction in 2D canvas draw operations**, preserving 120 FPS ProMotion performance on Apple Silicon.

#### Benchmark Tooling (`tools/perf/map_render_benchmark.js`)
- Must simulate 1,000 frames of camera movement across a 120×90 procedural map.
- Measures:
  - Average FPS ($\ge 30$ FPS target).
  - Draw call count per frame ($\le (W/64 + 5) \times (H/32 + 5)$).
  - Canvas memory allocation ($\le 8 \text{ MB}$).

---

## 3. Recommended Implementation Roadmap

| Phase | Target Module | Key Changes |
|---|---|---|
| **Phase 1** | Server Pipeline (`procedural_map_engine.py`, `zone_engine.py`) | Expand engine for 60×45 to 120×90 grids; serialize compact `Uint8Array` tile stream; validate spawn points against safe radius and walkable tiles. |
| **Phase 2** | Client Grid & Math (`world_renderer.js`, `iso_math.js`) | Implement `window.currentMapGrid` receiver; replace math loops with view-culling frustum & 16×16 chunk pre-renderer; build LRU chunk cache pool. |
| **Phase 3** | Collision & Movement (`collision_engine.js`) | Integrate tile lookup into `isPositionBlocked(wx, wy, radius)`; enforce boss gate blockage; preserve 2-axis wall sliding; apply terrain speed modifiers. |
| **Phase 4** | Fog of War & Minimap (`world_renderer.js`, `index.html`) | Implement `window.fogGrid` (`Uint8Array`), 8-tile radius reveal, `localStorage` persistence, and 120×80 Minimap canvas in top-right HUD. |
| **Phase 5** | Encounters & Boss Gate (`monster_system.js`, `combat_skills.js`) | Anchor packs to encounter zone tiles; collision-aware monster AI; boss gate proximity popup and kill tracker auto-unlock. |
| **Phase 6** | Verification & Benchmark | Run `tools/perf/map_render_benchmark.js`, write `tests/unit/test_tile_collision.py` (>= 10 test cases), and verify full test suite (>= 894 unit tests). |

---

## 4. Verification Checkpoints

1. **Python Unit Tests**: Run `pytest tests/unit/test_war_fog_and_procedural_map.py` (verify 20/20 pass).
2. **New Collision Tests**: Run `pytest tests/unit/test_tile_collision.py` once implemented.
3. **Performance Benchmark**: Run `node tools/perf/map_render_benchmark.js` and verify `avg_fps >= 30`, `draw_calls <= 4`, `ram <= 8MB`.
4. **Browser E2E Audit**: Inspect canvas draw call count via DevTools; verify character slides along tile walls, Fog of War persists across zone re-entry, and Boss Gate unlocks upon eliminating required packs.
