175 lines
18 KiB
Markdown
175 lines
18 KiB
Markdown
# Project: Diablo II v1.13c Authentic Isometric Sprite Automap & Minimap System (`diablo2-web`)
|
||
|
||
## Architecture
|
||
- **1.13c Ground Truth Lookup & Weight Engine (`src/formats/automap-table.ts`, re-exported at `src/game/automap-table.ts`)**:
|
||
- Parses all 3,288 data rows of `data\global\excel\AutoMap.txt` by column position (`0..4` and `6, 8, 10, 12`, avoiding the duplicate `'Type2'` header at indices 7 and 9) into a `36 × 20` bucket table indexed by `AUTOMAP_LEVEL_NAMES` (`0..35`, 1-to-1 with `LvlTypes.txt` IDs `1..35`) and `AUTOMAP_TILE_NAMES` (`0..19`, `"fl"`..`"fi"`).
|
||
- Implements `getAutomapCellId` matching `(Style, StartSequence..EndSequence)` (`Style == -1` and `StartSequence == -1` as wildcards) and selecting a valid frame from `Cel1..Cel4` (`!= -1`) via SplitMix64 spatial hashing.
|
||
- Enforces the `D2Client.dll` `0x6fba1528` cell priority weight table (49 `(cellId, groupId)` pairs across 18 priority groups `0..17`) when deduplicating cells at identical `(xPixel, yPixel)` coordinates so doors, exits, stairs, and warps are never overwritten by ordinary walls.
|
||
- Applies all 1.13c special tile, object, monster, and pre-baked town chunk overrides (`0x6fd89360`, `0x6fb11d00`, `0x6fb10c40`, `0x6fb12760`).
|
||
- **Offline UI & Level Asset Packing Pipeline (`scripts/pack-ui.ts`, `src/game/d2map.ts`, `scripts/pack-act-assets.ts`)**:
|
||
- `scripts/pack-ui.ts` decodes all 8 automap DC6 sprite sheets from `data\global\ui\Automap\` (`MaxiMap.dc6`, `MaxiMapS.dc6`, `Act2Map.dc6`, `Act2MapS.dc6`, `Act4Map.dc6`, `Act4MapS.dc6`, `ExTnMap.dc6`, `ExTnMapS.dc6`) and packs them into `public/ui/` and `public/assets/ui/automap/` with exact per-frame dimensions and offsets, updating `public/ui/manifest.json` and `src/ui/baked-ui-meta.ts`.
|
||
- Both live `buildIsoMapScene` (`src/game/d2map.ts`) and offline `bakeDs1Variant` (`scripts/pack-act-assets.ts`) generate the 4 ordered automap cell layers (`floors`, `walls`, `objects`, `extras`) with authentic coordinates (`xPixel = (cellX - cellY) * 8`, `yPixel = (cellX + cellY) * 4 + (tileType >= 16 ? 24 : 0)` for tiles; `xPixel = trunc(xClient / 10) + 1`, `yPixel = trunc(yClient / 10) - 3` for objects/NPCs) across all 136 levels.
|
||
- **Runtime Isometric Automap & Minimap UI (`src/ui/minimap.ts`, `src/ui/hud-manager.ts`, `src/scene/act-scene.ts`)**:
|
||
- Renders the 4 automap sprite layers in strict order (`floors -> walls -> objects -> extras`) using `screenX = trunc((xPixel * 10) / nDivisor) - offsetX`, `screenY = trunc((yPixel * 10) / nDivisor) - offsetY`.
|
||
- Supports **Corner Minimap mode** (`bMinimap = 1`, `nDivisor = 20`, `*MapS.dc6` sheets, `279×225` top-right/top-left viewport clipped via `gl.scissor` with no non-authentic background box) and **Full-Screen Automap mode** (`bMinimap = 0`, `nDivisor = 10`, `*Map.dc6` sheets, arrow-key panning `±8px`, `Home` reset, and automatic `±160px` horizontal center shift when left/right HUD panels are open), toggled via `Tab` and `V` (plus `F9` fade and `F10` names).
|
||
- Implements Room/chunk-based fog-of-war exploration reveal (`0x6fb12af0` & `0x6fb12580`), 12-segment closed isometric `X` crosshairs (`0x6fb0f1c0`, `0x6fb82de8`), dynamic portal icons (`306`/`307`/`300`), entrance/warp destination labels (`Font6`), and top-right right-aligned gold `Font16` HUD text (Level Name and Difficulty).
|
||
|
||
## Feature Inventory
|
||
Every feature from the Survey phase is assigned to a milestone below. No feature is left unassigned.
|
||
| # | Feature | Description | Milestone | Source |
|
||
|---|---------|-------------|-----------|--------|
|
||
| F1 | `AutoMap.txt` 36 Level Names & 20 Tile Names | Exact `AUTOMAP_LEVEL_NAMES` (`0..35`, 1-to-1 with `LvlTypes.txt` `1..35` including `"1 Monestary"`) and `AUTOMAP_TILE_NAMES` (`0..19`, `"fl"`..`"fi"`). | M1 | Survey 1 & 2 |
|
||
| F2 | `AutoMap.txt` 3,288-Row Parser & `getAutomapCellId` | Parse all 3,288 rows of `data\global\excel\AutoMap.txt` by column index, build `36 × 20` buckets, match `(Style, StartSequence..EndSequence)` (`-1` wildcards), and pick valid frame from `Cel1..Cel4` (`!= -1`) via SplitMix64 hash. | M1 | Survey 1 & 2 |
|
||
| F3 | `0x6fba1528` Cell Priority Weight Table & Deduplication | Extract all 49 `(cellId, groupId)` pairs across 18 priority groups (`0..17`), implement `getAutomapCellWeight`, and deduplicate identical `(xPixel, yPixel)` cells so high-priority doors, exits, stairs, and warps are never overwritten by ordinary walls. | M1 | Survey 1 |
|
||
| F4 | 1.13c Special Tile, Object & Monster Overrides | Apply hidden wall bit 0 skip (`flags & 1`), Rogue Campfire (`Level 2, style=8, seq=0 -> 405`), Act 2 Palace Cellar warp (`Level 40, type=8, style=25, seq=0 -> 305`), `Objects.txt` `AutoMap` column, Stash (`267 -> 319`), Cain (`26` & monster IDs `146,254,265,266,473,532,540 -> 267`), Portal (`59/60 -> 306` or `300` in Level 4), Wirt (`268 -> 309`), Khalim Chests (`405..407 -> 319`), Compelling Orb (`366`), Arcane Waypoint (`402` in `levelId=74`), and `MonStats2.txt` (`!noMap && automapCel > 0`). | M1 | Survey 1 & 2 |
|
||
| F5 | Pre-Baked Town Base Map Chunk Generation (`0x6fb12760`) | Generate `fSaved=1` Act 2 Lut Gholein (`levelId=40`, `Act2Map` 4×5 = 20 frames), `fSaved=2` Act 4 Pandemonium Fortress (`levelId=103`, `Act4Map` 2×2 = 4 frames), and `fSaved=3` Act 5 Harrogath (`levelId=109`, `ExTnMap` 2×4 = 8 frames) into `extras` layer with exact 1.13c coordinates. | M1 | Survey 1 |
|
||
| F6 | Offline Packing of All 8 Automap DC6 Sprite Sheets (`scripts/pack-ui.ts`) | Pack `MaxiMap.dc6`, `MaxiMapS.dc6`, `Act2Map.dc6`, `Act2MapS.dc6`, `Act4Map.dc6`, `Act4MapS.dc6`, `ExTnMap.dc6`, `ExTnMapS.dc6` into `public/ui/` and `public/assets/ui/automap/` with exact per-frame dimensions and offsets; update `public/ui/manifest.json` and `src/ui/baked-ui-meta.ts`. | M1 | Survey 1 & 2 |
|
||
| F7 | Live & Offline 4-Layer Automap Generation Across All 136 Levels | Expose `layerId` (`Levels.txt` `Layer`) in `src/game/acts.ts`, generate `{ floors, walls, objects, extras }` with `xPixel = (cellX - cellY) * 8`, `yPixel = (cellX + cellY) * 4 + (tileType >= 16 ? 24 : 0)` in `src/game/d2map.ts` and `scripts/pack-act-assets.ts`, fail fast on missing/invalid inputs, re-bake all 136 levels (`npm run pack:data`), and verify in `scripts/verify-packs.ts`. | M2 | Survey 1 & 2 |
|
||
| F8 | 4-Layer Isometric Sprite Automap & Minimap Renderer (`src/ui/minimap.ts`) | Load all 8 packed automap sprite sheets and render `floors -> walls -> objects -> extras` in strict order using `screenX = trunc((xPixel * 10) / nDivisor) - offsetX`, `screenY = trunc((yPixel * 10) / nDivisor) - offsetY`. | M3 | Survey 1 & 3 |
|
||
| F9 | Corner Minimap (`bMinimap=1`) vs Full Automap (`bMinimap=0`), Viewport, Panel Shift & Hotkeys | Corner Minimap (`nDivisor=20`, `*MapS` sheets, `279×225` top-right/top-left viewport clipped via `gl.scissor`, no background box) and Full Automap (`nDivisor=10`, `*Map` sheets, arrow-key panning `±8px`, `Home` reset, `±160px` center shift when left/right HUD panels open), toggled via `Tab` and `V` (plus `F9` fade, `F10` names). | M3 | Survey 1 & 3 |
|
||
| F10 | Room/Chunk-Based Fog-of-War Exploration Reveal (`0x6fb12af0` & `0x6fb12580`) | Reveal current room/chunk and adjacent same-level rooms when player movement octile distance satisfies `max(|dx|,|dy|)*2 + min(|dx|,|dy|) >= 160` sub-units or on room transition. | M3 | Survey 1 & 3 |
|
||
| F11 | 12-Segment Isometric `X` Crosshair (`0x6fb0f1c0`, `0x6fb82de8`) & Dynamic Portals | Render 12-segment closed polygon (`0x6fb82de8` scaled by 2, shifted `(-1, +5)` when `bMinimap == 1`) for player (`RGB(0,0,255)` / palette `0x97`), party/allies (`RGB(0,255,0)` + `Font6` label), mercenaries/summons (`RGB(72,160,52)` / `RGB(244,244,244)`), bosses/superuniques (`RGB(255,0,0)`), and active town portals (`MaxiMap(S)` frame `306`/`307`/`300`). | M3 | Survey 1 & 3 |
|
||
| F12 | Entrance/Warp Area Name Labels & Top-Right Gold `Font16` HUD Text (`0x6fb0f9f0`) | Render centered `Font6` area name labels on revealed entrances/warps (and `"Stash"` on Stash), and top-right right-aligned gold `Font16` HUD text (Level Name and Difficulty) at `x = screenWidth - 16`. | M3 | Survey 1 & 3 |
|
||
| F13 | Full-Spectrum 136-Level Verification & Browser Audit (`scripts/verify-automap.ts`) | Verify all 136 level IDs (Acts 1–5 towns including `40`, `103`, `109`, wildernesses, dungeons) produce non-empty valid automap cells and render cleanly in headless Chrome in both `bMinimap=1` and `bMinimap=0` modes with zero console/page/WebGL errors, plus Tiers 1–5 E2E tests. | M4 | Survey 1, 2 & 3 |
|
||
|
||
## Milestones
|
||
| # | Name | Scope | Dependencies | Status |
|
||
|---|------|-------|-------------|--------|
|
||
| M1 | `M1-R1-Automap-Table-And-UI-Sprites` | Implement `src/formats/automap-table.ts` (and `src/game/automap-table.ts` re-export), `src/formats/automap-table.test.ts` (testing all 3,288 rows, bucket lookup, `0x6fba1528` weight deduplication, `0x6fb82de8` crosshair geometry, and special tile/object/town overrides), and update `scripts/pack-ui.ts` to pack all 8 automap DC6 sprite sheets (`MaxiMap(S)`, `Act2Map(S)`, `Act4Map(S)`, `ExTnMap(S)`) into `public/ui/` and `public/assets/ui/automap/`. Sync `.agents/teamwork/PROJECT.md` to root `PROJECT.md`. | none | DONE (`src/formats/automap-table.ts`, `src/game/automap-table.ts`, `src/formats/automap-table.test.ts` 11/11 passed, `scripts/pack-ui.ts` 8 sheets packed) |
|
||
| M2 | `M2-R2-Map-Scene-Automap-Baking` | Update `src/game/acts.ts` (`layerId`), `src/game/d2map.ts` (`buildIsoMapScene`), `scripts/pack-act-assets.ts` (`bakeDs1Variant`), `package.json` (`pack:data`), and `scripts/verify-packs.ts` to generate and verify the 4 ordered automap cell layers (`floors`, `walls`, `objects`, `extras`) across all 136 levels in both live `buildIsoMapScene` and baked `scene.json`. | M1 | DONE (all 365 scenes across 136 levels baked in `samples/d2-packs`, `npm run verify:packs` 4061/4061 passed) |
|
||
| M3 | `M3-R3-Isometric-Automap-Minimap-UI` | Rewrite `src/ui/minimap.ts` and update `src/ui/hud-manager.ts` and `src/scene/act-scene.ts` to implement the authentic 1.13c 4-layer isometric sprite renderer, Corner Minimap (`bMinimap=1`, `nDivisor=20`, `279×225`) & Full Automap (`bMinimap=0`, `nDivisor=10`, arrow-key panning, `±160px` panel shift), `Tab`/`V`/`F9`/`F10` hotkeys, room fog-of-war reveal, 12-segment crosshairs, portal icons, warp labels, top-right gold `Font16` HUD text, and unit tests (`tests/automap-renderer.test.ts`). | M1, M2 | DONE (`src/ui/minimap.ts`, `src/ui/hud-manager.ts`, `src/scene/act-scene.ts`, `tests/automap-renderer.test.ts` 13/13 passed, `scripts/verify-automap.ts` 1556/1556 passed) |
|
||
| E2E | `E2E-Testing-Track` | Design and implement `TEST_INFRA.md`, 4-tier requirement-driven opaque-box E2E test suite (`tests/e2e-automap-113c/tier1..tier4.test.ts`), and 136-level + headless browser verification script (`scripts/verify-automap.ts`); publish `TEST_READY.md`. | M1, M2, M3 | DONE (`tests/e2e-automap-113c/tier1..4.test.ts` 160/160 passed, `TEST_INFRA.md`, `TEST_READY.md`) |
|
||
| M4 | `M4-Final-E2E-And-Tier5-Hardening` | Phase 1: Pass 100% of E2E tests (Tiers 1–4), `npm run typecheck`, `npm test`, `npm run verify:packs`, `npm run verify:acts`, `npm run verify:all`, and `scripts/verify-automap.ts` across all 136 levels. Phase 2: Tier 5 white-box adversarial coverage hardening (`tests/e2e-automap-113c/tier5-adversarial-coverage.test.ts`). | M1, M2, M3, E2E | IN_PROGRESS |
|
||
|
||
## Interface Contracts
|
||
|
||
### 1. `src/formats/automap-table.ts` (re-exported by `src/game/automap-table.ts`)
|
||
```ts
|
||
export type AutomapAtlasSlot = 0 | 1 | 2 | 3 // 0 = MaxiMap(S), 1 = Act2Map(S), 2 = Act4Map(S), 3 = ExTnMap(S)
|
||
|
||
export interface AutomapRow {
|
||
readonly rowIndex: number
|
||
readonly levelName: string
|
||
readonly tileName: string
|
||
readonly levelTypeId: number // 0..35
|
||
readonly tileType: number // 0..19
|
||
readonly style: number // -1 or 0..N
|
||
readonly startSequence: number // -1 or 0..N
|
||
readonly endSequence: number // -1 or 0..N
|
||
readonly cels: readonly number[]
|
||
readonly celCount: number
|
||
}
|
||
|
||
export interface AutomapTable {
|
||
readonly rows: readonly AutomapRow[]
|
||
readonly buckets: ReadonlyArray<ReadonlyArray<readonly AutomapRow[]>> // [36][20]
|
||
readonly levelRanges: ReadonlyArray<{ readonly start: number; readonly end: number }> // [36]
|
||
}
|
||
|
||
export interface AutomapCell {
|
||
readonly cellNo: number
|
||
readonly xPixel: number
|
||
readonly yPixel: number
|
||
readonly cellX: number
|
||
readonly cellY: number
|
||
readonly fSaved?: AutomapAtlasSlot
|
||
readonly weight?: number
|
||
}
|
||
|
||
export interface AutomapLayerData {
|
||
readonly layerId: number
|
||
readonly levelTypeId: number
|
||
readonly floors: readonly AutomapCell[]
|
||
readonly walls: readonly AutomapCell[]
|
||
readonly objects: readonly AutomapCell[]
|
||
readonly extras: readonly AutomapCell[]
|
||
}
|
||
|
||
export const AUTOMAP_LEVEL_NAMES: readonly string[] // length 36 (0..35)
|
||
export const AUTOMAP_TILE_NAMES: readonly string[] // length 20 (0..19)
|
||
export const AUTOMAP_CELL_WEIGHT_PAIRS: ReadonlyArray<readonly [number, number]> // 49 pairs across groups 0..17
|
||
export const AUTOMAP_CROSSHAIR_VERTICES: ReadonlyArray<readonly [number, number]> // 13 pairs from 0x6fb82de8
|
||
export const AUTOMAP_CROSSHAIR_SCALED_VERTICES: ReadonlyArray<readonly [number, number]> // 13 pairs scaled by 2
|
||
|
||
export function parseAutomapTable(raw: Uint8Array | string): AutomapTable
|
||
export function getAutomapLevelTypeId(levelNameOrLvlTypeName: string | number): number
|
||
export function getAutomapTileType(tileName: string): number
|
||
export function getAutomapCellId(
|
||
table: AutomapTable,
|
||
levelTypeId: number,
|
||
tileType: number,
|
||
style: number,
|
||
sequence: number,
|
||
cellX?: number,
|
||
cellY?: number,
|
||
seed?: number,
|
||
): number
|
||
export function getAutomapCellWeight(cellNo: number): number
|
||
export function computeAutomapTilePixelCoords(cellX: number, cellY: number, tileType: number): { xPixel: number; yPixel: number }
|
||
export function computeAutomapObjectPixelCoords(subX: number, subY: number): { xPixel: number; yPixel: number }
|
||
export function resolveTileAutomapCellId(
|
||
table: AutomapTable,
|
||
params: {
|
||
levelId: number
|
||
levelTypeId: number
|
||
tileType: number
|
||
style: number
|
||
sequence: number
|
||
flags?: number
|
||
cellX?: number
|
||
cellY?: number
|
||
seed?: number
|
||
},
|
||
): number
|
||
export function resolveObjectAutomapCellId(params: {
|
||
objectClassId: number
|
||
autoMapCol: number
|
||
levelId?: number
|
||
mode?: number
|
||
}): number
|
||
export function resolveMonsterAutomapCellId(params: {
|
||
monsterClassId: number
|
||
automapCel: number
|
||
noMap?: boolean
|
||
isAlive?: boolean
|
||
}): number
|
||
export function deduplicateAutomapCells(cells: readonly AutomapCell[]): AutomapCell[]
|
||
export function generateTownSavedExtras(
|
||
levelId: number,
|
||
roomOriginSubX?: number,
|
||
roomOriginSubY?: number,
|
||
roomSubWidth?: number,
|
||
): AutomapCell[]
|
||
export function getCrosshairSegments(
|
||
cx: number,
|
||
cy: number,
|
||
bMinimap: boolean,
|
||
): ReadonlyArray<{ readonly x0: number; readonly y0: number; readonly x1: number; readonly y1: number }>
|
||
```
|
||
|
||
### 2. Packed UI Atlases (`scripts/pack-ui.ts` ↔ `src/ui/baked-ui-meta.ts` & `src/ui/minimap.ts`)
|
||
- `scripts/pack-ui.ts` outputs all 8 automap DC6 sprite sheets (`MaxiMap.dc6`, `MaxiMapS.dc6`, `Act2Map.dc6`, `Act2MapS.dc6`, `Act4Map.dc6`, `Act4MapS.dc6`, `ExTnMap.dc6`, `ExTnMapS.dc6`) to:
|
||
- `public/ui/automap-maxi.png`, `public/ui/automap-maxis.png`, `public/ui/automap-act2.png`, `public/ui/automap-act2s.png`, `public/ui/automap-act4.png`, `public/ui/automap-act4s.png`, `public/ui/automap-extn.png`, `public/ui/automap-extns.png` (and recorded in `public/ui/manifest.json` + `src/ui/baked-ui-meta.ts`), AND
|
||
- `public/assets/ui/automap/MaxiMap.png`, `MaxiMapS.png`, `Act2Map.png`, `Act2MapS.png`, `Act4Map.png`, `Act4MapS.png`, `ExTnMap.png`, `ExTnMapS.png` + `public/assets/ui/automap/manifest.json` with exact per-frame `{ width, height, offsetX, offsetY, x, y }` metadata.
|
||
|
||
### 3. Scene & Runtime Automap Data (`src/game/d2map.ts` & `scripts/pack-act-assets.ts` ↔ `src/scene/act-scene.ts` & `src/ui/minimap.ts`)
|
||
- Both `IsoMapScene` (`src/game/d2map.ts`) and `scene.json` (`PackSceneJson` in `scripts/pack-act-assets.ts` and `src/scene/act-scene.ts`) include:
|
||
- `automap: AutomapLayerData` (`{ layerId, levelTypeId, floors, walls, objects, extras }`).
|
||
- `src/ui/minimap.ts` (`Minimap`) exposes:
|
||
- `visible: boolean` (toggled by `Tab`)
|
||
- `bMinimap: boolean` (or `isMinimap: boolean`, default `true` = Corner Minimap `nDivisor = 20`; `false` = Full-Screen Automap `nDivisor = 10`, toggled by `V`)
|
||
- `fadeMode: 0 | 1 | 2` (toggled by `F9`)
|
||
- `showNames: boolean` (toggled by `F10`)
|
||
- `panX: number`, `panY: number` (stepped `±8px` by Arrow keys when Full-Screen Automap is active; reset by `Home`)
|
||
- `reveal(level: MinimapLevel, subXOrCellX: number, subYOrCellY: number): void` (implementing 1.13c room/chunk fog-of-war reveal with octile movement threshold `max(|dx|,|dy|)*2 + min(|dx|,|dy|) >= 160` sub-units / room transition)
|
||
- `draw(...)` & `drawHudOverlay(ctx: CanvasRenderingContext2D, font: D2FontRenderer, ...)` (rendering 4 sprite layers, 12-segment crosshairs, dynamic portals, warp/stash labels, and top-right gold `Font16` Level Name + Difficulty)
|
||
|
||
## Code Layout
|
||
- `src/formats/automap-table.ts` & `src/game/automap-table.ts`: 1.13c `AutoMap.txt` parser, `36 × 20` bucket lookup, `0x6fba1528` weight table & deduplication, special tile/object/monster/town overrides, and `0x6fb82de8` crosshair geometry.
|
||
- `src/formats/automap-table.test.ts`: Comprehensive unit test suite for `automap-table.ts`.
|
||
- `scripts/pack-ui.ts`, `public/ui/`, `public/assets/ui/automap/`, `src/ui/baked-ui-meta.ts`: Offline UI packer and packed atlas metadata for all 8 automap DC6 sprite sheets.
|
||
- `src/game/acts.ts`, `src/game/d2map.ts`, `scripts/pack-act-assets.ts`, `scripts/verify-packs.ts`: Live and offline 4-layer automap cell generation and 136-level `scene.json` baking.
|
||
- `src/ui/minimap.ts`, `src/ui/hud-manager.ts`, `src/scene/act-scene.ts`: Authentic 1.13c isometric sprite Automap & Minimap renderer, input bindings (`Tab`, `V`, `F9`, `F10`, Arrow keys, `Home`), room reveal, 12-segment crosshairs, warp labels, and top-right HUD text.
|
||
- `tests/automap-renderer.test.ts`: Unit tests for `src/ui/minimap.ts` renderer, room reveal, crosshairs, projection, and HUD/input integration.
|
||
- `tests/e2e-automap-113c/` & `scripts/verify-automap.ts`: 4-tier requirement-driven E2E test suite, Tier 5 adversarial coverage tests, and 136-level + headless Chrome CDP verification script.
|