diablo2-web/PROJECT.md

25 KiB
Raw Blame History

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:

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.