14 KiB
14 KiB
Test Infrastructure & 4-Tier E2E Test Plan: D2GS v1.13c Online Gameplay Loops & Server Authority
1. Test Philosophy & Ground Truth Invariants
The tests/e2e-d2gs-online/ suite is an opaque-box, requirement-driven 4-tier E2E test suite verifying all 16 features (F1–F16) defined in PROJECT.md and ORIGINAL_REQUEST.md (2026-10-01T13:08:09Z).
Core Verification Principles
- Diablo II v1.13c Binary & Data-Table Ground Truth:
- Every C2S binary packet emitted by
encodeD2gsClientCommand/clientCommandToC2sPacketsis verified againstC2S_PACKET_SIZES[opcode](src/netproto/d2gs/tables/s2c-sizes.ts) and exact little-endian wire layout (u8,u16LE,u32LE). - Every online item tooltip (
onlineItemDataToUiInventoryItemandHudModel.syncFromWorld()) is verified against canonical 1.13c item tables (getEmbeddedDropTables(),Armor.txt,Weapons.txt,Misc.txt,MagicPrefix.txt,MagicSuffix.txt,RarePrefix.txt,RareSuffix.txt,SetItems.txt,UniqueItems.txt,Runes.txt,ItemStatCost.txt) — never rawStat #N: Vfallbacks. - Every Town and Quest NPC across Acts 1–5 is verified against
TOWN_NPC_DESCRIPTORSandNPC_HCIDX_TO_DESCRIPTOR_ID(src/common/units/npc-table.ts).
- Every C2S binary packet emitted by
- Strict Server Authority (Zero Optimistic Client Mutation):
- When
HudManager/WorldPanelsHudhas aHudCommandSink(HudModel) attached (hasCommandSink === true), UI interactions emit typedClientCommandpayloads and C2S wire bytes without mutating local inventory, equipment, belt, stash, cube, mercenary, party, or trade state ahead of the server. - Only authoritative
ServerEventupdates applied toClientWorld(src/client/world/client-world.ts) and synchronized viaHudModel.syncFromWorld()(src/client/ui-model/hud-model.ts) mutate the visible UI state.
- When
- Opaque-Box Contract Testing:
- All tests interact through public module boundaries (
src/netproto/index.ts,src/common/items/item-bridge.ts,src/common/items/ground-items.ts,src/common/units/npc-table.ts,src/client/world/client-world.ts,src/client/ui-model/hud-model.ts,src/client/ui/hud-manager.ts,src/client/ui/world-panels.ts,src/client/view/unit-drawables.ts,src/client/view/scene-source.ts,src/client/input/command-mapper.ts).
- All tests interact through public module boundaries (
2. Complete 16-Feature Inventory (F1–F16)
| ID | Feature | Requirement | Primary Public Contracts Under Test |
|---|---|---|---|
| F1 | Canonical 1.13c Online Item Tooltip Bridge | R1.1 | onlineItemDataToUiInventoryItem, HudModel.syncFromWorld(): CHI/ENG base names, Magic/Rare/Set/Unique/Runeword names, 1H/2H damage, defense, durability, reqLevel/reqStr/reqDex/reqClass, sorted ItemStatCost lines (stats/statsEn), sockets/usedSockets/socketedItems. |
| F2 | C2S Container, Belt, NPC, Merc, Warp, Party & Trade Packet Encoders | R1.2, R2–R5 | encodeD2gsClientCommand & clientCommandToC2sPackets 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, button=1/2), 0x5E Party (PartyAction, 6B), 0x61 MercItem (3B), 0x62 NpcResurrectMerc (MercResurrect, 5B). |
| F3 | Server-Authoritative Container, Belt, Identify, Socket & Tome UI Wiring | R1.2 | HudModel & HudManager: equip slot swap (0x1D) vs socket (0x28); container grid scroll-to-book (0x29), socket (0x28), swap (0x1F); belt put (0x23), remove (0x24), swap (0x25), shift-use on merc (0x26 useOnMerc=1); right-click isc/ibk identify targeting (0x27). |
| F4 | Weapon Set II (bodyLoc 11 & 12) Sync |
R1.3 | HudModel.syncFromWorld(): bodyLoc 11 (rarm2) -> hud.inventory.swapWeapons.weapon1, bodyLoc 12 (larm2) -> hud.inventory.swapWeapons.weapon2, preserved across 0x60/0x97 weapon switch. |
| F5 | Act 1–5 Town & Quest NPC Descriptors | R2.1 | TOWN_NPC_DESCRIPTORS, NPC_HCIDX_TO_DESCRIPTOR_ID, getTownNpcDescriptorByHcIdx: Cain (146, 244, 245, 246, 520, 265), Warriv (155, 175), Meshif (210, 258), Kaelan (203), Tyrael (251, 367), Hadriel (259), Qual-Kehk (515), Kashya (150), Greiz (198), Asheara (252), Charsi (154), Larzuk (511), Anya (512). |
| F6 | NPC Info (0x27), SetNPCTalk (0x91), Talk Submenu & 0x31 QuestMessage |
R2.2 | ClientWorld.apply({ type: 'Npc', kind: 'info' | 'menu' | 'heal' }), synchronous NpcInit menu opening, Talk submenu (Introduction, Gossip, Quest speech), .tbl speech dialog box, and 0x31 QuestMessage. |
| F7 | Act Travel Menu Options & C2S 0x38 EntityAction |
R2.3 | buildNpcMenuOptions & HudModel.handleNpcMenuAction: travel_east (Warriv 155, Meshif 210), travel_west (Warriv 175, Meshif 258), travel_harrogath (Tyrael 367) emitting NpcEntityAction (0x38, 13B). |
| F8 | Mercenary World State (0x4E, 0x4F, 0x9B & lifePct) |
R3.1 | ClientWorld.apply({ type: 'Merc' }): hireListStart (0x4F), forHire (0x4E { nameId, seed }), reviveCost (0x9B { reviveCost, deadNameId }), lifePct updates (0x0C, 0xAB, 0x69, 0x81, 0x9E..0xA0), and world.containers.merc unequip-to-cursor (0x08). |
| F9 | Mercenary Hire (0x36) & Resurrect (0x62) UI |
R3.2 | hire option on mercenary captains (150, 198, 252, 515) -> 0x38 (action=3) -> Hire panel populated from world.mercHireList -> MercHire (0x36, 9B); dead merc (reviveCost > 0) exposes resurrect -> MercResurrect (0x62, 5B). |
| F10 | Mercenary HUD Portrait & Paperdoll (O Hotkey, 0x61 MercItem) |
R3.3 | Top-left mercenary portrait + life bar in HudManager; 'O' hotkey toggles 'merc' panel; world.containers.merc syncs to merc equipment slots (helm, armor, weapon1, weapon2); clicking slot emits MercItem (0x61, 3B). |
| F11 | Dungeon/Stair Warp Clicking (0x09 AssignWarp → C2S 0x09 WarpTo) |
R4.1 | 0x09 AssignWarp preserves warpId on UnitSnapshot & ClientUnit; buildSceneDrawables & ClientWorldSceneSource.pickUnitAtScreen expose 'warp'/'tile' units with localized destination labels; clicking emits MoveToUnit (0x04) + TakeWarp/WarpTo (0x09, 9B). |
| F12 | Portal Ownership Hover (0x60/0x82) & Ground Item Labels (0x16) |
R4.2 | Town Portal (classId 59/60) enriched with world.portals (<ownerName>'s Town Portal, destination areaId); ground gold piles propagate goldAmount (<N> Gold / <N> 金币, never hardcoded '1 Gold'); Alt ground labels layout without overlap and click-to-pickup emits ItemPickup (0x16). |
| F13 | SkillCast (0x4C/0x4D/0x6C/0x99/0x9A) Animation |
R4.3 | ClientWorld.apply({ type: 'SkillCast' }) updates caster unit castState, facing direction toward target, and attack/cast animation mode ('A1'/'SC', mode 7/10 for player, 4/7 for monster). |
| F14 | Player Death Overlay & C2S 0x41 Resurrect |
R4.4 | world.self.life <= 0 (when maxLife > 0) or death mode sets isDead / isSelfDead, renders classic death overlay ("You Have Died. Press ESC to continue." / "你挂了,请按 ESC 键继续"), and pressing Escape or clicking overlay emits { type: 'Resurrect' } (0x41, 1B). |
| F15 | Multiplayer Party State & Party Panel (P Hotkey, 0x5D/0x5E) |
R5.1 | ClientWorld.apply({ type: 'Party' }) handles roster (0x5B/0x5C), relation, memberState, memberPos; 'P' hotkey toggles 'party' panel; Invite/Cancel/Accept/Leave emit PartyAction (0x5E, 6B); Hostile (button=1) and Loot (button=2) emit Hostile (0x5D, 7B); party portraits & automap markers. |
| F16 | Player-to-Player Trade UI (0x77–0x79 & C2S 0x4F) & Quest Reward Item Panels |
R5.2, R5.3 | 0x77 ButtonActions, 0x78 TradeAccepted (partnerName), 0x79 GoldInTrade (selfGold, partnerGold), tradePartner & tradeSelf 10×4 grids, Trade Accept/Cancel (0x4F ClickButton, 7B); Quest Reward panels (imbue on Charsi 154, add_sockets on Larzuk 511, personalize on Anya 512). |
3. 4-Tier Suite Architecture & Coverage Thresholds
tests/e2e-d2gs-online/
├── helpers.ts # Shared fixtures, mock Canvas/DOM harness, packet/item builders, runtime wrappers
├── tier1-feature-coverage.test.ts # Tier 1: Core feature happy-path coverage (80 tests: 5 per F1–F16)
├── tier2-boundary-corner.test.ts # Tier 2: Boundary, corner, negative & anti-regression coverage (80 tests: 5 per F1–F16)
├── tier3-cross-feature.test.ts # Tier 3: Pairwise cross-feature integration coverage (16 tests across F1–F16 pairs)
└── tier4-real-world-scenarios.test.ts # Tier 4: End-to-end multi-step online gameplay session scenarios (8 scenarios)
| Tier | File | Minimum Required | Implemented | Scope |
|---|---|---|---|---|
| Tier 1 | tests/e2e-d2gs-online/tier1-feature-coverage.test.ts |
>= 80 (>= 5 / feature) |
80 (5 × 16) |
Core happy-path behavior for F1–F16 |
| Tier 2 | tests/e2e-d2gs-online/tier2-boundary-corner.test.ts |
>= 80 (>= 5 / feature) |
80 (5 × 16) |
Boundary values, malformed/stale packets, server rejection, zero optimistic mutation |
| Tier 3 | tests/e2e-d2gs-online/tier3-cross-feature.test.ts |
>= 16 |
16 | Pairwise cross-feature workflows (F1×F3, F2×F3, F1×F4, F5×F6, F5×F7, F8×F9, F8×F10, F1×F10, F11×F12, F12×F1, F13×F14, F14×F8, F15×F12, F15×F16, F5×F16, F1×F16) |
| Tier 4 | tests/e2e-d2gs-online/tier4-real-world-scenarios.test.ts |
>= 8 |
8 | Full multi-step online gameplay session loops |
| Total | tests/e2e-d2gs-online/*.test.ts |
>= 184 |
184 | All 16 features (F1–F16) |
4. Tier 4 Real-World Online Gameplay Session Scenarios
- Scenario 1 — Town Identification, Tome Packing, Rune Socketing & Weapon Swap Loop (
F1 + F2 + F3 + F4):- Player picks up an unidentified Unique item, right-clicks Tome of Identify (
ibk) to arm identify targeting, left-clicks the item (0x27 IdentifyItem), receives server0x9Cidentified update with canonical 1.13c stats, packs loose Scroll of Identify (isc) intoibk(0x29 ScrollToBook), sockets a rune into a 2-socket helm (0x28 SocketItem), and verifies Weapon Set II (bodyLoc11/12) persistence across weapon switch (0x97).
- Player picks up an unidentified Unique item, right-clicks Tome of Identify (
- Scenario 2 — Belt Potion Management & Shift-Click Mercenary Feeding (
F2 + F3 + F8 + F10):- Player places potions into belt (
0x23 BeltPut), swaps a belt slot with a Super Healing Potion on cursor (0x25 BeltSwap), removes a belt potion (0x24 BeltRemove), and Shift-uses a belt slot (0x26 UseBeltItemwithuseOnMerc = 1) to heal a damaged Act 2 mercenary whoselifePctsubsequently updates via S2C0x0C/0x9E.
- Player places potions into belt (
- Scenario 3 — Multi-Act Town NPC Dialogue, Cain Identification & Act Travel (
F2 + F5 + F6 + F7):- Player interacts with Deckard Cain (
hcIdx244), opensTalksubmenu (Introduction/Gossip/Quest speech ->0x31 QuestMessage), triggers Cain Identify Items (0x38action=4), then interacts with Warriv (155->travel_eastto Act 2), Meshif (210->travel_eastto Act 3), and Tyrael (367->travel_harrogathto Act 5).
- Player interacts with Deckard Cain (
- Scenario 4 — Mercenary Captain Hiring, Equipment Paperdoll (
O) & Post-Combat Resurrection (F1 + F2 + F8 + F9 + F10):- Player opens Kashya (
150) / Greiz (198) menu, clicksHire(0x38action=3), receives0x4F+0x4Ehire candidates, hires a mercenary (0x36 HireMerc), opens'O'Mercenary Paperdoll, equips/swaps merc gear (0x61 MercItem), takes lethal merc damage (0x9B MercReviveCost), and resurrects the mercenary (0x62 NpcResurrectMerc).
- Player opens Kashya (
- Scenario 5 — Dungeon Warp Traversal, Town Portal Ownership Hover & Ground Loot Pickup (
F2 + F11 + F12):- Server assigns a dungeon warp tile (
0x09 AssignWarp), player hovers/clicks warp (0x04 MoveToUnit+0x09 WarpTo), enters dungeon, hovers an allied Town Portal (0x60/0x82->"Sorceress's Town Portal"), holdsAltto inspect non-overlapping ground labels (750 Gold+ Magic item), and clicks to pick up (0x16 ItemPickup).
- Server assigns a dungeon warp tile (
- Scenario 6 — Combat SkillCast Animation, Player Death Overlay & Town Corpse Resurrection (
F2 + F13 + F14):- Monster and player exchange
SkillCastpackets (0x4C/0x4D/0x6C) updating facing octants and'A1'/'SC'animation modes; lethal damage reducesworld.self.lifeto0, activatingisDeadand rendering"You Have Died. Press ESC to continue."; pressingEscapedispatches0x41 Resurrect(1B), and server respawn restores town state.
- Monster and player exchange
- Scenario 7 — Multiplayer Party Roster, Invite/Accept, Hostile & Corpse Loot Permissions (
F2 + F15):- Second player joins (
0x5BwithpartyId=0xffffand non-emptycharName), player opens'P'Party panel, invites (0x5Eaction=0x06) and accepts (0x5Eaction=0x08), receives partymemberState(0x7F) andmemberPos(0x90) for top-left party portrait and automap marker, toggles Corpse Loot (0x5Dbutton=2) and Hostile (0x5Dbutton=1), and cleans up when peer leaves (0x5C).
- Second player joins (
- Scenario 8 — Player-to-Player Trade Session & Act 1/5 Quest Reward Item Upgrading (
F1 + F2 + F5 + F16):- Player initiates trade (
0x77action=0x01), receivestradePartneritem (container: 'tradePartner') and0x79 GoldInTrade, placestradeSelfitem (container: 'trade'), receives0x78 TradeAcceptedwithpartnerName, clicks Trade Accept (0x4FbuttonId=0x04), completes trade (0x77action=0x06), and uses Charsiimbue(154), Larzukadd_sockets(511), and Anyapersonalize(512) quest reward panels.
- Player initiates trade (