diablo2-web/tests/e2e-d2gs-online/tier3-cross-feature.test.ts

694 lines
23 KiB
TypeScript
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.

import { describe, expect, it } from 'vitest'
import {
createOnlineHudSession,
D2StatId,
extractStatStrings,
layoutGroundItemLabels,
makeItemData,
readU16LE,
readU32LE,
resolveGroundItemMetadata,
} from './helpers.ts'
describe('Tier 3 — Pairwise Cross-Feature Integration Coverage (16 Tests)', () => {
it('CF1 (F1 × F3): identifying an unidentified Unique item via 0x27 IdentifyItem + S2C 0x9C reveals canonical 1.13c stats', () => {
const { world, hud, hudModel, sentCommands, sentPackets } = createOnlineHudSession()
const tome = makeItemData({
id: 4001,
code: 'ibk',
location: 0,
container: 'inventory',
gridX: 0,
gridY: 0,
invWidth: 1,
invHeight: 2,
})
const unidAxe = makeItemData({
id: 4002,
code: 'hax',
quality: 7,
uniqueItemId: 0, // The Gnasher
identified: false,
location: 0,
container: 'inventory',
gridX: 2,
gridY: 0,
invWidth: 1,
invHeight: 3,
})
world.apply({ type: 'ItemEvent', item: tome })
world.apply({ type: 'ItemEvent', item: unidAxe })
hudModel.syncFromWorld()
expect(hud.inventory.inventory.find((i) => i.id === 4002)?.identified).toBe(false)
;(hudModel as Record<string, any>).identifyItem(4002, 4001)
expect(sentCommands).toContainEqual({ type: 'IdentifyItem', itemId: 4002, scrollId: 4001 })
expect(sentPackets[0]![0]).toBe(0x27)
// Server responds with identified item and canonical 1.13c stats
world.apply({
type: 'ItemEvent',
item: makeItemData({
...unidAxe,
action: 0x02,
identified: true,
stats: [
{ statId: 17, param: 0, value: 65 },
{ statId: 18, param: 0, value: 65 },
{ statId: 135, param: 0, value: 50 },
],
}),
})
hudModel.syncFromWorld()
const identifiedAxe = hud.inventory.inventory.find((i) => i.id === 4002)!
expect(identifiedAxe.identified).toBe(true)
const statsText = extractStatStrings(identifiedAxe.stats).join('\n')
expect(statsText).not.toMatch(/Stat #\d+/i)
expect(statsText).toMatch(/65%|50%/)
})
it('CF2 (F1 × F3): socketing a Rune into a socketed helm via 0x28 SocketItem + S2C 0x9D updates socketedItems and tooltip', () => {
const { world, hud, hudModel, sentCommands, sentPackets } = createOnlineHudSession()
const helm = makeItemData({
id: 4010,
code: 'cap',
location: 0,
container: 'inventory',
gridX: 0,
gridY: 0,
invWidth: 2,
invHeight: 2,
socketed: true,
totalSockets: 2,
usedSockets: 0,
stats: [{ statId: D2StatId.ARMORCLASS, param: 0, value: 5 }],
})
const tirRune = makeItemData({
id: 4011,
code: 'r03',
location: 4,
container: 'cursor',
})
world.apply({ type: 'ItemEvent', item: helm })
world.apply({ type: 'ItemEvent', item: tirRune })
hudModel.syncFromWorld()
;(hudModel as Record<string, any>).socketItem(4011, 4010)
expect(sentCommands).toContainEqual({ type: 'SocketItem', itemId: 4011, targetItemId: 4010 })
expect(sentPackets[0]![0]).toBe(0x28)
// Server confirms socket insertion
world.apply({
type: 'ItemEvent',
item: makeItemData({ ...helm, usedSockets: 1, socketedChildCount: 1 }),
})
world.apply({
type: 'ItemEvent',
item: makeItemData({
...tirRune,
opcode: 0x9d,
action: 0x0b,
location: 6,
container: 'socket',
inSocket: true,
ownerType: 4,
ownerId: 4010,
gridX: 0,
}),
})
hudModel.syncFromWorld()
const syncedHelm = hud.inventory.inventory.find((i) => i.id === 4010)!
expect(syncedHelm.usedSockets).toBe(1)
expect(syncedHelm.socketedItems?.length).toBe(1)
})
it('CF3 (F2 × F3): packing Scroll of Identify (isc) into Tome of Identify (ibk) emits 0x29 ScrollToBook and syncs tome quantity', () => {
const { world, hudModel, sentCommands, sentPackets } = createOnlineHudSession()
const tome = makeItemData({
id: 4020,
code: 'ibk',
location: 0,
container: 'inventory',
gridX: 0,
gridY: 0,
quantity: 5,
stats: [{ statId: D2StatId.QUANTITY, param: 0, value: 5 }],
})
const scroll = makeItemData({
id: 4021,
code: 'isc',
location: 4,
container: 'cursor',
})
world.apply({ type: 'ItemEvent', item: tome })
world.apply({ type: 'ItemEvent', item: scroll })
hudModel.syncFromWorld()
;(hudModel as Record<string, any>).scrollToBook(4021, 4020)
expect(sentCommands).toContainEqual({ type: 'ScrollToBook', scrollId: 4021, bookId: 4020 })
expect(sentPackets[0]![0]).toBe(0x29)
expect(readU32LE(sentPackets[0]!, 1)).toBe(4021)
expect(readU32LE(sentPackets[0]!, 5)).toBe(4020)
})
it('CF4 (F1 × F4): equipping Rare/Unique weapon in Weapon Set II (bodyLoc 11 & 12) syncs 1.13c stats across weapon switch', () => {
const { world, hud, hudModel } = createOnlineHudSession()
world.apply({
type: 'ItemEvent',
item: makeItemData({
id: 4030,
code: 'bsd',
quality: 6,
rareName1: 3,
rareName2: 8,
location: 1,
container: 'equipped',
bodyLoc: 11,
equippedSlot: 11,
stats: [
{ statId: 17, param: 0, value: 80 },
{ statId: 18, param: 0, value: 80 },
],
}),
})
hudModel.syncFromWorld()
expect(hud.inventory.swapWeapons.weapon1).not.toBeNull()
expect(hud.inventory.swapWeapons.weapon1?.quality).toBe('rare')
const statLines = extractStatStrings(hud.inventory.swapWeapons.weapon1?.stats).join('\n')
expect(statLines).not.toMatch(/Stat #\d+/i)
expect(statLines).toMatch(/80%/)
world.apply({ type: 'WeaponSwitch' })
hudModel.syncFromWorld()
expect(hud.inventory.activeWeaponSlot).toBe(1)
expect(hud.inventory.swapWeapons.weapon1?.id).toBe(4030)
})
it('CF5 (F5 × F6): Deckard Cain (hcIdx 244) interaction via 0x59 + S2C 0x27/0x91 opens Cain menu and Talk sends 0x31 QuestMessage', () => {
const { world, worldPanels, hudModel, sentCommands, sentPackets } = createOnlineHudSession()
world.apply({
type: 'UnitAssign',
unit: { unitType: 1, id: 4040, classId: 244, name: 'Deckard Cain', x: 5104, y: 5054 },
})
world.noteOutgoingCommand({ type: 'NpcInit', target: { unitType: 1, id: 4040 } })
world.apply({
type: 'Npc',
kind: 'info',
unitType: 1,
unitId: 4040,
raw: new Uint8Array(40),
})
world.apply({
type: 'Npc',
kind: 'menu',
npcUnitId: 4040,
raw: new Uint8Array(26),
})
hudModel.syncFromWorld()
const optIds = worldPanels.npcMenu?.options.map((o) => o.id) ?? []
expect(optIds).toContain('identify')
expect(optIds).toContain('talk')
worldPanels.clickNpcMenuOption('talk')
if (!sentCommands.some((c) => c.type === 'QuestMessage')) {
const subTopic = worldPanels.npcMenu?.options.find((o) => String(o.id).startsWith('talk_'))
if (subTopic) worldPanels.clickNpcMenuOption(subTopic.id)
}
expect(sentCommands.some((c) => c.type === 'QuestMessage')).toBe(true)
expect(sentPackets.some((p) => p[0] === 0x31 && p.byteLength === 9)).toBe(true)
})
it('CF6 (F5 × F7): Warriv Act 1 (155) travel_east (0x38) -> S2C LoadAct (Act 2) -> Warriv Act 2 (175) travel_west', () => {
const { world, worldPanels, hudModel, sentCommands, sentPackets, clearRecorded } =
createOnlineHudSession({ act: 0, areaId: 1 })
world.apply({
type: 'UnitAssign',
unit: { unitType: 1, id: 4050, classId: 155, name: 'Warriv', x: 5104, y: 5054 },
})
world.noteOutgoingCommand({ type: 'NpcInit', target: { unitType: 1, id: 4050 } })
hudModel.syncFromWorld()
worldPanels.clickNpcMenuOption('travel_east' as any)
expect(sentCommands).toContainEqual(
expect.objectContaining({ type: 'NpcEntityAction', npcId: 4050 }),
)
expect(sentPackets.some((p) => p[0] === 0x38 && p.byteLength === 13)).toBe(true)
// Server transitions player to Act 2 Lut Gholein
clearRecorded()
world.apply({ type: 'LoadAct', act: 1, mapSeed: 0x2222, areaId: 40, objectSeed: 0x3333 })
world.apply({
type: 'UnitAssign',
unit: { unitType: 1, id: 4051, classId: 175, name: 'Warriv', x: 5104, y: 5054 },
})
world.noteOutgoingCommand({ type: 'NpcInit', target: { unitType: 1, id: 4051 } })
hudModel.syncFromWorld()
expect(worldPanels.npcMenu?.options.some((o) => o.id === ('travel_west' as any))).toBe(true)
worldPanels.clickNpcMenuOption('travel_west' as any)
expect(sentCommands).toContainEqual(
expect.objectContaining({ type: 'NpcEntityAction', npcId: 4051 }),
)
})
it('CF7 (F8 × F9): Kashya (150) hire menu -> 0x38 (action=3) -> S2C 0x4F/0x4E candidates -> 0x36 MercHire -> 0x81 AssignMerc', () => {
const { world, worldPanels, hudModel, sentCommands, sentPackets } = createOnlineHudSession({
selfId: 1,
})
world.apply({
type: 'UnitAssign',
unit: { unitType: 1, id: 4060, classId: 150, name: 'Kashya', x: 5104, y: 5054 },
})
world.noteOutgoingCommand({ type: 'NpcInit', target: { unitType: 1, id: 4060 } })
hudModel.syncFromWorld()
worldPanels.clickNpcMenuOption('hire' as any)
expect(sentCommands).toContainEqual(
expect.objectContaining({ type: 'NpcEntityAction', action: 3, npcId: 4060 }),
)
world.apply({ type: 'Merc', kind: 'hireListStart' })
world.apply({ type: 'Merc', kind: 'forHire', nameId: 7, seed: 0xabcdef })
hudModel.syncFromWorld()
;(hudModel as Record<string, any>).hireMerc(4060, 7)
expect(sentCommands).toContainEqual({ type: 'MercHire', npcId: 4060, mercId: 7 })
expect(sentPackets.some((p) => p[0] === 0x36 && p.byteLength === 9)).toBe(true)
world.apply({
type: 'Merc',
kind: 'assign',
ownerId: 1,
mercId: 500,
classId: 271,
nameId: 7,
})
hudModel.syncFromWorld()
expect(world.merc?.mercId).toBe(500)
})
it('CF8 (F8 × F9): mercenary lethal Damage (0x0C) + 0x9B MercReviveCost enables Greiz (198) resurrect -> 0x62 MercResurrect', () => {
const { world, worldPanels, hudModel, sentCommands, sentPackets } = createOnlineHudSession({
selfId: 1,
})
world.apply({ type: 'Merc', kind: 'assign', ownerId: 1, mercId: 500, classId: 338 })
world.apply({
type: 'Damage',
unitType: 1,
unitId: 500,
lifePct: 0,
raw: new Uint8Array(9),
})
world.apply({ type: 'Merc', kind: 'reviveCost', deadNameId: 12, reviveCost: 11000 })
world.apply({
type: 'UnitAssign',
unit: { unitType: 1, id: 4070, classId: 198, name: 'Greiz', x: 5104, y: 5054 },
})
world.noteOutgoingCommand({ type: 'NpcInit', target: { unitType: 1, id: 4070 } })
hudModel.syncFromWorld()
expect(worldPanels.npcMenu?.options.some((o) => o.id === ('resurrect' as any))).toBe(true)
worldPanels.clickNpcMenuOption('resurrect' as any)
expect(sentCommands).toContainEqual({ type: 'MercResurrect', npcId: 4070 })
expect(sentPackets.some((p) => p[0] === 0x62 && p.byteLength === 5)).toBe(true)
})
it('CF9 (F1 × F10): equipping 1.13c items onto mercenary (world.containers.merc) syncs tooltips in O paperdoll and emits 0x61 MercItem', () => {
const { world, hud, hudModel, dom, sentCommands, sentPackets } = createOnlineHudSession({
selfId: 1,
})
world.apply({ type: 'Merc', kind: 'assign', ownerId: 1, mercId: 500, classId: 338 })
world.apply({
type: 'ItemEvent',
item: makeItemData({
id: 4080,
opcode: 0x9d,
action: 0x06,
code: 'scy', // Scythe
quality: 4,
location: 1,
container: 'merc',
bodyLoc: 4,
equippedSlot: 4,
stats: [
{ statId: 17, param: 0, value: 50 },
{ statId: 18, param: 0, value: 50 },
],
}),
})
hudModel.syncFromWorld()
dom.dispatchKeyDown('o')
const mercState =
(hud as Record<string, any>).merc ?? (hud.worldPanels as Record<string, any>).merc
const weapon =
mercState?.equipped?.weapon1 ?? mercState?.equipped?.rightHand ?? mercState?.weapon1
expect(weapon).toBeDefined()
expect(weapon.twoHandDamage).toBeDefined()
;(hudModel as Record<string, any>).mercItem(4)
expect(sentCommands).toContainEqual({ type: 'MercItem', slot: 4 })
expect(sentPackets.some((p) => p[0] === 0x61 && p.byteLength === 3)).toBe(true)
})
it('CF10 (F3 × F8 × F10): Shift+useBeltSlot dispatches 0x26 BeltUse (useOnMerc=1) and S2C 0xAB NPCHeal restores merc portrait lifePct', () => {
const { world, hudModel, sentCommands, sentPackets } = createOnlineHudSession({ selfId: 1 })
world.apply({
type: 'UnitAssign',
unit: { unitType: 1, id: 500, classId: 338, name: 'Emilio', x: 5102, y: 5052, lifePct: 40 },
})
world.apply({ type: 'Merc', kind: 'assign', ownerId: 1, mercId: 500, classId: 338 })
world.apply({
type: 'Damage',
unitType: 1,
unitId: 500,
lifePct: 40,
raw: new Uint8Array(9),
})
world.apply({
type: 'ItemEvent',
item: makeItemData({
id: 4090,
code: 'hp4',
location: 2,
container: 'belt',
gridX: 0,
x: 0,
}),
})
hudModel.syncFromWorld()
hudModel.useBeltSlot(0, true)
expect(sentCommands).toContainEqual({ type: 'BeltUse', itemId: 4090, useOnMerc: 1 })
expect(sentPackets[0]![0]).toBe(0x26)
expect(readU32LE(sentPackets[0]!, 5)).toBe(1)
world.apply({ type: 'Npc', kind: 'heal', unitType: 1, unitId: 500, lifePct: 128 })
hudModel.syncFromWorld()
expect((world.merc as Record<string, any>).lifePct).toBe(128)
})
it('CF11 (F11 × F12): clicking dungeon warp (0x04 + 0x09) and spawning Town Portal (0x60/0x82) resolves warp and portal hover labels', () => {
const { world, onlineSession, commandMapper, sentCommands, sentPackets } =
createOnlineHudSession({ x: 5100, y: 5050 })
world.apply({
type: 'UnitAssign',
unit: {
unitType: 5,
id: 4100,
classId: 4,
warpId: 4,
name: 'To Den of Evil',
x: 5103,
y: 5053,
} as any,
})
const warpUnit = world.getUnit('tile', 4100)!
commandMapper.triggerPrimaryWorldAction(5103, 5053, warpUnit, 5000)
expect(sentCommands.some((c) => c.type === 'MoveToUnit')).toBe(true)
expect(sentPackets.some((p) => p[0] === 0x09 && p.byteLength === 9)).toBe(true)
world.apply({
type: 'UnitAssign',
unit: { unitType: 2, id: 4101, classId: 59, name: 'Town Portal', x: 5105, y: 5055 },
})
world.apply({
type: 'Portal',
kind: 'ownership',
ownerId: 1,
ownerName: 'Hero',
localId: 4101,
remoteId: 4102,
})
const portalRenderable = onlineSession.buildRenderableUnits().find((u) => u.id === 4101)
expect(portalRenderable?.name).toContain("Hero's Town Portal")
})
it('CF12 (F1 × F12): ground item drop (750 Gold + Unique Cap) -> Alt label layout -> 0x16 ItemPickup -> S2C 0x9C inventory tooltip', () => {
const { world, hud, hudModel, commandMapper, sentCommands } = createOnlineHudSession()
const groundGold = makeItemData({
id: 4110,
action: 0x01,
code: 'gld',
location: 3,
container: 'ground',
worldX: 5102,
worldY: 5052,
x: 5102,
y: 5052,
quantity: 750,
})
const groundCap = makeItemData({
id: 4111,
action: 0x01,
code: 'cap',
quality: 7,
uniqueItemId: 0, // Biggin's Bonnet
location: 3,
container: 'ground',
worldX: 5102,
worldY: 5052,
x: 5102,
y: 5052,
invWidth: 2,
invHeight: 2,
})
world.apply({ type: 'ItemEvent', item: groundGold })
world.apply({ type: 'ItemEvent', item: groundCap })
const metaGold = resolveGroundItemMetadata({ id: 4110, code: 'gld', goldAmount: 750 })
const metaCap = resolveGroundItemMetadata({ id: 4111, code: 'cap', quality: 7 })
const boxes = layoutGroundItemLabels([
{ id: 4110, screenX: 400, screenY: 300, label: metaGold.labelEn, color: metaGold.color },
{ id: 4111, screenX: 400, screenY: 300, label: metaCap.labelEn, color: metaCap.color },
])
expect(boxes.length).toBe(2)
expect(boxes[0]!.y).not.toBe(boxes[1]!.y)
const capUnit = world.getUnit('item', 4111)!
commandMapper.triggerPrimaryWorldAction(5102, 5052, capUnit, 6000)
expect(sentCommands).toContainEqual({ type: 'ItemPickup', itemId: 4111, toCursor: false })
// Server moves Cap into player's inventory
world.apply({
type: 'ItemEvent',
item: makeItemData({
...groundCap,
action: 0x00,
location: 0,
container: 'inventory',
gridX: 0,
gridY: 0,
stats: [{ statId: D2StatId.MAXHP, param: 0, value: 15 << 8 }],
}),
})
hudModel.syncFromWorld()
expect(hud.inventory.inventory.find((i) => i.id === 4111)?.quality).toBe('unique')
})
it('CF13 (F13 × F14): monster SkillCast (0x6C) + lethal PlayerLifeManaChange (0x95) triggers death overlay and Escape sends 0x41 Resurrect', () => {
const { world, hudModel, commandMapper, renderFrame, sentCommands, sentPackets } =
createOnlineHudSession({ selfId: 1, life: 100, maxLife: 100 })
world.apply({
type: 'UnitAssign',
unit: { unitType: 1, id: 4120, classId: 1, name: 'Fallen', x: 5105, y: 5055 },
})
world.apply({
type: 'SkillCast',
kind: 'monsterAttack',
monsterId: 4120,
attackType: 0,
targetId: 1,
targetType: 0,
target: { x: 5100, y: 5050 },
})
world.apply({
type: 'PlayerLifeManaChange',
life: 0,
mana: 20,
stamina: 40,
x: 5100,
y: 5050,
vx: 0,
vy: 0,
})
hudModel.syncFromWorld()
const ctx = renderFrame()
expect(ctx.drawnTexts.map((t) => t.text).join('\n')).toMatch(/You Have Died|你挂了|Press ESC/i)
commandMapper.handleKeyDown({ key: 'Escape' })
expect(sentCommands).toContainEqual({ type: 'Resurrect' })
expect(sentPackets.some((p) => p[0] === 0x41 && p.byteLength === 1)).toBe(true)
})
it('CF14 (F8 × F14): simultaneous mercenary death (0x9B) and player death -> Resurrect (0x41) -> Qual-Kehk (515) MercResurrect (0x62)', () => {
const { world, worldPanels, hudModel, commandMapper, sentCommands, sentPackets } =
createOnlineHudSession({ selfId: 1, life: 100, maxLife: 100 })
world.apply({ type: 'Merc', kind: 'assign', ownerId: 1, mercId: 500, classId: 561 })
world.apply({ type: 'Merc', kind: 'reviveCost', deadNameId: 40, reviveCost: 20000 })
world.apply({
type: 'PlayerLifeManaChange',
life: 0,
mana: 0,
stamina: 0,
x: 5100,
y: 5050,
vx: 0,
vy: 0,
})
hudModel.syncFromWorld()
commandMapper.handleKeyDown({ key: 'Escape' })
expect(sentCommands).toContainEqual({ type: 'Resurrect' })
// Server respawns player in Harrogath with life=100
world.apply({
type: 'PlayerLifeManaChange',
life: 100,
mana: 80,
stamina: 100,
x: 5100,
y: 5050,
vx: 0,
vy: 0,
})
world.apply({
type: 'UnitAssign',
unit: { unitType: 1, id: 4130, classId: 515, name: 'Qual-Kehk', x: 5104, y: 5054 },
})
world.noteOutgoingCommand({ type: 'NpcInit', target: { unitType: 1, id: 4130 } })
hudModel.syncFromWorld()
worldPanels.clickNpcMenuOption('resurrect' as any)
expect(sentCommands).toContainEqual({ type: 'MercResurrect', npcId: 4130 })
expect(sentPackets.some((p) => p[0] === 0x62 && p.byteLength === 5)).toBe(true)
})
it('CF15 (F12 × F15): allied party member joins (0x5B/0x8B/0x7F/0x90) and spawns Town Portal (0x82) with ownerName label', () => {
const { world, onlineSession, hudModel } = createOnlineHudSession({ selfId: 1 })
world.apply({
type: 'Party',
kind: 'roster',
playerId: 2,
charClass: 1,
charLevel: 35,
partyId: 1,
charName: 'Jaina',
})
world.apply({
type: 'Party',
kind: 'relation',
subKind: 'partyState',
playerId: 2,
state: 1,
})
world.apply({
type: 'Party',
kind: 'memberState',
isSelf: false,
unitId: 2,
lifePct: 120,
areaId: 2,
})
world.apply({
type: 'Party',
kind: 'memberPos',
playerId: 2,
at: { x: 5120, y: 5070 },
})
world.apply({
type: 'UnitAssign',
unit: { unitType: 2, id: 4140, classId: 59, name: 'Town Portal', x: 5108, y: 5058 },
})
world.apply({
type: 'Portal',
kind: 'ownership',
ownerId: 2,
ownerName: 'Jaina',
localId: 4140,
remoteId: 4141,
})
hudModel.syncFromWorld()
expect(world.party.get(2)?.charName).toBe('Jaina')
const portal = onlineSession.buildRenderableUnits().find((u) => u.id === 4140)
expect(portal?.name).toContain("Jaina's Town Portal")
})
it('CF16 (F1 × F5 × F16): player trade session (0x77/0x78/0x79 -> 0x4F) followed by Larzuk (511) add_sockets quest reward panel', () => {
const { world, worldPanels, hudModel, sentCommands, sentPackets } = createOnlineHudSession({
selfId: 1,
})
world.apply({ type: 'Trade', kind: 'button', action: 0x01 })
world.apply({
type: 'Trade',
kind: 'accepted',
partnerName: 'TraderJoe',
partnerId: 2,
} as any)
world.apply({
type: 'ItemEvent',
item: makeItemData({
id: 4150,
code: 'bsd',
quality: 2,
location: 0,
container: 'tradePartner',
storePage: 2,
storagePage: 2,
gridX: 0,
gridY: 0,
}),
})
hudModel.syncFromWorld()
expect(world.trade.active).toBe(true)
if (typeof (hudModel as Record<string, any>).clickButton === 'function') {
;(hudModel as Record<string, any>).clickButton(0x04, 0)
} else {
;(hudModel as Record<string, any>).sendCommand?.({
type: 'ClickButton',
button: 0x04,
buttonId: 0x04,
complement: 0,
})
}
expect(sentPackets.some((p) => p[0] === 0x4f && readU16LE(p, 1) === 0x04)).toBe(true)
// Trade completes (0x06) and item lands in player's inventory
world.apply({ type: 'Trade', kind: 'button', action: 0x06 })
world.apply({
type: 'ItemEvent',
item: makeItemData({
id: 4150,
code: 'bsd',
quality: 2,
location: 0,
container: 'inventory',
storePage: 0,
storagePage: 0,
gridX: 0,
gridY: 0,
}),
})
world.apply({
type: 'UnitAssign',
unit: { unitType: 1, id: 4151, classId: 511, name: 'Larzuk', x: 5104, y: 5054 },
})
world.noteOutgoingCommand({ type: 'NpcInit', target: { unitType: 1, id: 4151 } })
hudModel.syncFromWorld()
expect(worldPanels.npcMenu?.options.some((o) => o.id === ('add_sockets' as any))).toBe(true)
worldPanels.clickNpcMenuOption('add_sockets' as any)
expect(sentCommands.length).toBeGreaterThan(0)
})
})