test(client): capture-backed gamble flow (fixture 09, Gheed)
Gamble preview click emits NpcBuy{gamble:true, itemMode:0, shiftFill:false}
with the #10107 type-2 price; the captured 0x2A result 12 shows
TransactionResults10.
This commit is contained in:
parent
812eb55d36
commit
34a895a12b
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* Gamble UI over live capture 09 (Gheed, WebBotThree with 2 gold): the menu sends 0x38 action 2,
|
||||
* the gamble grid is exactly the server's 0x9C AddToShop items, clicking a ring sends a gamble
|
||||
* 0x32 NpcBuy, and the server's rejection (0x2A result 12) shows tbl string 3362.
|
||||
*/
|
||||
import path from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { replayD2CapFile } from '../../tools/netproto-replay.ts'
|
||||
import type { ClientCommand, ServerEvent } from '../../src/netproto/index.ts'
|
||||
import { ClientWorld } from '../../src/client/world/client-world.ts'
|
||||
import { HudManager } from '../../src/client/ui/hud-manager.ts'
|
||||
import { VENDOR_GRID_ORIGIN } from '../../src/client/ui/world-panels.ts'
|
||||
import { requireTbl } from '../../src/client/i18n/lang.ts'
|
||||
import { HudModel } from '../../src/client/ui-model/hud-model.ts'
|
||||
import type { VendorPriceFn } from '../../src/client/ui-model/vendor-price.ts'
|
||||
|
||||
const GAMBLE_CAPTURE = path.resolve(__dirname, '../fixtures/netproto/09-d2gs-gamble.d2cap')
|
||||
/** Gheed (MonStats hcIdx 147) is unit #4 in capture 09. */
|
||||
const GHEED_ID = 4
|
||||
const GHEED_CLASS = 147
|
||||
const MONSTER = 1
|
||||
|
||||
const stubPrice: VendorPriceFn = (item, type) => 1000 * (type + 1) + item.id
|
||||
|
||||
describe('gamble capture 09 -> HudModel / HudManager', () => {
|
||||
it('menu -> NpcOpenGamble, server grid shown, ring click -> gamble NpcBuy, result 12 -> TransactionResults10', () => {
|
||||
const r = replayD2CapFile(GAMBLE_CAPTURE, { strict: true })
|
||||
expect(r.unknownPackets).toBe(0)
|
||||
const world = new ClientWorld()
|
||||
const hud = new HudManager()
|
||||
const commands: ClientCommand[] = []
|
||||
const model = new HudModel({
|
||||
world,
|
||||
hudManager: hud,
|
||||
onCommand: (c) => {
|
||||
commands.push(c)
|
||||
world.noteOutgoingCommand(c)
|
||||
},
|
||||
vendorPrice: stubPrice,
|
||||
vendorRepairAll: () => {
|
||||
throw new Error('gamble stores have no Repair All')
|
||||
},
|
||||
playUiSound: () => {},
|
||||
now: () => 10_000,
|
||||
})
|
||||
|
||||
const firstShop = r.events.findIndex((e) => e.type === 'ItemEvent' && e.container === 'shop')
|
||||
expect(firstShop).toBeGreaterThan(0)
|
||||
for (const e of r.events.slice(0, firstShop)) world.apply(e)
|
||||
model.syncFromWorld()
|
||||
expect(world.getUnit('monster', GHEED_ID)?.classId).toBe(GHEED_CLASS)
|
||||
|
||||
// Click Gheed (C2S 0x13) -> NPC menu with "gamble".
|
||||
world.noteOutgoingCommand({ type: 'NpcInit', target: { unitType: MONSTER, id: GHEED_ID } })
|
||||
model.syncFromWorld()
|
||||
const menu = hud.worldPanels.npcMenu!
|
||||
expect(menu.npcId).toBe(GHEED_ID)
|
||||
const gambleIdx = menu.options!.findIndex((o) => o.id === 'gamble')
|
||||
expect(gambleIdx).toBeGreaterThanOrEqual(0)
|
||||
const layout = hud.worldPanels.getNpcMenuLayout(hud.font)!
|
||||
const down = (x: number, y: number, shiftKey = false) =>
|
||||
hud.handleHudMouseDown({
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
button: 0,
|
||||
shiftKey,
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {},
|
||||
} as unknown as MouseEvent)
|
||||
down(layout.left + Math.trunc(layout.width / 2), layout.optionYs[gambleIdx]! - 3)
|
||||
expect(commands).toEqual([{ type: 'NpcOpenGamble', npcId: GHEED_ID }])
|
||||
expect(hud.leftPanel).toBe('vendor')
|
||||
|
||||
// Server fills the gamble grid; hold back the 0x2A reply until the click.
|
||||
const rest = r.events.slice(firstShop)
|
||||
const reply = rest.filter((e): e is Extract<ServerEvent, { type: 'NpcTransaction' }> => e.type === 'NpcTransaction')
|
||||
expect(reply.map((e) => e.result)).toEqual([12])
|
||||
for (const e of rest) if (e.type !== 'NpcTransaction') world.apply(e)
|
||||
model.syncFromWorld()
|
||||
|
||||
const wp = hud.worldPanels
|
||||
expect(wp.vendorMode).toBe('gamble')
|
||||
expect(wp.gamblePlacements.length).toBe(world.containers.shop.size)
|
||||
expect(wp.vendorRepairAllCost).toBeNull()
|
||||
for (const pl of wp.gamblePlacements) {
|
||||
const raw = world.containers.shop.get(Number(pl.item.id))!
|
||||
expect(pl.item.buyCost).toBe(stubPrice(raw, 2, { world, npcClass: GHEED_CLASS }))
|
||||
}
|
||||
|
||||
const ring = wp.gamblePlacements.find((pl) => pl.item.code === 'rin')!
|
||||
expect(ring).toBeDefined()
|
||||
const ringId = Number(ring.item.id)
|
||||
const ringRaw = world.containers.shop.get(ringId)!
|
||||
// Shift is ignored for gamble buys (D2Client sends shiftFill 0 in gamble mode).
|
||||
down(
|
||||
VENDOR_GRID_ORIGIN.x + ring.col * VENDOR_GRID_ORIGIN.cellPx + 5,
|
||||
VENDOR_GRID_ORIGIN.y + ring.row * VENDOR_GRID_ORIGIN.cellPx + 5,
|
||||
true,
|
||||
)
|
||||
expect(commands.at(-1)).toEqual({
|
||||
type: 'NpcBuy',
|
||||
npcId: GHEED_ID,
|
||||
itemId: ringId,
|
||||
itemMode: 0,
|
||||
gamble: true,
|
||||
shiftFill: false,
|
||||
cost: stubPrice(ringRaw, 2, { world, npcClass: GHEED_CLASS }),
|
||||
})
|
||||
expect(world.self.gold).toBe(2)
|
||||
|
||||
for (const e of reply) world.apply(e)
|
||||
model.syncFromWorld()
|
||||
expect(model.lastNpcTransactionMessage).toEqual({ tblKey: 'TransactionResults10', text: requireTbl('TransactionResults10', 'CHI') })
|
||||
expect(world.self.gold).toBe(2)
|
||||
expect(world.containers.shop.has(ringId)).toBe(true)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue