diablo2-web/PROJECT.md

174 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Project: D2GS v1.13c Online Gameplay Loops & Server Authority (`diablo2-web`)
## Architecture
The Diablo II v1.13c Web Client (`diablo2-web`) is structured into strictly isolated modules enforcing server-authoritative online gameplay over the D2GS v1.13c protocol:
```text
diablo2-web/
├── src/
│ ├── common/
│ │ ├── items/ # 1.13c item tables (embedded-drop-tables.ts), stat formatter (item-bridge.ts), tooltip builder (item-tooltip.ts), ground-items.ts
│ │ └── units/ # All-Act Town & Quest NPC descriptors (npc-table.ts: TOWN_NPC_DESCRIPTORS, NPC_HCIDX_TO_DESCRIPTOR_ID)
│ ├── netproto/
│ │ ├── index.ts # Sole public barrel export for netproto (strict boundary for src/client/**)
│ │ ├── domain/ # Typed ClientCommand (client-command.ts), ServerEvent (server-event.ts), ItemData & UnitSnapshot (ids.ts)
│ │ └── d2gs/
│ │ ├── tables/ # Authoritative 1.13c packet size tables (s2c-sizes.ts: C2S_PACKET_SIZES, S2C_PACKET_SIZES)
│ │ ├── items/ # 0x9C/0x9D item bitstream decoder (item-bitstream.ts)
│ │ ├── c2s/ # C2S binary packet encoders (items.ts, npc.ts, party.ts, movement.ts, interact.ts, ui.ts)
│ │ ├── s2c/ # S2C binary packet decoders (units.ts, npc.ts, merc.ts, party.ts, trade.ts, waypoint.ts, combat.ts)
│ │ └── registry.ts # encodeClientCommand & decodeD2gsServerPacket dispatchers
│ └── client/
│ ├── world/ # Authoritative ClientWorld state reducer (client-world.ts, client-unit.ts, inventory.ts, self.ts)
│ ├── view/ # Scene drawables & picking (unit-drawables.ts, scene-source.ts)
│ ├── scene/ # WorldRenderer (world-renderer.ts)
│ ├── session/ # OnlineSession (online-session.ts)
│ ├── input/ # CommandMapper (command-mapper.ts)
│ ├── ui-model/ # HudModel (hud-model.ts): syncs ClientWorld -> HudManager/WorldPanelsHud & emits ClientCommand
│ └── ui/ # HudManager (hud-manager.ts), WorldPanelsHud (world-panels.ts), InventoryPanel (inventory.ts), Minimap (minimap.ts)
└── tests/
├── client/ # Unit & integration tests (hud-server-authority, vendor-stash-ui, vendor-stash-replay, client-world, hud-session-play)
└── e2e-d2gs-online/ # 4-tier opaque-box E2E test suite + Tier 5 adversarial coverage hardening
```
### Architectural Invariants
1. **Diablo II v1.13c Ground Truth**: Every C2S and S2C opcode, packet byte length (`C2S_PACKET_SIZES` and `S2C_PACKET_SIZES` in `src/netproto/d2gs/tables/s2c-sizes.ts`), item bitstream field (`src/netproto/d2gs/items/item-bitstream.ts`), and Excel/TBL data table (`src/common/items/`, `src/common/units/npc-table.ts`) must strictly conform to Diablo II v1.13c.
2. **Server Authority**: When `HudManager` has a `_commandSink` attached (`hasCommandSink === true`), user actions MUST emit typed `ClientCommand` events (`src/netproto/domain/client-command.ts`) encoded via `encodeClientCommand` (`src/netproto/d2gs/registry.ts`) without mutating local container/stat/skill state ahead of the server. `ServerEvent` updates applied to `ClientWorld` (`src/client/world/client-world.ts`) and synced via `HudModel.syncFromWorld()` (`src/client/ui-model/hud-model.ts`) drive all UI state.
3. **Strict Import Boundary**: Files in `src/client/**` MUST ONLY import protocol types/functions from `src/netproto/index.ts` (never deep-importing `src/netproto/d2gs/**`).
---
## Feature Inventory
Every feature from the Phase 0 Survey (derived from `ORIGINAL_REQUEST.md` `2026-10-01T13:08:09Z` and Survey Reports 1, 2, and 3) is listed below with its assigned milestone. No feature is left unassigned.
| # | Feature | Description | Milestone | Source |
|---|---------|-------------|-----------|--------|
| F1 | Canonical 1.13c Online Item Tooltip Bridge (R1.1) | Bridge online `ItemData` (`item-bitstream.ts`) and socketed child items (`world.containers.sockets.get(item.id)`) through canonical 1.13c item tables (`getEmbeddedDropTables()`) and `ItemStatCost` descriptor formatter (`item-bridge.ts`, `item-tooltip.ts`, `hud-model.ts`). Resolve localized CHI/ENG base item names, Magic/Rare/Set/Unique/Runeword names, 1H/2H weapon damage, defense, durability, level/str/dex/class requirements, sorted `ItemStatCost` property lines (`stats` and `statsEn` instead of raw `Stat #N`), and socketed child item previews (`socketedItems`, `usedSockets`). | M1, M3 | R1.1, Survey 1 |
| F2 | C2S Container, Belt, NPC, Merc, Warp, Party & Trade Packet Encoders (R1.2, R2–R5) | Implement/complete C2S binary encoders and `ClientCommand` + `encodeClientCommand` (`registry.ts`) mappings matching `C2S_PACKET_SIZES`: `0x09 WarpTo` (9B), `0x1D SwapCursorBodyItem` (`ItemSwap`, 9B), `0x1F SwapContainerItem` (17B), `0x23 BeltPut` (9B), `0x24 BeltRemove` (5B), `0x25 BeltSwap` (9B), `0x27 IdentifyItem` (9B), `0x28 SocketItem` (9B), `0x29 ScrollToBook` (9B), `0x31 QuestMessage` (9B), `0x36 HireMerc` (`MercHire`, 9B), `0x38 EntityAction` (`NpcEntityAction`, 13B), `0x41 Resurrect` (1B), `0x4F Trade` (`ClickButton`, 7B), `0x5D Hostile` (7B with `button` parameter `1`=hostile, `2`=loot), `0x5E Party` (`PartyAction`, 6B), `0x61 MercItem` (3B), `0x62 NpcResurrectMerc` (`MercResurrect`, 5B). | M1 | R1.2, Acceptance Criteria, Survey 1–3 |
| F3 | Server-Authoritative Container, Belt, Identify, Socket & Tome UI Wiring (R1.2) | Wire `HudCommandSink`, `HudModel`, and `HudManager` so: (a) left-clicking an occupied equipment slot with a cursor item emits `0x28 SocketItem` (if socketable) or `0x1D ItemSwap` (9B); (b) left-clicking an occupied inventory/stash/cube cell with a cursor item emits `0x29 ScrollToBook` (matching scroll into tome), `0x28 SocketItem` (compatible filler into socketed item), or `0x1F SwapContainerItem` (17B); (c) left-clicking belt slots emits `0x24 BeltRemove` (empty cursor on occupied slot), `0x23 BeltPut` (cursor potion on empty slot), or `0x25 BeltSwap` (cursor potion on occupied slot); (d) right-clicking `isc`/`ibk` in online mode arms identify targeting mode and subsequent left-click on an unidentified item dispatches `0x27 IdentifyItem`. | M3 | R1.2, Survey 1 |
| F4 | Weapon Set II (`bodyLoc` 11 & 12) Sync (R1.3) | Map `bodyLoc` 11 (`rarm2`) and 12 (`larm2`) in `HudModel.syncFromWorld` / `syncInventory` to `hud.inventory.swapWeapons` (`weapon1` and `weapon2`) so inactive weapon slots remain synced across `0x60` / `0x97 WeaponSwitch`. | M3 | R1.3, Survey 1 |
| F5 | Act 1–5 Town & Quest NPC Descriptors (R2.1) | Complete `TOWN_NPC_DESCRIPTORS` and export `NPC_HCIDX_TO_DESCRIPTOR_ID` in `src/common/units/npc-table.ts` for all Deckard Cain `hcIdx` variants (`146`, `244`, `245`, `246`, `520`, `265`), Warriv Act 1 & Act 2 (`155`, `175`), Meshif Act 2 & Act 3 (`210`, `258`), Kaelan (`203`), Tyrael (`251`, `367`), Hadriel (`259`), and Qual-Kehk (`515`), including `canHire`, `travelActions`, `questRewardActions`, `gossipLines`, and `questLines`. | M1 | R2.1, Survey 2 |
| F6 | NPC Info (`0x27`), SetNPCTalk (`0x91`), Talk Submenu & `0x31 QuestMessage` (R2.2) | Process S2C `0x27 NPCInfo`, `0x91 SetNPCTalk`, and `0xAB NPCHeal` in `ClientWorld.apply` (`case 'Npc'`, preserving backward-compatible immediate menu opening on outgoing `NpcInit` for synthetic tests) [M1]. Upgrade the `Talk` (`交談`) menu in `WorldPanelsHud` and `HudModel` to support `Introduction` / `Gossip` / active Quest Speech topics with a scrollable `.tbl` speech dialog box and C2S `0x31 QuestMessage` dispatch [M3]. | M1, M3 | R2.2, Survey 2 |
| F7 | Act Travel Menu Options & C2S `0x38 EntityAction` (R2.3) | Add Act transition menu options (`travel_east`, `travel_west`, `travel_harrogath`) to Warriv (`155`, `175`), Meshif (`210`, `258`), and Tyrael (`367`) and wire them in `WorldPanelsHud` and `HudModel` to C2S `0x38 EntityAction` travel commands. | M3 | R2.3, Survey 2 |
| F8 | Mercenary World State (`0x4E`, `0x4F`, `0x9B` & `lifePct`) (R3.1) | In `ClientWorld.apply` (`case 'Merc'`), store `0x4E MercForHire` candidate entries (`nameId`, `seed`), clear the list on `0x4F MercForHireListStart`, store `0x9B MercReviveCost` (`reviveCost`, `deadNameId`), track active mercenary life percentage (`lifePct`), and fix `world.containers.merc` unequip-to-cursor handling. | M1 | R3.1, Survey 2 |
| F9 | Mercenary Hire (`0x36`) & Resurrect (`0x62`) UI (R3.2) | Wire the `Hire` NPC menu option (`Kashya` `150`, `Greiz` `198`, `Asheara` `252`, `Qual-Kehk` `515`) to send `0x38 EntityAction (action=3)`, open the Mercenary Hire panel populated from `world.merc.hireList`, and send `0x36 HireMerc` on selection. Show the `Resurrect` option when a dead mercenary is awaiting revival and dispatch `0x62 NpcResurrectMerc`. | M3 | R3.2, Survey 2 |
| F10 | Mercenary HUD Portrait & Paperdoll (`O` Hotkey, `0x61 MercItem`) (R3.3) | Render the active mercenary top-left HUD portrait and life bar in `HudManager`. Sync `world.containers.merc` into the Mercenary Paperdoll panel (`O` hotkey) in `HudModel` / `WorldPanelsHud` / `HudManager`, and dispatch `0x61 MercItem` (3B: `u8 0x61 \| u16 slot`) when clicking mercenary equipment slots. | M3 | R3.3, Survey 2 |
| F11 | Dungeon/Stair Warp Clicking (`0x09 AssignWarp` → C2S `0x09 WarpTo`) (R4.1) | Preserve `warpId` on `0x09 AssignWarp` in `s2c/units.ts` & `ClientUnit` [M1]; project `kind === 'warp'` units in `buildSceneDrawables` (`src/client/view/unit-drawables.ts`) and `ClientWorldSceneSource.pickUnitAtScreen` (`src/client/view/scene-source.ts`) with localized destination labels and emit `MoveToUnit (0x04)` + `WarpTo (0x09)` when clicking a warp entrance [M2]. | M1, M2 | R4.1, Survey 3 |
| F12 | Portal Ownership Hover (`0x60`/`0x82`) & Ground Item Labels (`0x16`) (R4.2) | Store ground item `goldAmount` (`item.quantity`) and resolved item names on `UnitState` / `ClientUnit` and `resolveGroundItemMetadata` [M1]. Enrich Town Portal objects (`classId` 59/60) with `world.portals.get(id)` (`ownerName`, destination `areaId`), replace hardcoded `'1 Gold'` in `world-renderer.ts`, and support `Alt` key ground item label display and click-to-pickup (`0x16`) [M2]. | M1, M2 | R4.2, Survey 3 |
| F13 | `SkillCast` (`0x4C`/`0x4D`/`0x6C`/`0x99`/`0x9A`) Animation (R4.3) | Handle `case 'SkillCast'` in `ClientWorld.apply` by updating the caster unit's active cast state, facing direction toward the target, and attack/cast animation mode [M1] so `unit-drawables.ts` and `world-renderer.ts` animate melee swings and spell casts [M2]. | M1, M2 | R4.3, Survey 3 |
| F14 | Player Death Overlay & C2S `0x41 Resurrect` (R4.4) | Detect player death (`world.self.life <= 0` or `selfUnit.mode === 0` after spawn) in `ClientWorld` [M1], render the classic D2 death overlay (`"You Have Died. Press ESC to continue."` / `"你挂了,请按 ESC 键继续"`) in `HudManager`, and emit `{ type: 'Resurrect' }` (C2S `0x41`) when pressing `Escape` or clicking the overlay [M2, M3]. | M1, M2, M3 | R4.4, Survey 3 |
| F15 | Multiplayer Party State & Party Panel (`P` Hotkey, `0x5D`/`0x5E`) (R5.1) | Handle `memberState` (`0x66`/`0x7F`), `relation` (`0x8B`/`0x8C`/`0x74`/`0x8E`), `memberPos` (`0x90`), and player leave (`0x5C`) in `ClientWorld.apply` [M1]. Implement automap party player markers in `world-renderer.ts` [M2] and the Party Screen (`P` hotkey) in `WorldPanelsHud` + `HudModel` + `HudManager` with Invite/Accept/Cancel/Leave (`0x5E Party`), Hostile toggle (`0x5D Hostile` button=1), Corpse Loot toggle (`0x5D Hostile` button=2), and top-left party portraits [M3]. | M1, M2, M3 | R5.1, Survey 3 |
| F16 | Player-to-Player Trade UI (`0x77`/`0x78`/`0x79` & C2S `0x4F`) & Quest Reward Item Panels (R5.2, R5.3) | Track `0x77 ButtonActions`, `0x78 TradeAccepted` (`partnerName`), and `0x79 GoldInTrade` in `ClientWorld.apply` [M1]. Render the 2-player Trade left-dock panel (`tradePartner` top 10×4 grid + partner gold, `tradeSelf` bottom 10×4 grid + self gold + Trade Accept check button sending `0x4F Trade`) and wire `imbue` (Charsi), `add_sockets` (Larzuk), and `personalize` (Anya) NPC menu actions to open the single-item quest reward box and dispatch the corresponding C2S action in `WorldPanelsHud` + `HudModel` + `HudManager` [M3]. | M1, M3 | R5.2, R5.3, Survey 2 & 3 |
---
## Milestones
| # | Name | Scope | Dependencies | Status |
|---|------|-------|-------------|--------|
| M1 | Protocol Layer, Common Item/NPC Tables & `ClientWorld` Authoritative State (`src/netproto/`, `src/common/`, `src/client/world/`) | F1 (bridge helper), F2, F5, and state layer of F6, F8, F11, F12, F13, F14, F15, F16: Complete all C2S encoders (`0x09`, `0x1D`, `0x1F`, `0x23`–`0x29`, `0x31`, `0x36`, `0x38`, `0x41`, `0x4F`, `0x5D` with `button`, `0x5E`, `0x61`, `0x62`), `ClientCommand` & `encodeClientCommand` (`registry.ts`), S2C `0x09` `warpId` & `0x78` `partnerName`, `TOWN_NPC_DESCRIPTORS` + `NPC_HCIDX_TO_DESCRIPTOR_ID` (`npc-table.ts`), canonical 1.13c `itemDataToUiInventoryItem` converter (`item-bridge.ts`), `goldAmount` resolution (`ground-items.ts`), and all `ClientWorld.apply` reducers (`Npc`, `Merc`, `SkillCast`, `Party`, `Trade`, `isDead`, ground `goldAmount`, warp `warpId`). Verified in `tests/client/client-world.test.ts` & `tests/netproto/d2gs-{c2s,s2c}.test.ts` (14/14 PASS, 0 TS errors). | none | DONE |
| M2 | Scene View, Warp Picking, Portals, Ground Item Labels & `SkillCast` Visuals (`src/client/view/`, `src/client/scene/`, `src/client/session/`, `src/client/input/`) | F11, F12, F13, F14 (Escape/input), F15 (automap markers): Project `kind === 'warp'` in `unit-drawables.ts` and `ClientWorldSceneSource.pickUnitAtScreen` (`scene-source.ts`) with localized destination labels emitting `MoveToUnit (0x04)` + `WarpTo (0x09)`; enrich Town Portal (`classId` 59/60) with `world.portals.get(id)` (`ownerName`, `areaId`); propagate ground `goldAmount` and resolved item names to `UnitState` & `world-renderer.ts` (replacing `'1 Gold'`) with `Alt` label display & click-to-pickup (`0x16`); animate `SkillCast` melee swings & spell casts; add automap party/portal markers. Verified in `tests/client/hud-session-play.test.ts` (5/5 PASS, 0 TS errors). | M1 | DONE |
| M3 | HUD Model, `HudManager` & `WorldPanelsHud` — Tooltips, Containers, NPC Dialogue/Travel, Mercs, Death Overlay, Party, Trade & Quest Rewards (`src/client/ui-model/`, `src/client/ui/`) | F1, F3, F4, F6, F7, F9, F10, F14 (HUD overlay), F15 (Party UI & portraits), F16: Wire canonical 1.13c online item tooltips (`stats`, `statsEn`, `socketedItems`, `usedSockets`) & `bodyLoc` 11/12 `swapWeapons`; wire `0x1D`, `0x1F`, `0x23`–`0x29` container/belt/identify/socket/tome clicks; upgrade `Talk` submenu (`Introduction`/`Gossip`/Quest speech + `.tbl` dialog + `0x31 QuestMessage`) & Act Travel (`travel_east`/`travel_west`/`travel_harrogath` -> `0x38`); wire Mercenary `Hire` (`0x38` action=3 -> `0x36`), `Resurrect` (`0x62`), top-left Merc portrait, and `O` Paperdoll (`0x61 MercItem`); render Player Death overlay & `0x41 Resurrect`; implement `P` Party Screen (`0x5E`, `0x5D` button=1/2) & party portraits; implement 2-player Trade panel (`0x4F`) & Quest Reward panels (`imbue`, `add_sockets`, `personalize`). Verified in `tests/client/hud-server-authority.test.ts` and `tests/client/vendor-stash-ui.test.ts` (45/45 PASS). | M1 | DONE |
| M4 | Final Milestone: Pass 100% E2E Test Suite (Tiers 1–4) & Adversarial Coverage Hardening (Tier 5) | Phase 1: Wait for `TEST_READY.md`, run and pass 100% of E2E tests (`tests/e2e-d2gs-online/**` Tiers 1–4) and all unit/integration/boundary suites + `npx tsc --noEmit`. Phase 2: White-box Tier 5 adversarial coverage hardening (`tests/e2e-d2gs-online/tier5-adversarial-hardening.test.ts`, 204/204 E2E tests PASS, 269/269 14-file verification suite PASS, 0 TS errors; Unanimous 5/5 Verification Gate PASS across 2 Reviewers, 2 Challengers, and 1 Forensic Auditor). | M2, M3, E2E | DONE |
| E2E | E2E Testing Track (`TEST_INFRA.md`, Tiers 1–4 Test Suite, `TEST_READY.md`) | Design and create requirement-driven, opaque-box 4-tier E2E test suite in `tests/e2e-d2gs-online/` covering all 16 features (`F1`–`F16`) with `80` Tier 1 tests, `80` Tier 2 tests, `16` Tier 3 tests, and `8` Tier 4 application scenarios (`184` total, plus `20` Tier 5 tests = `204` total); publish `TEST_INFRA.md` and `TEST_READY.md`. | none | DONE |
---
## Interface Contracts
### 1. `src/netproto/` ↔ `src/client/` (`ClientCommand` & C2S Encoders)
All newly added and updated C2S encoders MUST produce a `Uint8Array` whose `byteLength` equals `C2S_PACKET_SIZES[opcode]` in `src/netproto/d2gs/tables/s2c-sizes.ts`:
- `0x09 WarpTo` (9B: `u8 0x09 | u32LE unitType(5) | u32LE warpId`):
- `encodeC2sWarpTo(warpId: number, unitType?: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'TakeWarp'; readonly warpId: number }` and `{ readonly type: 'WarpTo'; readonly warpId: number; readonly unitType?: number }` (both encoded as opcode `0x09`, 9B).
- `0x1D SwapCursorBodyItem` (9B: `u8 0x1D | u32LE itemId | u16LE slot | u16LE 0`):
- `encodeC2sItemSwap(itemId: number, slot: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'ItemSwap'; readonly itemId: number; readonly slot: number }`
- `0x1F SwapContainerItem` (17B: `u8 0x1F | u32LE cursorItemId | u32LE targetItemId | u32LE x | u32LE y`):
- `encodeC2sSwapContainerItem(cursorItemId: number, targetItemId: number, x: number, y: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'SwapContainerItem'; readonly cursorItemId: number; readonly targetItemId: number; readonly x: number; readonly y: number }`
- `0x23 BeltPut` (9B: `u8 0x23 | u32LE itemId | u32LE slot`):
- `encodeC2sBeltPut(itemId: number, slot: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'BeltPut'; readonly itemId: number; readonly slot: number }`
- `0x24 BeltRemove` (5B: `u8 0x24 | u32LE itemId`):
- `encodeC2sBeltRemove(itemId: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'BeltRemove'; readonly itemId: number }`
- `0x25 BeltSwap` (9B: `u8 0x25 | u32LE cursorItemId | u32LE beltItemId`):
- `encodeC2sBeltSwap(cursorItemId: number, beltItemId: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'BeltSwap'; readonly cursorItemId: number; readonly beltItemId: number }`
- `0x27 IdentifyItem` (9B: `u8 0x27 | u32LE itemId | u32LE scrollId`):
- `encodeC2sIdentifyItem(itemId: number, scrollId: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'IdentifyItem'; readonly itemId: number; readonly scrollId: number }`
- `0x28 SocketItem` (9B: `u8 0x28 | u32LE itemId | u32LE targetItemId`):
- `encodeC2sSocketItem(itemId: number, targetItemId: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'SocketItem'; readonly itemId: number; readonly targetItemId: number }`
- `0x29 ScrollToBook` (9B: `u8 0x29 | u32LE scrollId | u32LE bookId`):
- `encodeC2sScrollToBook(scrollId: number, bookId: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'ScrollToBook'; readonly scrollId: number; readonly bookId: number }`
- `0x31 QuestMessage` (9B: `u8 0x31 | u32LE npcId | u32LE messageId`):
- `encodeC2sQuestMessage(npcId: number, messageId: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'QuestMessage'; readonly npcId: number; readonly messageId: number }`
- `0x36 HireMerc` (9B: `u8 0x36 | u32LE npcId | u32LE mercId`):
- `encodeC2sNpcMercHire(npcId: number, mercId: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'MercHire'; readonly npcId: number; readonly mercId: number }`
- `0x38 EntityAction` (13B: `u8 0x38 | u32LE action | u32LE npcId | u32LE complement`):
- `encodeC2sNpcEntityAction(action: number, npcId: number, complement?: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'NpcEntityAction'; readonly action: number; readonly npcId: number; readonly complement?: number }` (in addition to `NpcOpenTrade` and `NpcOpenGamble`)
- `0x41 Resurrect` (1B: `u8 0x41`):
- `encodeC2sResurrect(): Uint8Array`
- `ClientCommand`: `{ readonly type: 'Resurrect' }`
- `0x4F Trade` / `ClickButton` (7B: `u8 0x4F | u16LE buttonId | u16LE amountLow | u16LE amountHigh`):
- `encodeC2sClickButton(buttonId: number, complement?: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'ClickButton'; readonly buttonId: number; readonly complement?: number }`
- `0x5D Hostile` (7B: `u8 0x5D | u8 button | u8 state | u32LE targetPlayerId`):
- `encodeC2sHostile(targetPlayerId: number, hostile: boolean, button?: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'Hostile'; readonly targetPlayerId: number; readonly hostile: boolean; readonly button?: number }` (`button = 1` for Hostile toggle, `button = 2` for Corpse Loot toggle)
- `0x5E Party` (6B: `u8 0x5E | u8 action | u32LE targetPlayerId`):
- `encodeC2sPartyAction(action: number, targetPlayerId: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'PartyAction'; readonly action: number; readonly targetPlayerId: number }`
- `0x61 MercItem` (3B: `u8 0x61 | u16LE slot`):
- `encodeC2sMercItem(slot: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'MercItem'; readonly slot: number }`
- `0x62 NpcResurrectMerc` (5B: `u8 0x62 | u32LE npcId`):
- `encodeC2sNpcMercResurrect(npcId: number): Uint8Array`
- `ClientCommand`: `{ readonly type: 'MercResurrect'; readonly npcId: number }`
### 2. `src/common/units/npc-table.ts` ↔ `src/client/`
- `TownNpcServiceDescriptor` includes:
- `canHire?: boolean` (`150` Kashya, `198` Greiz, `252` Asheara, `515` Qual-Kehk)
- `travelActions?: readonly ('travel_east' | 'travel_west' | 'travel_harrogath')[]` (`155` Warriv Act 1 -> `['travel_east']`, `175` Warriv Act 2 -> `['travel_west']`, `210` Meshif Act 2 -> `['travel_east']`, `258` Meshif Act 3 -> `['travel_west']`, `367` Tyrael Act 4 -> `['travel_harrogath']`)
- `questRewardActions?: readonly ('imbue' | 'add_sockets' | 'personalize')[]` (`154` Charsi -> `['imbue']`, `511` Larzuk -> `['add_sockets']`, `512` Anya -> `['personalize']`)
- `gossipLines?: readonly string[]`
- `questLines?: Readonly<Record<string, string>>`
- `TOWN_NPC_DESCRIPTORS` and `NPC_HCIDX_TO_DESCRIPTOR_ID` cover all Act 1–5 Town & Quest NPC `hcIdx` values, including Cain (`146`, `244`, `245`, `246`, `520`, `265`), Warriv (`155`, `175`), Meshif (`210`, `258`), Kaelan (`203`), Tyrael (`251`, `367`), Hadriel (`259`), and Qual-Kehk (`515`).
### 3. `ClientWorld` (`src/client/world/client-world.ts`) ↔ `HudModel` & `SceneSource`
- **Mercenary state (`world.merc`)**:
- `hireList: readonly { nameId: number; seed: number }[]` (cleared on `0x4F`, appended on `0x4E`)
- `reviveCost: number`, `deadNameId: number` (populated on `0x9B MercReviveCost`)
- `lifePct: number` (updated on `0x0C`, `0xAB`, `0x69`, `0x81`, `0x9E..0xA0`)
- **NPC interaction state (`world.npcInteraction`)**:
- Stores `0x27 NPCInfo` (`npcInfoRaw`) and `0x91 SetNPCTalk` (`npcTalkRaw`), while keeping immediate `npcId` assignment on outgoing `NpcInit` for synthetic tests.
- Supports `window: 'none' | 'trade' | 'gamble' | 'hire' | 'imbue' | 'add_sockets' | 'personalize'`.
- **Warp, Portal, Ground Item, `SkillCast` & Death state**:
- `kind === 'warp'` / `unitType === 'tile'` units carry `warpId` and localized destination label; `buildSceneDrawables` (`unit-drawables.ts`) and `ClientWorldSceneSource.pickUnitAtScreen` (`scene-source.ts`) expose them as `'warp'` so clicking emits `MoveToUnit (0x04)` + `TakeWarp`/`WarpTo (0x09)`.
- Town Portal objects (`classId` `59`/`60`) join `world.portals.get(id)` (`ownerName`, `areaId`).
- Ground item units carry `goldAmount` (`item.quantity`) and resolved item names.
- `case 'SkillCast'` updates caster unit `castState`, facing `dir`, and attack/cast animation mode (`'A1'`/`'SC'`).
- Player death (`world.self.life <= 0` or `selfUnit.mode === 0` after spawn) sets `isDead`, renders the death overlay (`"You Have Died. Press ESC to continue."` / `"你挂了,请按 ESC 键继续"`), and emits `{ type: 'Resurrect' }` (`0x41`) on `Escape` or overlay click.
- **Party & Trade state**:
- `world.party` tracks `memberState` (`0x66`/`0x7F`), `relation` (`0x8B`/`0x8C`/`0x74`/`0x8E`), `memberPos` (`0x90`), and removes leaving players (`0x5C`).
- `world.trade` tracks `0x77 ButtonActions`, `0x78 TradeAccepted` (`partnerName`), and `0x79 GoldInTrade` (`selfGold`, `partnerGold`), with `tradePartner` and `tradeSelf` 10×4 item grids synced into `WorldPanelsHud`.
---
## Code Layout & Write Ownership
- **M1 (`sub_orch_m1_d2gs`)**: Owns `src/netproto/**`, `src/common/**`, `src/client/world/**`, and `tests/client/client-world.test.ts` (plus any `tests/netproto/**` packet tests).
- **M2 (`sub_orch_m2_d2gs`)** *(after M1 completes; runs in parallel with M3)*: Owns `src/client/view/**`, `src/client/scene/**`, `src/client/session/**`, `src/client/input/**`, and `tests/client/hud-session-play.test.ts`.
- **M3 (`sub_orch_m3_d2gs`)** *(after M1 completes; runs in parallel with M2)*: Owns `src/client/ui-model/**`, `src/client/ui/**`, `tests/client/hud-server-authority.test.ts`, and `tests/client/vendor-stash-ui.test.ts`.
- **E2E (`sub_orch_e2e_d2gs`)** *(runs in parallel with M1–M3)*: Owns EXCLUSIVELY `/usr/local/google/home/taodao/diablo2-web/TEST_INFRA.md`, `/usr/local/google/home/taodao/diablo2-web/TEST_READY.md`, and `/usr/local/google/home/taodao/diablo2-web/tests/e2e-d2gs-online/**`.
- **M4 (`sub_orch_m4_d2gs`)** *(after M2, M3, and E2E complete)*: Owns full-repo E2E verification and Tier 5 adversarial coverage hardening.