# Investigation & Technical Architecture Report: Fog of War Multi-State Engine & Persistence

- **Author**: Explorer M4 1 (`explorer_m4_1`)
- **Assigned Milestone**: Milestone 4 (Fog of War & Minimap HUD)
- **Target File**: `client/webapp/js/ui/war_fog.js`
- **Reference Docs**: `ORIGINAL_REQUEST.md` (R4), `PROJECT.md`, `GEMINI.md`, `AGENTS.md`
- **Status**: Complete & Verified (Zero Heap, < 2 KB Bit-Packing, <= 300 lines)

---

## 1. Executive Summary

This investigation analyzes the requirements, current implementation, and proposed architecture for the **Fog of War (FoW) Multi-State Core Engine and Persistence** for the FreeExile PoE2-style procedural tile map system.

### Core Discoveries & Validation
1. **Existing Baseline**:
   - `client/webapp/js/ui/war_fog.js` currently contains 318 lines dedicated to an anti-bot maze modal simulator (`#modal-war-fog`).
   - It lacked the canonical in-game Fog of War engine (`window.fogGrid` flat `Uint8Array`, tile coordinate accessors, zero-heap dynamic vision update, and localStorage persistence).
   - In-game systems (`world_renderer.js`, `tile_map_renderer.js`, `minimap_hud.js`) require a standard API: `WarFog.updatePlayerVision(wx, wy, radius)`, `WarFog.getFogState(tx, ty)`, `window.fogGrid`.
2. **3-State Fog Model**:
   - `0 = UNEXPLORED`: Pitch black, shroud covers terrain and suppresses all dynamic entities/monsters.
   - `1 = EXPLORED_FOGGED`: Terrain previously discovered, rendered with 55-70% dark overlay; dynamic entities (monsters) hidden to defeat maphacks.
   - `2 = VISIBLE`: Currently inside player vision radius (8 tiles); fully rendered and lit.
3. **Zero-Heap Dynamic Vision Decay & Reveal**:
   - Vision updates run at frame rate (30-60 Hz).
   - Allocating arrays or coordinate objects in `updatePlayerVision()` would trigger garbage collection pauses, breaking 120 FPS ($8.33\text{ ms}$) budgets.
   - Designed algorithm executes in $0.0002\text{ ms}$ ($0.2\mu s$) per frame with **ZERO** heap allocations.
   - Only demotes previous bounding box tiles from `VISIBLE (2)` to `EXPLORED_FOGGED (1)` and promotes new radius 8 tiles to `VISIBLE (2)`.
4. **Sub-2 KB LocalStorage Bit-Packing**:
   - Maximum zone dimension is $120 \times 90 = 10,800$ tiles.
   - By packing 1 bit per explored tile (bit 1 = explored/fogged, bit 0 = unexplored) with a 4-byte header (`width`, `height`), the entire $120 \times 90$ grid is packed into **1,354 bytes raw** or **1,808 Base64 characters** ($1.766\text{ KB} < 2.0\text{ KB}$).
   - Smallest grid ($60 \times 45 = 2,700$ tiles) packs into **342 bytes raw** or **456 Base64 characters** ($0.445\text{ KB}$).
   - Key format: `fe_fog_${zoneId}_${seed}` (e.g. `fe_fog_zone_tang_kiem_nhai_42`).
   - Round-trip fidelity verified at 100% bit preservation (10,800/10,800 tiles matching).
5. **Strict Line Cap Compliance**:
   - `war_fog.js` must remain $\le 300$ lines (Soft cap 350, Hard cap 500 lines).
   - By streamlining the modal adapter while implementing the complete core engine, the proposed `war_fog.js` is **296 lines**, strictly satisfying the $\le 300$ line constraint without sacrificing any test or runtime functionality.

---

## 2. In-Depth Analysis of Current `client/webapp/js/ui/war_fog.js`

### 2.1 File State & Structure
- **Current line count**: 318 lines.
- **Dependencies**: Imports `generateMazeData` from `./war_fog_maze.js` and `renderProceduralMap` from `./war_fog_renderer.js`.
- **Primary Function**: Standalone modal simulator for Anti-Bot Maze simulation (`#modal-war-fog`), triggered by `#btn-open-war-fog` (hotkey `Z`).
- **Missing In-Game Capabilities**:
  - No flat row-major `Uint8Array` in `window.fogGrid`.
  - No `initFog(width, height, zoneId, seed)`.
  - No `getFogState(tx, ty)` or `setFogState(tx, ty, state)`.
  - No zero-heap `updatePlayerVision(playerWx, playerWy, radius)`.
  - No `localStorage` persistence under `fe_fog_${zoneId}_${seed}`.

### 2.2 Compatibility Constraints
`tests/unit/test_mobile_webapp_config.py` lines 224-236 test for the presence of `#modal-war-fog`, `#btn-open-war-fog`, `#btn-close-war-fog`, `#warfog-canvas`, `#btn-warfog-regen`, `#btn-warfog-step`, `#btn-warfog-break-barricade`, `#txt-warfog-sinuosity`, and the `war_fog.js` script tag.
`tools/run_playwright_validation.py` lines 82-100 click `#btn-open-war-fog` and dismiss the modal with ESC.
Therefore, `war_fog.js` **must retain** the modal simulator event listeners and exports while hosting the core in-game Fog of War engine.

---

## 3. Core Engine Architecture & Design

### 3.1 3-State Fog Model Matrix
The Fog of War matrix is stored as a contiguous flat typed array:
```javascript
export const FOG_STATE = Object.freeze({
  UNEXPLORED: 0,
  EXPLORED_FOGGED: 1,
  VISIBLE: 2
});

let fogGrid = new Uint8Array(mapW * mapH);
```
- Row-major indexing: `index = ty * mapW + tx`.
- Bounds checking: Any query outside `[0, mapW - 1]` or `[0, mapH - 1]` immediately returns `FOG_STATE.UNEXPLORED (0)` without throwing or allocating.

### 3.2 Zero-Heap Dynamic Vision Decay & Reveal Algorithm
When `updatePlayerVision(playerWx, playerWy, radius = 8, force = false)` is invoked:
1. **Early Return Guard**:
   ```javascript
   const tx = Math.floor(playerWx), ty = Math.floor(playerWy);
   if (!force && tx === lastVisX && ty === lastVisY && radius === lastVisR) return;
   ```
   If player has not crossed into a new tile coordinate and radius is unchanged, 0 calculations are performed.
2. **Zero-Heap Decay Phase**:
   Instead of iterating over the entire $120 \times 90$ grid, we iterate strictly over the previous bounding box:
   ```javascript
   if (lastVisX !== -9999) {
     const pMinX = Math.max(0, lastVisX - lastVisR), pMaxX = Math.min(mapW - 1, lastVisX + lastVisR);
     const pMinY = Math.max(0, lastVisY - lastVisR), pMaxY = Math.min(mapH - 1, lastVisY + lastVisR);
     for (let y = pMinY; y <= pMaxY; y++) {
       const row = y * mapW;
       for (let x = pMinX; x <= pMaxX; x++) {
         const idx = row + x;
         if (fogGrid[idx] === FOG_STATE.VISIBLE) fogGrid[idx] = FOG_STATE.EXPLORED_FOGGED;
       }
     }
   }
   ```
   Maximum iterations: $(2 \times 8 + 1)^2 = 289$ tiles (less than $1\mu s$).
3. **Zero-Heap Reveal Phase**:
   Iterate over the new bounding box and apply Euclidean distance check $(dx^2 + dy^2 \le r^2)$:
   ```javascript
   const r2 = radius * radius;
   const cMinX = Math.max(0, tx - radius), cMaxX = Math.min(mapW - 1, tx + radius);
   const cMinY = Math.max(0, ty - radius), cMaxY = Math.min(mapH - 1, ty + radius);
   let revealedNew = false;

   for (let y = cMinY; y <= cMaxY; y++) {
     const dy = y - ty, dy2 = dy * dy, row = y * mapW;
     for (let x = cMinX; x <= cMaxX; x++) {
       const dx = x - tx;
       if (dx * dx + dy2 <= r2) {
         const idx = row + x;
         if (fogGrid[idx] !== FOG_STATE.VISIBLE) {
           if (fogGrid[idx] === FOG_STATE.UNEXPLORED) revealedNew = true;
           fogGrid[idx] = FOG_STATE.VISIBLE;
           fogDirty = true;
         }
       }
     }
   }
   ```
4. **Performance Benchmark**:
   - 100,000 continuous updates on $120 \times 90$ grid: **21.41 ms total** (**0.0002 ms / update**).
   - Heap allocations: **0 bytes**.

---

## 4. Sub-2 KB LocalStorage Bit-Packing Scheme

### 4.1 Wire Format Specification
```
+---------------+---------------+------------------------------------------+
| Bytes 0-1     | Bytes 2-3     | Bytes 4 .. (4 + ceil(W * H / 8) - 1)     |
| Width (LE)    | Height (LE)   | Explored Bitmask (1 bit per tile)        |
| uint16        | uint16        | Bit 0=UNEXPLORED, Bit 1=EXPLORED_FOGGED  |
+---------------+---------------+------------------------------------------+
```

### 4.2 Size Calculation Across Zones
| Zone Dimension | Total Tiles | Raw Payload Bytes | Base64 Length | Storage Size | Budget (< 2 KB) |
|---|---|---|---|---|---|
| $60 \times 45$ (Zone 1) | 2,700 | 342 bytes | 456 chars | **0.445 KB** | PASS (< 22% of budget) |
| $68 \times 52$ (Zone 2) | 3,536 | 446 bytes | 596 chars | **0.582 KB** | PASS (< 30% of budget) |
| $80 \times 60$ (Midgame) | 4,800 | 604 bytes | 808 chars | **0.789 KB** | PASS (< 40% of budget) |
| $120 \times 90$ (Endgame) | 10,800 | 1,354 bytes | 1,808 chars | **1.766 KB** | PASS (< 89% of budget) |

### 4.3 Key Naming & Debounced Auto-Save
- Key: `fe_fog_${zoneId}_${seed}`
- Auto-save is debounced at 1200 ms during movement to avoid write thrashing in localStorage.
- Immediate save (`saveFog(zoneId, seed)`) is triggered on zone exit or level teleportation.

---

## 5. Integration Contracts with Other Modules

```
                +-----------------------------------------+
                |          ProceduralMapEngine            |
                |  (Server generates map seed & grid)     |
                +-----------------------------------------+
                                     |
                                     v
                        [TileGridLoader / WS]
                     (Sets currentMapWidth/Height)
                                     |
                                     v
                          +--------------------+
                          |     WarFog.js      | <---+  (Loads & Debounced Saves)
                          | (window.fogGrid)   | ----+  LocalStorage: fe_fog_${zoneId}_${seed}
                          +--------------------+
                             /              \
                            /                \
                           v                  v
          +-------------------------+   +-------------------------+
          |   war_fog_renderer.js   |   |     minimap_hud.js      |
          | (Frustum Fog Shroud &   |   | (120x80 HUD minimap     |
          |  Monster Entity Culling)|   |  Biome terrain & dots)  |
          +-------------------------+   +-------------------------+
```

1. **TileMapRenderer**:
   Does not need to know about fog state for baking terrain chunks; instead, `war_fog_renderer.js` applies the fog shroud on top of rendered tiles.
2. **EntityRenderer & MonsterSystem**:
   Monsters check `WarFog.getFogState(Math.floor(m.wx), Math.floor(m.wy)) === 2` (`VISIBLE`). If `getFogState < 2`, monster sprite and nameplate are suppressed from rendering.
3. **MinimapHUD**:
   Queries `WarFog.getFogState(tx, ty)`:
   - `UNEXPLORED (0)`: Black `#050508`.
   - `EXPLORED (1 or 2)`: Biome ground color (`#3a3630`, etc.).
   - Player dot: White `#ffffff`.
   - Boss Gate dot: Red `#ef4444`.
   - Waypoint dot: Green `#22c55e`.
   - POI dot: Yellow `#fbbf24`.

---

## 6. Verification & Automated Test Results

The test suite `proposed_test_war_fog.py` was executed with `pytest`:
- `test_line_count_within_limits`: **PASSED** (296 lines $\le 300$).
- `test_es_module_exports_present`: **PASSED** (All canonical functions exported).
- `test_init_and_bounds`: **PASSED** (Safe boundary and out-of-bounds fallback).
- `test_3_state_vision_reveal_and_decay`: **PASSED** ($0 \rightarrow 2 \rightarrow 1$ lifecycle).
- `test_compact_bitpacking_under_2kb`: **PASSED** (1.766 KB for 120x90, 10,800/10,800 bit matches).

Existing test regression (`tests/unit/test_mobile_webapp_config.py` and `tests/e2e/test_poe2_map_system_e2e.py`):
- **96 passed in 1.33s** (100% clean, zero regressions).
