174 lines
25 KiB
Markdown
174 lines
25 KiB
Markdown
# 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.
|