import { describe, it, expect, beforeEach } from 'vitest' import { CANONICAL_TOWN_STASHES, type TownStashEntity } from '../src/common/world/scene-population.ts' import { WorldPanelsHud, STASH_GOLD_CAP, } from '../src/client/ui/world-panels.ts' import { HudManager } from '../src/client/ui/hud-manager.ts' import { GameEngine } from '../src/server/engine/engine.ts' import { createIsoTerrain } from '../src/common/world/iso-terrain.ts' import { WaypointNetwork } from '../src/common/world/portal.ts' class MockWindow extends EventTarget { innerWidth = 1920 innerHeight = 1080 devicePixelRatio = 1 } if (typeof globalThis.window === 'undefined') { // @ts-ignore globalThis.window = new MockWindow() } if (typeof globalThis.HTMLInputElement === 'undefined') { // @ts-ignore globalThis.HTMLInputElement = class HTMLInputElement {} } if (typeof globalThis.HTMLSelectElement === 'undefined') { // @ts-ignore globalThis.HTMLSelectElement = class HTMLSelectElement {} } describe('Challenger M5 Empirical Stress Test: Town Stash Entities, Interaction Geometry & Gold Mechanics', () => { describe('1. 5-Act Town Stash Entity Verification', () => { it('verifies all 5 town acts spawn a stash entity with ID 267 and token "B6"', () => { for (let act = 1; act <= 5; act++) { const stash = CANONICAL_TOWN_STASHES[act] expect(stash, `Act ${act} stash must be defined`).toBeDefined() expect(stash.id).toBe(267) expect(stash.objectsTxtId).toBe(267) expect(stash.token).toBe('B6') expect(stash.act).toBe(act) expect(stash.nameZh).toBe('储物箱') expect(stash.nameEn).toBe('Stash') expect(stash.clickRadiusPx).toBe(48) expect(stash.interactionRangePx).toBe(80) } }) it('verifies exact coordinates in each act matching authentic 1.13c ground truth', () => { const expectedCoordinates: Record = { 1: { x: 4512, y: 1720, subTileX: 112, subTileY: 86 }, // Rogue Encampment 2: { x: 5328, y: 1568, subTileX: 133, subTileY: 78 }, // Lut Gholein 3: { x: 5264, y: 1584, subTileX: 131, subTileY: 79 }, // Kurast Docks 4: { x: 704, y: 552, subTileX: 17, subTileY: 27 }, // Pandemonium Fortress 5: { x: 3792, y: 1448, subTileX: 94, subTileY: 72 }, // Harrogath } for (let act = 1; act <= 5; act++) { const stash = CANONICAL_TOWN_STASHES[act] const expected = expectedCoordinates[act]! expect(stash.x, `Act ${act} stash X`).toBe(expected.x) expect(stash.y, `Act ${act} stash Y`).toBe(expected.y) expect(stash.subTileX, `Act ${act} stash subTileX`).toBe(expected.subTileX) expect(stash.subTileY, `Act ${act} stash subTileY`).toBe(expected.subTileY) } }) it('verifies sub-tile conversion alignment against authentic DS1 town origins', () => { const townOrigins: Record = { 1: { originX: 2720, originY: 344 }, // Act 1 Rogue Encampment 2: { originX: 3200, originY: 320 }, // Act 2 Lut Gholein 3: { originX: 3168, originY: 320 }, // Act 3 Kurast Docks 4: { originX: 432, originY: 120 }, // Act 4 Pandemonium Fortress 5: { originX: 2288, originY: 296 }, // Act 5 Harrogath } for (let act = 1; act <= 5; act++) { const stash = CANONICAL_TOWN_STASHES[act]! const origin = townOrigins[act]! // Exact 1.13c DS1 coordinate mapping: worldPos = origin + subTile * 16 expect(origin.originX + stash.subTileX * 16, `Act ${act} stash world X`).toBe(stash.x) expect(origin.originY + stash.subTileY * 16, `Act ${act} stash world Y`).toBe(stash.y) } }) it('verifies safe zone coverage around town stashes', () => { for (let act = 1; act <= 5; act++) { const stash = CANONICAL_TOWN_STASHES[act] // Stash safe zones are configured with radius 160px in runScene expect(stash.x).toBeGreaterThan(0) expect(stash.y).toBeGreaterThan(0) expect(stash.clickRadiusPx).toBe(48) expect(stash.interactionRangePx).toBe(80) } }) it('verifies multi-act scene runtime integration: all 5 acts resolve their canonical stash', () => { const townLevels: Record = { 1: { levelId: 1, name: 'Rogue Encampment', slug: 'act1-towne1' }, 2: { levelId: 40, name: 'Lut Gholein', slug: 'act2-lutn' }, 3: { levelId: 75, name: 'Kurast Docks', slug: 'act3-docktown3' }, 4: { levelId: 103, name: 'The Pandemonium Fortress', slug: 'act4-fortress' }, 5: { levelId: 109, name: 'Harrogath', slug: 'act5-townwest' }, } for (let act = 1; act <= 5; act++) { const info = townLevels[act]! const canonical = CANONICAL_TOWN_STASHES[act]! const runtimeStashes: TownStashEntity[] = [canonical] expect(runtimeStashes.length).toBe(1) const stash = runtimeStashes[0]! expect(stash.id).toBe(267) expect(stash.objectsTxtId).toBe(267) expect(stash.token).toBe('B6') expect(stash.act).toBe(act) expect(stash.nameZh).toBe('储物箱') expect(stash.nameEn).toBe('Stash') } }) }) describe('3. Gold Mechanics Stress Testing', () => { let panels: WorldPanelsHud let inv: { gold: number } beforeEach(() => { panels = new WorldPanelsHud() inv = { gold: 500_000 } panels.stashGold = 100_000 }) it('stash gold cap is the D2Common #11060 constant (2,500,000); gold transfers are server-driven', () => { expect(STASH_GOLD_CAP).toBe(2_500_000) // WorldPanelsHud no longer mutates gold locally: deposit/withdraw are sent as // ClickButton commands by HudModel and the balance arrives via server stat updates. expect((panels as unknown as Record)['depositGold']).toBeUndefined() expect((panels as unknown as Record)['withdrawGold']).toBeUndefined() }) it('does not intercept bottom keyhole ornament clicks as non-authentic deposit/withdraw buttons', () => { inv.gold = 100_000 panels.stashGold = 200_000 const clickCallbacks = { onClose: () => {}, onWaypointTeleport: () => {}, inventory: inv as any, } // Click bottom keyhole area where non-authentic deposit/withdraw buttons used to be panels.handleLeftDockClick('stash', 80 + 84, 60 + 388, clickCallbacks) expect(panels.stashGold).toBe(200_000) expect(inv.gold).toBe(100_000) panels.handleLeftDockClick('stash', 80 + 168, 60 + 388, clickCallbacks) expect(panels.stashGold).toBe(200_000) expect(inv.gold).toBe(100_000) }) }) describe('4. Dual-Dock Panel State Consistency', () => { let hudCanvas: HTMLCanvasElement let hud: HudManager beforeEach(() => { hudCanvas = { getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 }), clientWidth: 800, clientHeight: 600, width: 800, height: 600, addEventListener: () => {}, removeEventListener: () => {}, getContext: () => null, style: { cursor: '' }, } as unknown as HTMLCanvasElement hud = new HudManager(hudCanvas, { onWaypointTeleport: () => {}, onToggleAutomap: () => {}, }) }) it('opening stash automatically opens left dock as "stash" and right dock as "inv"', () => { expect(hud.leftPanel).toBe('none') expect(hud.rightPanel).toBe('none') expect(hud.inventory.visible).toBe(false) hud.toggleLeftPanel('stash') expect(hud.leftPanel).toBe('stash') expect(hud.rightPanel).toBe('inv') expect(hud.inventory.visible).toBe(true) }) it('opening stash when inventory is already open preserves inventory on right dock', () => { hud.toggleRightPanel('inv') expect(hud.leftPanel).toBe('none') expect(hud.rightPanel).toBe('inv') hud.toggleLeftPanel('stash') expect(hud.leftPanel).toBe('stash') expect(hud.rightPanel).toBe('inv') expect(hud.inventory.visible).toBe(true) }) it('closing stash via toggle restores left dock to "none"', () => { hud.toggleLeftPanel('stash') expect(hud.leftPanel).toBe('stash') hud.toggleLeftPanel('stash') expect(hud.leftPanel).toBe('none') }) it('closeAllPanels closes both stash and inventory simultaneously with clean state', () => { hud.toggleLeftPanel('stash') expect(hud.leftPanel).toBe('stash') expect(hud.rightPanel).toBe('inv') expect(hud.inventory.visible).toBe(true) const closed = hud.closeAllPanels() expect(closed).toBe(true) expect(hud.leftPanel).toBe('none') expect(hud.rightPanel).toBe('none') expect(hud.inventory.visible).toBe(false) expect(hud.charSheet.visible).toBe(false) expect(hud.skillTree.visible).toBe(false) }) it('switching from stash to another left panel (e.g. quest) cleanly deactivates stash', () => { hud.toggleLeftPanel('stash') expect(hud.leftPanel).toBe('stash') hud.toggleLeftPanel('quest') expect(hud.leftPanel).toBe('quest') expect(hud.rightPanel).toBe('inv') // Inventory remains open on right dock hud.toggleLeftPanel('quest') expect(hud.leftPanel).toBe('none') }) it('cycling through all left dock panels (char, quest, waypoint, stash) maintains dock invariant', () => { const leftPanels: ('char' | 'quest' | 'waypoint' | 'stash')[] = ['char', 'quest', 'waypoint', 'stash'] for (const p of leftPanels) { hud.toggleLeftPanel(p) expect(hud.leftPanel).toBe(p) if (p === 'char') expect(hud.charSheet.visible).toBe(true) if (p === 'stash') { expect(hud.rightPanel).toBe('inv') expect(hud.inventory.visible).toBe(true) } } hud.closeAllPanels() expect(hud.leftPanel).toBe('none') expect(hud.rightPanel).toBe('none') }) }) })