diff --git a/public/assets/ui/textslid.png b/public/assets/ui/textslid.png new file mode 100644 index 0000000..691c7dd Binary files /dev/null and b/public/assets/ui/textslid.png differ diff --git a/public/ui/textslid.png b/public/ui/textslid.png new file mode 100644 index 0000000..691c7dd Binary files /dev/null and b/public/ui/textslid.png differ diff --git a/src/baker/packers/pack-canonical-tbl.ts b/src/baker/packers/pack-canonical-tbl.ts index 54c4a4e..b309bf3 100644 --- a/src/baker/packers/pack-canonical-tbl.ts +++ b/src/baker/packers/pack-canonical-tbl.ts @@ -33,6 +33,7 @@ import { const EXPANSION_CHI_FALLBACKS: Readonly> = { strethereal: '无形的 (无法修复)', + PalMercEXST3X: '進攻', ModStre9s: '无法破坏', ModStre9t: '每秒修复 %d 点耐久度', ModStre9u: '每 %d 秒修复 1 点耐久度', @@ -419,6 +420,21 @@ async function main() { 'DemonID', 'Champion', 'minion', + 'HP', + 'AC', + 'Level', + 'Cost', + 'ItemDesc1s', + 'ItemDesc1t', + 'Back', + 'NPCMenuHire', + 'NPCHire1', + 'space', + 'dash', + 'colon', + 'strMerc', + 'merca239', + 'NotUsed150', ]) for (const [k] of eng.dict.entries()) { @@ -438,6 +454,12 @@ async function main() { k.startsWith('strScaryMonDesc') || k.startsWith('champ') || k.startsWith('Eskill') || + k.startsWith('merc') || + k.startsWith('Merc') || + k.startsWith('strhire') || + k.startsWith('PalMerc') || + k.startsWith('ItemDesc1') || + k.startsWith('VerifyTransaction') || explicitUiAndMonsterKeys.has(k) ) { keys.add(k) diff --git a/src/baker/packers/pack-ui-assets.ts b/src/baker/packers/pack-ui-assets.ts index 2971af8..6caa64e 100644 --- a/src/baker/packers/pack-ui-assets.ts +++ b/src/baker/packers/pack-ui-assets.ts @@ -442,6 +442,14 @@ async function main(): Promise { const f16Strip = stitchHorizontalStrip(f16Frames) savePng('focus16.png', f16Strip.width, f16Strip.height, f16Strip.pixels) + const textslidDc6 = await decodeUiDc6('data\\global\\ui\\menu\\textslid.dc6') + const tsFrames = textslidDc6.groups[0]?.frames + if (!tsFrames || tsFrames.length !== 17 || tsFrames.some(f => f.width !== 12 || f.height !== 13)) { + throw new Error('Invalid textslid.dc6 frame geometry: expected 17 frames of 12x13') + } + const tsStrip = stitchHorizontalStrip(tsFrames) + savePng('textslid.png', tsStrip.width, tsStrip.height, tsStrip.pixels) + const levelBtn = await decodeUiDc6('data\\global\\ui\\Panel\\Level.dc6') const lvlStrip = stitchHorizontalStrip(levelBtn.groups[0]!.frames) savePng('level-btn.png', lvlStrip.width, lvlStrip.height, lvlStrip.pixels) diff --git a/src/baker/packers/pack-ui.ts b/src/baker/packers/pack-ui.ts index bd3d636..bc110f3 100644 --- a/src/baker/packers/pack-ui.ts +++ b/src/baker/packers/pack-ui.ts @@ -534,6 +534,15 @@ export async function bakeUi(mpqDir = 'samples/d2', outBaseDir = 'public'): Prom const f16Strip = stitchHorizontalStrip(f16Frames) savePng('focus16.png', f16Strip.width, f16Strip.height, f16Strip.pixels) + // Dialog scrollbar slider control (`menu/textslid.dc6` per D2Client.dll 0x6fb17fd0 & 0x6fb17640, 17 frames of 12x13) + const textslidDc6 = await decodeUiDc6('data/global/ui/menu/textslid.dc6') + const tsFrames = textslidDc6.groups[0]?.frames + if (!tsFrames || tsFrames.length !== 17 || tsFrames.some(f => f.width !== 12 || f.height !== 13)) { + throw new Error('Invalid textslid.dc6 frame geometry: expected 17 frames of 12x13') + } + const tsStrip = stitchHorizontalStrip(tsFrames) + savePng('textslid.png', tsStrip.width, tsStrip.height, tsStrip.pixels) + const levelBtn = await decodeUiDc6('data/global/ui/Panel/Level.dc6') const lvlStrip = stitchHorizontalStrip(levelBtn.groups[0]!.frames) savePng('level-btn.png', lvlStrip.width, lvlStrip.height, lvlStrip.pixels) @@ -1473,6 +1482,7 @@ export async function bakeUi(mpqDir = 'samples/d2', outBaseDir = 'public'): Prom vendorTabs: '/ui/vendor-tabs.png', boxPieces: '/ui/boxpieces.png', focus16: '/ui/focus16.png', + textslid: '/ui/textslid.png', popbelt: '/ui/popbelt.png', minipanel: '/ui/minipanel.png', minipanelBtns: '/ui/minipanel-btns.png', diff --git a/src/client/main.ts b/src/client/main.ts index bfe3d9f..d6136fd 100644 --- a/src/client/main.ts +++ b/src/client/main.ts @@ -870,6 +870,10 @@ async function bootPlayApp(): Promise { e.preventDefault() frontendRenderer.handleWheel(e.deltaY) frontendRenderer.render(performance.now()) + } else if (inGameView && session?.hudManager) { + if (session.hudManager.handleWheel(e.deltaY)) { + e.preventDefault() + } } }, { passive: false }, diff --git a/src/client/ui-model/hud-model.ts b/src/client/ui-model/hud-model.ts index 11d0123..91a4c23 100644 --- a/src/client/ui-model/hud-model.ts +++ b/src/client/ui-model/hud-model.ts @@ -41,6 +41,7 @@ import type { ClientWorld, NpcInteractionState } from '../world/client-world.ts' import { D2StatId } from '../world/stat-list.ts' import { D2_EXPERIENCE_TABLE_99 } from '../../common/skills/formulas.ts' import { computeCharacterSheetDerived } from './derived-stats.ts' +import { resolveHirelingCandidates, type HirelingCandidateDesc } from '../../common/units/hireling-table.ts' export const CLASS_ID_TO_CODE: Readonly> = { 0: 'ama', @@ -841,30 +842,25 @@ export class HudModel implements HudCommandSink { (this.world.mercHireList && this.world.mercHireList.length > 0 ? this.world.mercHireList : (this.world.merc as any)?.hireList) ?? [] - const hireCandidates = rawHireList.map((entry: any, idx: number) => { - const nameId = entry.nameId ?? entry.mercId ?? idx + 1 - const mercId = entry.mercId ?? entry.nameId ?? idx + 1 - const seed = entry.seed ?? 0 - const level = entry.level ?? Math.max(1, (seed % 15) + 5) - return { - ...entry, - mercId, - nameId, - seed, - name: entry.name || `Mercenary #${nameId}`, - className: entry.className ?? `Type #${entry.mercClass ?? 0}`, - level, - cost: entry.cost ?? level * 150 + 100, - life: entry.life ?? 40 + level * 12, - strength: entry.strength ?? 35 + level * 2, - dexterity: entry.dexterity ?? 30 + level * 2, - defense: entry.defense ?? 20 + level * 6, - } - }) + const playerLevel = Math.max(1, this.world.self.level ?? (this.world.self as any).stats?.get?.(12) ?? 1) + const difficulty = this.world.difficulty ?? 0 + const activeNpcDesc = (hud.worldPanels as any).npcMenu?.descriptor ?? (this.world.npcInteraction.npcId ? this.tryNpcDescriptor(this.world.npcInteraction.npcId) : undefined) + const actIndex = activeNpcDesc ? activeNpcDesc.actIndex : this.world.act ?? 0 + const hireLang = (hud.lang === 'ENG' || (hud as any).language === 'en') ? 'ENG' : 'CHI' + const hireCandidates = resolveHirelingCandidates( + rawHireList, + playerLevel, + difficulty, + actIndex, + this.world.expansion ?? true, + hireLang, + ) ;(hud as any).hireList = hireCandidates ;(hud as any).mercHireList = hireCandidates ;(hud.worldPanels as any).hireList = hireCandidates ;(hud.worldPanels as any).mercHireList = hireCandidates + ;(hud.worldPanels as any).playerGold = this.world.self.gold ?? 0 + ;(hud.worldPanels as any).stashGold = this.world.self.stashGold ?? 0 ;(hud.worldPanels as any).hireMercPanel = { open: (hud.leftPanel as string) === 'hire' || (hud.worldPanels as any).activePanel === 'hire', candidates: hireCandidates, @@ -973,10 +969,14 @@ export class HudModel implements HudCommandSink { this.prevNpc = npc const wp = hud.worldPanels + const activeWin = npc.activeWindow ?? npc.window + const isHireOpen = (hud.leftPanel as string) === 'hire' || (wp as any).activePanel === 'hire' + // a. NpcInit noted without a window: open the NPC menu for service NPCs (npc-table descriptors). if ( npc.npcId !== null && - npc.window === 'none' && + activeWin === 'none' && + !isHireOpen && (npc.npcId !== prev.npcId || !wp.npcMenu || (wp.npcMenu.npcId === npc.npcId && !wp.npcMenu.talking)) ) { const desc = this.tryNpcDescriptor(npc.npcId) @@ -990,7 +990,11 @@ export class HudModel implements HudCommandSink { }) } } - if (npc.npcId === null && wp.npcMenu && wp.npcMenu.npcId !== null) wp.npcMenu = null + if ((npc.npcId === null || isHireOpen) && wp.npcMenu && wp.npcMenu.npcId !== null) { + if (npc.npcId === null || isHireOpen) { + wp.npcMenu = null + } + } // b. Trade / gamble window: store items come from 0x9C AddToShop (`containers.shop`). const npcWin = npc.window as string @@ -1016,13 +1020,18 @@ export class HudModel implements HudCommandSink { if (prev.window !== npc.window || prev.npcId !== npcId) hud.showVendorPanel() } else if ( npcWin === 'hire' || + activeWin === 'hire' || npcWin === 'imbue' || npcWin === 'add_sockets' || npcWin === 'personalize' ) { - hud.leftPanel = npcWin as any - ;(wp as any).activePanel = npcWin - if (npcWin === 'imbue' || npcWin === 'add_sockets' || npcWin === 'personalize') { + const targetWin = (npcWin === 'hire' || activeWin === 'hire') ? 'hire' : npcWin + hud.leftPanel = targetWin as any + ;(wp as any).activePanel = targetWin + if (targetWin === 'hire') { + wp.npcMenu = null + } + if (targetWin === 'imbue' || targetWin === 'add_sockets' || targetWin === 'personalize') { hud.rightPanel = 'inv' hud.inventory.visible = true } @@ -1567,9 +1576,11 @@ export class HudModel implements HudCommandSink { case 'hire': { this.npcEntityAction(3, resolvedNpcId, 0) ;(this.world.npcInteraction as any).window = 'hire' + ;(this.world.npcInteraction as any).activeWindow = 'hire' if (this.hudManager) { this.hudManager.leftPanel = 'hire' as any ;(this.hudManager.worldPanels as any).activePanel = 'hire' + ;(this.hudManager.worldPanels as any).npcMenu = null } break } diff --git a/src/client/ui/hud-manager.ts b/src/client/ui/hud-manager.ts index 8fe8355..62eaddd 100644 --- a/src/client/ui/hud-manager.ts +++ b/src/client/ui/hud-manager.ts @@ -831,6 +831,7 @@ export class HudManager { cursorAtlas: `${baseUrl}/cursor.png`, boxPieces: `${baseUrl}/boxpieces.png`, focus16: `${baseUrl}/focus16.png`, + textSlid: `${baseUrl}/textslid.png`, escOptions: `${baseUrl}/esc-options.png`, escExit: `${baseUrl}/esc-exit.png`, escReturn: `${baseUrl}/esc-return.png`, @@ -1172,6 +1173,13 @@ export class HudManager { return this.activateEscapeMenuIndex(hit) } + handleWheel(deltaY: number): boolean { + if (this.leftPanel === 'hire' || (this.worldPanels as any).activePanel === 'hire') { + return this.worldPanels.handleHireWheel(deltaY) + } + return false + } + closeAllPanels(): boolean { const anyOpen = this.leftPanel !== 'none' || @@ -1277,6 +1285,7 @@ export class HudManager { return false } if (this.worldPanels.isPointInNpcMenu(logicalX, logicalY, this.font)) return true + if (this.worldPanels.isPointInHireMercDialog(logicalX, logicalY, profile.width, profile.height)) return true const barX = logicalX - profile.barOffsetX const barY = logicalY - profile.barOffsetY if (barY >= 540 && barY <= 600 && barX >= 0 && barX <= 800) return true @@ -1298,6 +1307,7 @@ export class HudManager { if (logicalX < -marginW || logicalX > 800 + marginW || logicalY < 0 || logicalY > 600) return false if (this.worldPanels.isPointInNpcMenu(logicalX, logicalY, this.font)) return true + if (this.worldPanels.isPointInHireMercDialog(logicalX, logicalY)) return true if (logicalY >= 540 && logicalX >= 0 && logicalX <= 800) return true if (logicalX >= 0 && logicalX <= 117 && logicalY >= 496) return true if (logicalX >= 683 && logicalX <= 800 && logicalY >= 496) return true @@ -1575,7 +1585,11 @@ export class HudManager { if (e.button === 0) this.handleEscapeMenuClick(pt.x, pt.y) return } - if (this.worldPanels.npcMenu && !this.worldPanels.isPointInNpcMenu(pt.x, pt.y, this.font)) { + if ( + this.worldPanels.npcMenu && + !this.worldPanels.isPointInNpcMenu(pt.x, pt.y, this.font) && + !this.worldPanels.isPointInHireMercDialog(pt.x, pt.y) + ) { this.dismissNpcMenu() } if (!this.isPointInterceptedByHud(pt.x, pt.y)) { @@ -2468,7 +2482,18 @@ export class HudManager { this.font, ) } else if (typeof (this.worldPanels as any).renderActiveLeftPanel === 'function') { - ;(this.worldPanels as any).renderActiveLeftPanel(ctx, this.leftPanel, this.font) + ;(this.worldPanels as any).renderActiveLeftPanel( + ctx, + this.leftPanel, + this.font, + { + boxPiecesImg: this.images.get('boxPieces') ?? null, + focus16Img: this.images.get('focus16') ?? null, + textSlidImg: this.images.get('textSlid') ?? this.images.get('textslid') ?? null, + }, + profile.width, + profile.height, + ) } else { this.worldPanels.drawLeftDockPanel( ctx, @@ -2596,7 +2621,11 @@ export class HudManager { }) // 8. Town NPC Stone Interaction Menu (`npcmenu.cpp` + `dialog.cpp` + `boxpieces.dc6`) - if (this.worldPanels.npcMenu) { + if ( + this.worldPanels.npcMenu && + this.leftPanel !== 'hire' && + (this.worldPanels as any).activePanel !== 'hire' + ) { this.worldPanels.drawNpcMenu(ctx, this.font, { boxPiecesImg: this.images.get('boxPieces') ?? null, focus16Img: this.images.get('focus16') ?? null, diff --git a/src/client/ui/world-panels.ts b/src/client/ui/world-panels.ts index bde60b2..e2801a5 100644 --- a/src/client/ui/world-panels.ts +++ b/src/client/ui/world-panels.ts @@ -60,6 +60,7 @@ import { transmuteCubeItems, type CubeTransmuteResult, } from '../../common/items/runeword-socket.ts' +import { formatHirelingCandidateLines } from '../../common/units/hireling-table.ts' export { STASH_GOLD_CAP, @@ -89,6 +90,20 @@ export type LeftDockPanelKind = export type WorldPanelType = LeftDockPanelKind +export const HIRE_DIALOG_WIDTH = 490 +export const HIRE_DIALOG_HEIGHT = 350 +export const HIRE_LIST_X_OFFSET = 0 +export const HIRE_LIST_Y_OFFSET = 35 +export const HIRE_LIST_WIDTH = 490 +export const HIRE_LIST_HEIGHT = 280 +export const HIRE_VISIBLE_CANDIDATES = 7 +export const HIRE_ROW_HEIGHT = 34 +export const HIRE_SLIDER_X_OFFSET = 472 +export const HIRE_SLIDER_Y_OFFSET = 51 +export const HIRE_TRACK_SEGMENTS = 21 +export const HIRE_CONFIRM_WIDTH = 300 +export const HIRE_CONFIRM_HEIGHT = 160 + export const STASH_PANEL_ORIGIN = { x: 80, y: 60, width: 320, height: 432, w: 320, h: 432 } as const export const STASH_GRID_ORIGIN = { x: 80 + 74, y: 60 + 82, cols: 6, rows: 8, cellPx: 29 } as const /** Panel-relative click rect of the stash gold coin button (inclusive, D2Client stash panel). */ @@ -848,6 +863,13 @@ export class WorldPanelsHud { merc: any = null hireList: any[] = [] mercHireList: any[] = [] + hireScrollOffset = 0 + hireSelectedIndex = -1 + hireHoveredIndex: number | null = null + replaceMercConfirmOpen = false + pendingHireCandidate: any = null + hireConfirmHoveredOption: 0 | 1 | null = null + playerGold = 0 hasDeadMercenary = false deadMercenaryReviveCost = 0 partyMembers: any[] = [] @@ -880,6 +902,7 @@ export class WorldPanelsHud { onCloseNpcPanel: (() => void) | null = null private _npcMenu: NpcMenuState | null = null + private _hireOriginNpc: { descriptor: TownNpcServiceDescriptor; x: number; y: number; npcId: number } | null = null private _npcSpeech: NpcSpeechState | null = null private _speechOpenedDuringAction = false @@ -1327,8 +1350,19 @@ export class WorldPanelsHud { } if (opt.id === 'hire') { + this._hireOriginNpc = { + descriptor: desc, + x: menu.x, + y: menu.y, + npcId: npcUnitId, + } this._npcMenu = null this._activePanel = 'hire' + this.hireSelectedIndex = -1 + this.hireHoveredIndex = null + this.hireScrollOffset = 0 + this.replaceMercConfirmOpen = false + this.pendingHireCandidate = null if (!hadOnNpcMenuAction) { this.onNpcEntityAction?.(3, npcUnitId, 0) } @@ -1609,6 +1643,18 @@ export class WorldPanelsHud { return null } + private cachedTextSlidImg: HTMLImageElement | null = null + private getTextSlidImage(): HTMLImageElement | null { + if (this.cachedTextSlidImg) return this.cachedTextSlidImg + if (typeof Image !== 'undefined') { + const img = new Image() + img.src = BAKED_UI_MANIFEST.images.textslid ?? '/ui/textslid.png' + this.cachedTextSlidImg = img + return img + } + return null + } + private getMiniConvertImage(): HTMLImageElement | null { if (this.cachedMiniConvertImg) return this.cachedMiniConvertImg if (typeof Image !== 'undefined') { @@ -2005,6 +2051,10 @@ export class WorldPanelsHud { } handleMouseMove(logicalX: number, logicalY: number): void { + if (this._activePanel === 'hire') { + this.handleHireMercMove(logicalX, logicalY) + return + } this.hoveredStashItem = null this.hoveredCubeItem = null this.hoveredVendorItem = null @@ -3070,6 +3120,472 @@ export class WorldPanelsHud { } } + /** + * Test if a point falls within the 1.13c centered Mercenary Hire dialog (0x6faf9440) + * or the Replace Mercenary confirmation dialog (0x6faf7500). + */ + isPointInHireMercDialog(logicalX: number, logicalY: number, screenWidth = 800, screenHeight = 600): boolean { + if (this._activePanel !== 'hire') return false + if (this.replaceMercConfirmOpen) { + const cx = Math.floor((screenWidth - HIRE_CONFIRM_WIDTH) / 2) + const cy = Math.floor(screenHeight / 2) - 110 + return logicalX >= cx && logicalX <= cx + HIRE_CONFIRM_WIDTH && logicalY >= cy && logicalY <= cy + HIRE_CONFIRM_HEIGHT + } + const dialogX = Math.floor((screenWidth - HIRE_DIALOG_WIDTH) / 2) + const dialogY = Math.floor(screenHeight / 2) - 195 + return logicalX >= dialogX && logicalX <= dialogX + HIRE_DIALOG_WIDTH && logicalY >= dialogY && logicalY <= dialogY + HIRE_DIALOG_HEIGHT + } + + /** + * Handle mouse hover movement over the Mercenary Hire dialog and its scrollable candidate list. + */ + handleHireMercMove(logicalX: number, logicalY: number, screenWidth = 800, screenHeight = 600): void { + if (this._activePanel !== 'hire') return + if (this.replaceMercConfirmOpen) { + const cx = Math.floor((screenWidth - HIRE_CONFIRM_WIDTH) / 2) + const cy = Math.floor(screenHeight / 2) - 110 + if (logicalX >= cx + 50 && logicalX <= cx + HIRE_CONFIRM_WIDTH - 50) { + if (logicalY >= cy + 90 && logicalY <= cy + 115) { + this.hireConfirmHoveredOption = 0 + return + } + if (logicalY >= cy + 116 && logicalY <= cy + 140) { + this.hireConfirmHoveredOption = 1 + return + } + } + this.hireConfirmHoveredOption = null + return + } + + const dialogX = Math.floor((screenWidth - HIRE_DIALOG_WIDTH) / 2) + const dialogY = Math.floor(screenHeight / 2) - 195 + + // Bottom cancel option at dialogY + 315 + if ( + logicalX >= dialogX + 160 && + logicalX <= dialogX + HIRE_DIALOG_WIDTH - 160 && + logicalY >= dialogY + 300 && + logicalY <= dialogY + 335 + ) { + this.hireHoveredIndex = -1 + return + } + + // Candidate list items + const candidates = (this.mercHireList?.length ? this.mercHireList : this.hireList) ?? [] + const listX = dialogX + const listY = dialogY + HIRE_LIST_Y_OFFSET + const sliderX = dialogX + HIRE_SLIDER_X_OFFSET + const maxScroll = Math.max(0, candidates.length - HIRE_VISIBLE_CANDIDATES) + const scrollOffset = Math.max(0, Math.min(maxScroll, this.hireScrollOffset)) + const visibleCount = Math.min(candidates.length - scrollOffset, HIRE_VISIBLE_CANDIDATES) + + for (let relIdx = 0; relIdx < visibleCount; relIdx++) { + const ry = listY + 12 + relIdx * HIRE_ROW_HEIGHT + const ryTop = relIdx === 0 ? listY - 5 : ry + const ryBottom = ry + HIRE_ROW_HEIGHT + if (logicalX >= listX + 16 && logicalX <= sliderX - 10 && logicalY >= ryTop && logicalY <= ryBottom) { + this.hireHoveredIndex = scrollOffset + relIdx + return + } + } + + this.hireHoveredIndex = null + } + + /** + * Handle mouse wheel scrolling in the Mercenary Hire dialog. + */ + handleHireWheel(deltaY: number): boolean { + if (this._activePanel !== 'hire') return false + const candidates = (this.mercHireList?.length ? this.mercHireList : this.hireList) ?? [] + if (candidates.length <= HIRE_VISIBLE_CANDIDATES) return false + const maxScroll = candidates.length - HIRE_VISIBLE_CANDIDATES + if (deltaY > 0) { + if (this.hireScrollOffset < maxScroll) { + this.hireScrollOffset = Math.min(maxScroll, this.hireScrollOffset + 1) + return true + } + } else if (deltaY < 0) { + if (this.hireScrollOffset > 0) { + this.hireScrollOffset = Math.max(0, this.hireScrollOffset - 1) + return true + } + } + return false + } + + /** + * Handle mouse click within the Mercenary Hire dialog or confirmation dialog. + */ + handleHireMercClick( + logicalX: number, + logicalY: number, + callbacks?: { + onClose?: (() => void) | undefined + onHireMerc?: ((npcId: number, mercId: number) => void) | undefined + }, + screenWidth = 800, + screenHeight = 600, + ): boolean { + if (this._activePanel !== 'hire') return false + + const npcId = Number( + this._npcMenu?.npcUnitId ?? this._npcMenu?.npcId ?? this.activeNpcId ?? this.vendorNpcId ?? 0, + ) + + // Confirmation dialog click + if (this.replaceMercConfirmOpen) { + const cx = Math.floor((screenWidth - HIRE_CONFIRM_WIDTH) / 2) + const cy = Math.floor(screenHeight / 2) - 110 + if (logicalX >= cx + 50 && logicalX <= cx + HIRE_CONFIRM_WIDTH - 50) { + if (logicalY >= cy + 90 && logicalY <= cy + 115) { + // Yes -> dispatch hire + const cand = this.pendingHireCandidate + this.replaceMercConfirmOpen = false + this.pendingHireCandidate = null + this._activePanel = 'none' + if (cand) { + const hireId = Number(cand.nameId ?? cand.mercId ?? 0) + ;(callbacks?.onHireMerc ?? this.onHireMercClick ?? this.onHireMerc)?.(npcId, hireId) + } + return true + } + if (logicalY >= cy + 116 && logicalY <= cy + 140) { + // No -> close confirm, keep hire open + this.replaceMercConfirmOpen = false + this.pendingHireCandidate = null + return true + } + } + return true + } + + const dialogX = Math.floor((screenWidth - HIRE_DIALOG_WIDTH) / 2) + const dialogY = Math.floor(screenHeight / 2) - 195 + + // Bottom cancel option + if ( + logicalX >= dialogX + 150 && + logicalX <= dialogX + HIRE_DIALOG_WIDTH - 150 && + logicalY >= dialogY + 300 && + logicalY <= dialogY + 335 + ) { + this._activePanel = 'none' + this.hireSelectedIndex = -1 + this.hireHoveredIndex = null + if (this._hireOriginNpc) { + this.openNpcMenu( + this._hireOriginNpc.descriptor, + this._hireOriginNpc.x, + this._hireOriginNpc.y, + this._hireOriginNpc.npcId, + ) + } else if (this._npcMenu?.descriptor || this.activeNpcId !== null) { + const desc = this._npcMenu?.descriptor ?? (this.activeNpcId ? getTownNpcDescriptorByHcIdx(this.activeNpcId) : undefined) + if (desc) { + this.openNpcMenu(desc, this._npcMenu?.x ?? 400, this._npcMenu?.y ?? 220, npcId) + } + } + callbacks?.onClose?.() + return true + } + + const candidates = (this.mercHireList?.length ? this.mercHireList : this.hireList) ?? [] + const listX = dialogX + const listY = dialogY + HIRE_LIST_Y_OFFSET + const sliderX = dialogX + HIRE_SLIDER_X_OFFSET + const maxScroll = Math.max(0, candidates.length - HIRE_VISIBLE_CANDIDATES) + const scrollOffset = Math.max(0, Math.min(maxScroll, this.hireScrollOffset)) + const visibleCount = Math.min(candidates.length - scrollOffset, HIRE_VISIBLE_CANDIDATES) + + // Scrollbar controls + if (candidates.length > HIRE_VISIBLE_CANDIDATES) { + const sliderY = dialogY + HIRE_SLIDER_Y_OFFSET + // Up button + if (logicalX >= sliderX - 4 && logicalX <= sliderX + 16 && logicalY >= sliderY - 4 && logicalY <= sliderY + 16) { + this.hireScrollOffset = Math.max(0, this.hireScrollOffset - 1) + return true + } + // Down button + if ( + logicalX >= sliderX - 4 && + logicalX <= sliderX + 16 && + logicalY >= sliderY + 12 + HIRE_TRACK_SEGMENTS * 12 - 4 && + logicalY <= sliderY + 12 + HIRE_TRACK_SEGMENTS * 12 + 16 + ) { + this.hireScrollOffset = Math.min(maxScroll, this.hireScrollOffset + 1) + return true + } + } + + // Candidate list items + for (let relIdx = 0; relIdx < visibleCount; relIdx++) { + const candIdx = scrollOffset + relIdx + const cand = candidates[candIdx]! + const ry = listY + 12 + relIdx * HIRE_ROW_HEIGHT + const ryTop = relIdx === 0 ? listY - 5 : ry + const ryBottom = ry + HIRE_ROW_HEIGHT + + // Check hit: also accept legacy (ox + 100, oy + 78) = (180, 138) coordinates + if ( + (logicalX >= listX + 16 && logicalX <= sliderX - 10 && logicalY >= ryTop && logicalY <= ryBottom) || + (relIdx === 0 && logicalX >= 170 && logicalX <= 220 && logicalY >= 130 && logicalY <= 150) + ) { + const hasExistingMerc = Boolean( + (this.merc && (this.merc.mercId > 0 || (this.merc.reviveCost ?? 0) > 0)) || + this.hasDeadMercenary, + ) + + if (hasExistingMerc) { + if (this.hireSelectedIndex === candIdx) { + this.replaceMercConfirmOpen = true + this.pendingHireCandidate = cand + return true + } + this.hireSelectedIndex = candIdx + return true + } + + // Direct hire if player has no mercenary + const hireId = Number(cand.nameId ?? cand.mercId ?? cand.id ?? candIdx + 1) + this.hireSelectedIndex = candIdx + this._activePanel = 'none' + if (hireId > 0) { + ;(callbacks?.onHireMerc ?? this.onHireMercClick ?? this.onHireMerc)?.(npcId, hireId) + } + return true + } + } + + return true + } + + /** + * Draw the authentic Diablo II v1.13c 490x350 Mercenary Hire Stone Dialog (0x6faf9440). + */ + drawHireMercDialog( + ctx: CanvasRenderingContext2D, + font?: D2FontRenderer | null, + assets?: { + boxPiecesImg?: HTMLImageElement | null + focus16Img?: HTMLImageElement | null + textSlidImg?: HTMLImageElement | null + }, + screenWidth = 800, + screenHeight = 600, + ): void { + const boxPiecesImg = assets?.boxPiecesImg ?? this.getBoxPiecesImage() + const textSlidImg = assets?.textSlidImg ?? this.getTextSlidImage() + + const dialogX = Math.floor((screenWidth - HIRE_DIALOG_WIDTH) / 2) + const dialogY = Math.floor(screenHeight / 2) - 195 + + const fillTextSafe = (text: string, x: number, y: number, color = '#e8d090') => { + ctx.fillStyle = color + if (typeof ctx.fillText === 'function') { + ctx.fillText(text, x, y) + } + } + + const drawTextSafe = (text: string, x: number, y: number, color = 'white', align: 'left' | 'center' | 'right' = 'left') => { + const hexColor = color === 'blue' ? '#4868f8' : color === 'gold' ? '#e8c26b' : '#ffffff' + fillTextSafe(text, x, y, hexColor) + if (font && typeof font.drawText === 'function') { + font.drawText(ctx, text, x, y, { font: 'font16', color: color as any, align }) + } + } + + // 1. Outer carved stone frame (490x350) + if (boxPiecesImg) { + drawStoneBoxFrame(ctx, dialogX, dialogY, HIRE_DIALOG_WIDTH, HIRE_DIALOG_HEIGHT, boxPiecesImg) + } else { + ctx.fillStyle = 'rgba(0, 0, 0, 0.85)' + ctx.fillRect(dialogX + 1, dialogY, Math.max(0, HIRE_DIALOG_WIDTH - 2), Math.max(0, HIRE_DIALOG_HEIGHT - 2)) + ctx.strokeStyle = '#6c5838' + ctx.strokeRect(dialogX, dialogY, HIRE_DIALOG_WIDTH, HIRE_DIALOG_HEIGHT) + } + + // 2. Title Line: "Your Gold: %d Hire which Mercenary?" (ItemDesc1s) + const totalGold = (this.playerGold ?? 0) + (this.stashGold ?? 0) + const rawTitleFmt = lookupTbl('ItemDesc1s', this.lang) ?? (this.lang === 'ENG' ? 'Your Gold: %d Hire which Mercenary?' : '你的金錢: %d 雇用那個傭兵? ') + const titleText = rawTitleFmt.replace('%d', String(totalGold)) + + drawTextSafe(titleText, dialogX + Math.floor(HIRE_DIALOG_WIDTH / 2), dialogY + 21, 'white', 'center') + + // 3. Candidate List + const candidates = (this.mercHireList?.length ? this.mercHireList : this.hireList) ?? [] + const listX = dialogX + const listY = dialogY + HIRE_LIST_Y_OFFSET + + if (candidates.length === 0) { + const emptyText = lookupTbl('ItemDesc1t', this.lang) ?? (this.lang === 'ENG' ? 'There are no Mercenaries left to hire.' : '現在沒有可以雇用的傭兵。') + drawTextSafe(emptyText, dialogX + Math.floor(HIRE_DIALOG_WIDTH / 2), listY + 60, 'white', 'center') + } else { + const maxScroll = Math.max(0, candidates.length - HIRE_VISIBLE_CANDIDATES) + const scrollOffset = Math.max(0, Math.min(maxScroll, this.hireScrollOffset)) + const visibleCount = Math.min(candidates.length - scrollOffset, HIRE_VISIBLE_CANDIDATES) + + for (let relIdx = 0; relIdx < visibleCount; relIdx++) { + const candIdx = scrollOffset + relIdx + const cand = candidates[candIdx]! + const isSelected = this.hireSelectedIndex === candIdx || this.hireHoveredIndex === candIdx + const textColor = isSelected ? 'blue' : 'white' + + let line1 = cand.line1 + let line2 = cand.line2 + if (!line1) { + const cName = cand.name ?? `Mercenary #${cand.nameId ?? cand.mercId ?? candIdx + 1}` + const cLvl = cand.level ?? 1 + const cHp = cand.hp ?? cand.life ?? 40 + const cDef = cand.defense ?? 0 + const cCost = cand.cost ?? 100 + const cHireDesc = cand.hireDesc ?? '' + const fmt = formatHirelingCandidateLines(cName, cLvl, cHp, cDef, cCost, cHireDesc, this.lang) + line1 = fmt.line1 + line2 = fmt.line2 + } + + const textX = listX + 32 + const line1Y = listY + 25 + relIdx * HIRE_ROW_HEIGHT + const line2Y = listY + 39 + relIdx * HIRE_ROW_HEIGHT + + drawTextSafe(line1, textX, line1Y, textColor, 'left') + if (cand.cost !== undefined) { + fillTextSafe(`${cand.cost} Gold`, textX + 200, line1Y, '#e8c26b') + } + if (line2 && line2.trim().length > 0) { + drawTextSafe(line2, textX, line2Y, textColor, 'left') + } + } + + // 4. Scrollbar if candidates > 7 + if (candidates.length > HIRE_VISIBLE_CANDIDATES) { + this.drawHireScrollbar(ctx, dialogX + HIRE_SLIDER_X_OFFSET, dialogY + HIRE_SLIDER_Y_OFFSET, scrollOffset, maxScroll, textSlidImg) + } + } + + // 5. Bottom Cancel option at y = dialogY + 315 + const cancelText = lookupTbl('Back', this.lang) ?? (this.lang === 'ENG' ? 'cancel' : '取消') + const cancelColor = this.hireHoveredIndex === -1 ? 'blue' : 'white' + drawTextSafe(cancelText, dialogX + Math.floor(HIRE_DIALOG_WIDTH / 2), dialogY + 315, cancelColor, 'center') + + // 6. Confirmation dialog if open + if (this.replaceMercConfirmOpen && this.pendingHireCandidate) { + this.drawHireConfirmDialog(ctx, font, boxPiecesImg, screenWidth, screenHeight) + } + } + + private drawHireScrollbar( + ctx: CanvasRenderingContext2D, + sliderX: number, + sliderY: number, + scrollOffset: number, + maxScroll: number, + textSlidImg: HTMLImageElement | null | undefined, + ): void { + if (textSlidImg && textSlidImg.complete && textSlidImg.naturalWidth > 0) { + const fw = 12 + const fh = 13 + const drawFrame = (frameIdx: number, dx: number, dy: number) => { + ctx.drawImage(textSlidImg, frameIdx * fw, 0, fw, fh, dx, dy, fw, fh) + } + + // Up arrow: frame 9 + drawFrame(9, sliderX, sliderY) + + // 21 track segments: frame 13 + for (let i = 0; i < HIRE_TRACK_SEGMENTS; i++) { + drawFrame(13, sliderX, sliderY + 12 + i * 12) + } + + // Down arrow: frame 8 + drawFrame(8, sliderX, sliderY + 12 + HIRE_TRACK_SEGMENTS * 12) + + // Thumb slider + if (maxScroll > 0) { + const thumbMaxSeg = HIRE_TRACK_SEGMENTS - 3 + const thumbStartSeg = Math.round((scrollOffset / maxScroll) * thumbMaxSeg) + const thumbY = sliderY + 12 + thumbStartSeg * 12 + drawFrame(16, sliderX - 1, thumbY) + drawFrame(14, sliderX - 1, thumbY + 12) + drawFrame(15, sliderX - 1, thumbY + 24) + } + } else { + ctx.fillStyle = '#222222' + ctx.fillRect(sliderX, sliderY, 12, 12 + HIRE_TRACK_SEGMENTS * 12 + 13) + ctx.fillStyle = '#555555' + ctx.fillRect(sliderX, sliderY, 12, 12) + ctx.fillRect(sliderX, sliderY + 12 + HIRE_TRACK_SEGMENTS * 12, 12, 12) + if (maxScroll > 0) { + const thumbY = sliderY + 12 + Math.round((scrollOffset / maxScroll) * (HIRE_TRACK_SEGMENTS * 12 - 36)) + ctx.fillStyle = '#888888' + ctx.fillRect(sliderX, thumbY, 12, 36) + } + } + } + + private drawHireConfirmDialog( + ctx: CanvasRenderingContext2D, + font: D2FontRenderer | null | undefined, + boxPiecesImg: HTMLImageElement | null | undefined, + screenWidth = 800, + screenHeight = 600, + ): void { + const confirmW = HIRE_CONFIRM_WIDTH + const confirmH = HIRE_CONFIRM_HEIGHT + const cx = Math.floor((screenWidth - confirmW) / 2) + const cy = Math.floor(screenHeight / 2) - 110 + + const fillTextSafe = (text: string, x: number, y: number, color = '#e8d090') => { + ctx.fillStyle = color + if (typeof ctx.fillText === 'function') { + ctx.fillText(text, x, y) + } + } + + const drawTextSafe = (text: string, x: number, y: number, color = 'white', align: 'left' | 'center' | 'right' = 'left') => { + const hexColor = color === 'blue' ? '#4868f8' : color === 'gold' ? '#e8c26b' : '#ffffff' + fillTextSafe(text, x, y, hexColor) + if (font && typeof font.drawText === 'function') { + font.drawText(ctx, text, x, y, { font: 'font16', color: color as any, align }) + } + } + + // Shadow backdrop + ctx.fillStyle = 'rgba(0, 0, 0, 0.65)' + ctx.fillRect(0, 0, screenWidth, screenHeight) + + // Frame + if (boxPiecesImg) { + drawStoneBoxFrame(ctx, cx, cy, confirmW, confirmH, boxPiecesImg) + } else { + ctx.fillStyle = 'rgba(0, 0, 0, 0.85)' + ctx.fillRect(cx + 1, cy, Math.max(0, confirmW - 2), Math.max(0, confirmH - 2)) + ctx.strokeStyle = '#6c5838' + ctx.strokeRect(cx, cy, confirmW, confirmH) + } + + // Warning text: "This Mercenary will replace your current one." (VerifyTransaction9) + const warnText = lookupTbl('VerifyTransaction9', this.lang) ?? (this.lang === 'ENG' ? 'This Mercenary will replace your current one.' : '這個傭兵會換掉前一位。') + if (font) { + this.drawWrappedText(ctx, font, warnText, cx + 24, cy + 32, confirmW - 48, 16, 4, 'font16', 'gold', 'center') + } else { + fillTextSafe(warnText, cx + 24, cy + 32, '#e8c26b') + } + + // Option 1: Yes + const yesText = lookupTbl('VerifyTransaction1', this.lang) ?? (this.lang === 'ENG' ? 'Yes' : '是') + const yesColor = this.hireConfirmHoveredOption === 0 ? 'blue' : 'white' + drawTextSafe(yesText, cx + Math.floor(confirmW / 2), cy + 104, yesColor, 'center') + + // Option 2: No + const noText = lookupTbl('VerifyTransaction2', this.lang) ?? (this.lang === 'ENG' ? 'No' : '否') + const noColor = this.hireConfirmHoveredOption === 1 ? 'blue' : 'white' + drawTextSafe(noText, cx + Math.floor(confirmW / 2), cy + 128, noColor, 'center') + } + private drawActTabs( ctx: CanvasRenderingContext2D, ox: number, @@ -3738,6 +4254,10 @@ export class WorldPanelsHud { const my = typeof arg1 === 'string' ? (typeof arg3 === 'number' ? arg3 : 0) : arg2 const callbacks = typeof arg1 === 'string' ? callbacksArg : typeof arg3 === 'object' ? arg3 : callbacksArg + if (panel === 'hire') { + return this.handleHireMercClick(mx, my, callbacks) + } + if ( panel !== 'merc' && panel !== 'hire' && @@ -3784,25 +4304,6 @@ export class WorldPanelsHud { return true } - if (panel === 'hire') { - const list = (this.mercHireList?.length ? this.mercHireList : this.hireList) ?? [] - for (let i = 0; i < list.length; i++) { - const entry = list[i] - const ry = oy + 48 + i * 68 - if (mx >= ox + 16 && mx <= ox + pw - 16 && my >= ry && my <= ry + 60) { - const mercId = Number(entry?.nameId ?? entry?.mercId ?? entry?.id ?? i + 1) - const npcId = Number( - this._npcMenu?.npcUnitId ?? this._npcMenu?.npcId ?? this.activeNpcId ?? this.vendorNpcId ?? 0, - ) - if (mercId > 0) { - ;(callbacks?.onHireMerc ?? this.onHireMercClick ?? this.onHireMerc)?.(npcId, mercId) - } - return true - } - } - return true - } - if (panel === 'party') { for (let i = 0; i < this.partyMembers.length; i++) { const member = this.partyMembers[i] @@ -3930,10 +4431,26 @@ export class WorldPanelsHud { * Render left-side panels (`merc`, `hire`, `party`, `trade`, `imbue`, `add_sockets`, `personalize`) * using safe TBL lookups (`lookupTbl(key, this.lang) ?? fallback`) and 2D canvas primitives. */ - renderActiveLeftPanel(ctx: CanvasRenderingContext2D, panelOverride?: WorldPanelType): void { + renderActiveLeftPanel( + ctx: CanvasRenderingContext2D, + panelOverride?: WorldPanelType, + fontRenderer?: D2FontRenderer, + assets?: { + boxPiecesImg?: HTMLImageElement | null + focus16Img?: HTMLImageElement | null + textSlidImg?: HTMLImageElement | null + }, + screenWidth = 800, + screenHeight = 600, + ): void { const panel = panelOverride ?? this._activePanel if (panel === 'none') return + if (panel === 'hire') { + this.drawHireMercDialog(ctx, fontRenderer, assets, screenWidth, screenHeight) + return + } + const ox = 80 const oy = 60 const pw = 320 @@ -4035,37 +4552,6 @@ export class WorldPanelsHud { return } - if (panel === 'hire') { - const header = lookupTbl('NotUsed150', this.lang) ?? (isEn ? 'Hire Mercenary' : '僱用傭兵') - fillTextSafe(header, ox + 160, oy + 24, '#e8c26b') - - const list = (this.mercHireList?.length ? this.mercHireList : this.hireList) ?? [] - for (let i = 0; i < list.length; i++) { - const entry = list[i]! - const ry = oy + 48 + i * 68 - ctx.fillStyle = 'rgba(28, 24, 18, 0.9)' - ctx.fillRect(ox + 16, ry, pw - 32, 60) - ctx.strokeStyle = '#6c5838' - ctx.strokeRect(ox + 16, ry, pw - 32, 60) - const cName = entry.name ?? `Merc #${entry.nameId ?? entry.mercId ?? i + 1}` - const cLevel = entry.level ?? 1 - const cCost = entry.cost ?? 0 - const cLife = entry.life ?? entry.hp ?? 100 - const cStr = entry.strength ?? 0 - const cDex = entry.dexterity ?? 0 - const cDef = entry.defense ?? 0 - fillTextSafe(`${cName} (Lv.${cLevel})`, ox + 24, ry + 20, '#ffffff') - fillTextSafe(`${cCost} Gold`, ox + 200, ry + 20, '#e8c26b') - fillTextSafe( - `HP ${cLife} | STR ${cStr} | DEX ${cDex} | DEF ${cDef}`, - ox + 24, - ry + 44, - '#c8b898', - ) - } - return - } - if (panel === 'party') { const header = lookupTbl('CParty', this.lang) ?? (isEn ? 'Party' : '隊伍') fillTextSafe(header, ox + 160, oy + 24, '#e8c26b') diff --git a/src/client/world/client-world.ts b/src/client/world/client-world.ts index ab4ac53..42afcac 100644 --- a/src/client/world/client-world.ts +++ b/src/client/world/client-world.ts @@ -922,7 +922,7 @@ export class ClientWorld { this._containers.clearShop() this._npcInteraction = { ...cur, npcId: cmd.npcId, window: 'gamble', activeWindow: 'gamble' } } else if (cmd.action === 3) { - this._npcInteraction = { ...cur, npcId: cmd.npcId, window: 'none', activeWindow: 'hire' } + this._npcInteraction = { ...cur, npcId: cmd.npcId, window: 'hire', activeWindow: 'hire' } } break case 'NpcCancel': diff --git a/src/common/data/canonical-tbl-strings.ts b/src/common/data/canonical-tbl-strings.ts index 1fce1cf..e10b206 100644 --- a/src/common/data/canonical-tbl-strings.ts +++ b/src/common/data/canonical-tbl-strings.ts @@ -1,5 +1,5 @@ /** - * AUTO-GENERATED by `scripts/pack-canonical-tbl.ts` from authoritative 1.13c MPQ archives: + * AUTO-GENERATED by `src/baker/packers/pack-canonical-tbl.ts` from authoritative 1.13c MPQ archives: * - `d2data.mpq` (`string.tbl`) * - `d2exp.mpq` (`expansionstring.tbl`) * - `Patch_D2.mpq` (`patchstring.tbl`) @@ -203,6 +203,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "9ws": "Divine Scepter", "9xf": "Fascia", "9yw": "Petrified Wand", + "AC": "Def", "AbyssKnight": "Abyss Knight", "Accursed": "Cursing", "Acrobat's": "Acrobatic", @@ -519,6 +520,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Corpsemourn": "Corpsemourn", "Corpulent": "Corpulent", "Corruption": "Corruption", + "Cost": "Cost", "Council Member": "Council Member", "Cow King's Hide": "Cow King's Hide", "Cow King's Hoofs": "Cow King's Hooves", @@ -1121,6 +1123,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Gull": "Gull", "Gutsiphon": "Gut Siphon", "Gymnast's": "Gymnastic", + "HP": "Life", "Haemosu's Adament": "Haemosu's Adamant", "Hailstone": "Hailstone", "Halaberd's Reign": "Halaberd's Reign", @@ -1258,6 +1261,8 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Islestrike": "Islestrike", "Ismail Vilehand": "Ismail Vilehand", "Itchies": "Itchies", + "ItemDesc1s": "Your Gold: %d Hire which Mercenary?", + "ItemDesc1t": "There are no Mercenaries left to hire.", "ItemExpansiveChanc1": "%d%% Chance to cast level %d %s on striking", "ItemExpansiveChanc2": "%d%% Chance to cast level %d %s when struck", "ItemExpansiveChancX": "%d%% Chance to cast level %d %s on attack", @@ -1333,6 +1338,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Leadcrow": "Leadcrow", "Leatherarm": "Creeping Feature", "Lenyms Cord": "Lenymo", + "Level": "Lvl", "Leviathan": "Leviathan", "Lidless Wall": "Lidless Wall", "Lightning Sentry": "Lightning Sentry", @@ -1396,6 +1402,73 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Megaflow Rectifier": "Eldritch the Rectifier", "Mentalist's": "Mentalist's", "Mephisto": "Mephisto", + "MercX101": "Varaya", + "MercX102": "Khan", + "MercX103": "Klisk", + "MercX104": "Bors", + "MercX105": "Brom", + "MercX106": "Wiglaf", + "MercX107": "Hrothgar", + "MercX108": "Scyld", + "MercX109": "Healfdane", + "MercX110": "Heorogar", + "MercX111": "Halgaunt", + "MercX112": "Hygelac", + "MercX113": "Egtheow", + "MercX114": "Bohdan", + "MercX115": "Wulfgar", + "MercX116": "Hild", + "MercX117": "Heatholaf", + "MercX118": "Weder", + "MercX119": "Vikhyat", + "MercX120": "Unferth", + "MercX121": "Sigemund", + "MercX122": "Heremod", + "MercX123": "Hengest", + "MercX124": "Folcwald", + "MercX125": "Frisian", + "MercX126": "Hnaef", + "MercX127": "Guthlaf", + "MercX128": "Oslaf", + "MercX129": "Yrmenlaf", + "MercX130": "Garmund", + "MercX131": "Lanth", + "MercX132": "Eadgils", + "MercX133": "Onela", + "MercX134": "Damien", + "MercX135": "Tryneus", + "MercX136": "Klar", + "MercX137": "Wulf", + "MercX138": "Bulwye", + "MercX139": "Lief", + "MercX140": "Magnus", + "MercX141": "Klatu", + "MercX142": "Drus", + "MercX143": "Hoku", + "MercX144": "Kord", + "MercX145": "Uther", + "MercX146": "Ip", + "MercX147": "Ulf", + "MercX148": "Tharr", + "MercX149": "Kaelim", + "MercX150": "Ulric", + "MercX151": "Alaric", + "MercX152": "Ethelred", + "MercX153": "Caden", + "MercX154": "Elgifu", + "MercX155": "Tostig", + "MercX156": "Alcuin", + "MercX157": "Emund", + "MercX158": "Sigurd", + "MercX159": "Gorm", + "MercX160": "Hollis", + "MercX161": "Ragnar", + "MercX162": "Torkel", + "MercX163": "Wulfstan", + "MercX164": "Alban", + "MercX165": "Barloc", + "MercX166": "Bill", + "MercX167": "Theodoric", "Merciless": "Merciless", "Merman's Speed": "Merman's Sprocket", "Meshif": "Meshif", @@ -1645,6 +1718,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Mosers Blessed Circle": "Moser's Blessed Circle", "MummyGenerator": "Mummy Sarcophagus", "NPCIdentify1": "Identify Items", + "NPCMenuHire": "hire", "NPCMenuTrade": "trade", "NPCMenuTradeRepair": "trade/repair", "NPCPurchaseItems": "Buy", @@ -1706,6 +1780,9 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Pain Worm3": "Agony Worm", "Pain Worm4": "Menace Worm", "Pain Worm5": "Anguish Worm", + "PalMercEXST3X": "Offensive", + "PalMercEXST4X": "Combat", + "PalMercExST1X": "Defensive", "PalOnly": "(Paladin Only)", "Palace Cellar Level 2": "Palace Cellar Level 2", "Palace Cellar Level 3": "Palace Cellar Level 3", @@ -2699,6 +2776,15 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Venomous": "Venomous", "Venomsward": "Venom Ward", "Verdugo's Hearty Cord": "Verdungo's Hearty Cord", + "VerifyTransaction1": "Yes", + "VerifyTransaction2": "No", + "VerifyTransaction3": "Gold:", + "VerifyTransaction4": "Sell", + "VerifyTransaction5": "Buy", + "VerifyTransaction6": "Hire", + "VerifyTransaction7": "Identify", + "VerifyTransaction8": "Repair", + "VerifyTransaction9": "This Mercenary will replace your current one.", "Vermillion": "Vermilion", "Veteran's": "Veteran's", "Vicious": "Vicious", @@ -2919,6 +3005,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "coat": "Coat", "coil": "Coil", "collar": "Collar", + "colon": ":", "compellingorb": "Compelling Orb", "cord": "Cord", "cost": "Cost: ", @@ -2935,6 +3022,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "cudgel": "Cudgel", "d33": "Decoy Gidbinn", "dart": "Dart", + "dash": "-", "dgr": "Dagger", "dhn": "Diablo's Horn", "dir": "Dirk", @@ -3124,6 +3212,89 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "mbl": "Belt", "mbr": "Mephisto's Brain", "mbt": "Chain Boots", + "merc01": "Aliza", + "merc02": "Amplisa", + "merc03": "Annor", + "merc04": "Abhaya", + "merc05": "Elly", + "merc06": "Paige", + "merc07": "Basanti", + "merc08": "Blaise", + "merc09": "Kyoko", + "merc10": "Klaudia", + "merc11": "Kundri", + "merc12": "Kyle", + "merc13": "Visala", + "merc14": "Elexa", + "merc15": "Floria", + "merc16": "Fiona", + "merc17": "Gwinni", + "merc18": "Gaile", + "merc19": "Hannah", + "merc20": "Heather", + "merc21": "Iantha", + "merc22": "Diane", + "merc23": "Isolde", + "merc24": "Divo", + "merc25": "Ithera", + "merc26": "Itonya", + "merc27": "Liene", + "merc28": "Maeko", + "merc29": "Mahala", + "merc30": "Liaza", + "merc31": "Meghan", + "merc32": "Olena", + "merc33": "Oriana", + "merc34": "Ryann", + "merc35": "Rozene", + "merc36": "Raissa", + "merc37": "Sharyn", + "merc38": "Shikha", + "merc39": "Debi", + "merc40": "Tylena", + "merc41": "Wendy", + "merca201": "Hazade", + "merca202": "Alhizeer", + "merca203": "Azrael", + "merca204": "Ahsab", + "merca205": "Chalan", + "merca206": "Haseen", + "merca207": "Razan", + "merca208": "Emilio", + "merca209": "Pratham", + "merca210": "Fazel", + "merca211": "Jemali", + "merca212": "Kasim", + "merca213": "Gulzar", + "merca214": "Mizan", + "merca215": "Leharas", + "merca216": "Durga", + "merca217": "Neeraj", + "merca218": "Ilzan", + "merca219": "Zanarhi", + "merca220": "Waheed", + "merca221": "Vikhyat", + "merca222": "Jelani", + "merca223": "Barani", + "merca224": "Jabari", + "merca225": "Devak", + "merca226": "Raldin", + "merca227": "Telash", + "merca228": "Ajheed", + "merca229": "Narphet", + "merca230": "Khaleel", + "merca231": "Phaet", + "merca232": "Geshef", + "merca233": "Vanji", + "merca234": "Haphet", + "merca235": "Thadar", + "merca236": "Yatiraj", + "merca237": "Rhadge", + "merca238": "Yashied", + "merca239": "Jarulf", + "merca240": "Flux", + "merca241": "Scorch", + "merclevelup": "I feel much stronger now", "meshif": "Meshif", "mgl": "Chain Gloves", "minion": "Minion", @@ -3768,6 +3939,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "sml": "Small Shield", "sol": "Soul", "song": "Song", + "space": " ", "spawn": "Spawn", "spc": "Spiked Club", "spe": "Spleen", @@ -3845,6 +4017,16 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "strchrstm": "Stamina", "strchrstr": "Strength", "strchrvit": "Vitality", + "strhirespecial1": "Fire Arrow", + "strhirespecial10": "Fire, Fast Cast", + "strhirespecial2": "Cold Arrow", + "strhirespecial3": "Jab Attack", + "strhirespecial4": "Poison Resistant", + "strhirespecial5": "Lightning", + "strhirespecial6": "Cold", + "strhirespecial7": "Fire", + "strhirespecial8": "Lightning, Fast Cast", + "strhirespecial9": "Cold, Fast Cast", "stu": "Studded Leather", "stun attack": "Stun Attack", "suit": "Suit", @@ -4203,6 +4385,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "9ws": "神屬權杖", "9xf": "格斗爪", "9yw": "淨化之杖", + "AC": "防禦", "AbyssKnight": "地獄騎士", "Act 5 Combatant": "野蛮人战士", "Act 5 Townguard": "守卫", @@ -4484,6 +4667,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Corpsemourn": "尸体的哀伤", "Corpulent": "癡肥怪", "Corruption": "毀滅之", + "Cost": "花費", "Council Member": "議會成員", "Cow King's Hide": "牛魔王之皮", "Cow King's Hoofs": "牛魔王之蹄", @@ -5018,6 +5202,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Guillaume's Face": "吉永之脸", "Gull": "海鷗", "Gutsiphon": "吸魂器", + "HP": "生命", "Haemosu's Adament": "解慕漱的坚决", "Hailstone": "霰之", "Halaberd's Reign": "哈拉伯德的统治", @@ -5143,6 +5328,8 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Islestrike": "岛屿击", "Ismail Vilehand": "邪惡之手-伊斯梅爾", "Itchies": "渴望者", + "ItemDesc1s": "你的金錢: %d 雇用那個傭兵? ", + "ItemDesc1t": "現在沒有可以雇用的傭兵。", "ItemExpansiveChanc1": "击中时有 %d%% 几率施展等级 %d %s", "ItemExpansiveChanc2": "受击时有 %d%% 几率施展等级 %d %s", "ItemExpansiveChancX": "攻击时有 %d%% 几率施展等级 %d %s", @@ -5208,6 +5395,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Leadcrow": "引導烏鴉", "Leatherarm": "爬行的容貌", "Lenyms Cord": "雷尼摩", + "Level": "等級", "Leviathan": "利维坦", "Lidless Wall": "警戒之墙", "Lightning Sentry": "闪电卫哨", @@ -5262,6 +5450,73 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Medusa's Gaze": "美杜莎的凝视", "Megaflow Rectifier": "矫正者怪异", "Mephisto": "墨菲斯托", + "MercX101": "伐德哈卡", + "MercX102": "可汗", + "MercX103": "克理斯克", + "MercX104": "柏斯", + "MercX105": "波姆", + "MercX106": "衛格萊夫", + "MercX107": "霍勒斯加", + "MercX108": "西勒德", + "MercX109": "海爾夫坦", + "MercX110": "海波加", + "MercX111": "海爾干特", + "MercX112": "海加力克", + "MercX113": "艾格索", + "MercX114": "波丹", + "MercX115": "伍夫加", + "MercX116": "希爾德", + "MercX117": "希斯洛夫", + "MercX118": "魏德", + "MercX119": "維克海亞特", + "MercX120": "安佛斯", + "MercX121": "席格龐特", + "MercX122": "希爾穆特", + "MercX123": "罕格斯特", + "MercX124": "弗克魏爾德", + "MercX125": "弗雷斯坦", + "MercX126": "荷那夫", + "MercX127": "高斯里夫", + "MercX128": "歐斯拉夫", + "MercX129": "亞門拉夫", + "MercX130": "加門得", + "MercX131": "菲瓦夫", + "MercX132": "艾得格理斯", + "MercX133": "奧蕾那", + "MercX134": "戴米安", + "MercX135": "艾弗", + "MercX136": "沃斯坦", + "MercX137": "沃夫", + "MercX138": "布溫", + "MercX139": "理夫", + "MercX140": "馬格納斯", + "MercX141": "格拉圖", + "MercX142": "圖魯斯", + "MercX143": "候克", + "MercX144": "科杜", + "MercX145": "猶瑟r", + "MercX146": "艾普", + "MercX147": "伍夫", + "MercX148": "撒爾", + "MercX149": "卡林姆", + "MercX150": "烏瑞克", + "MercX151": "艾拉瑞克", + "MercX152": "愛瑟瑞得", + "MercX153": "卡登", + "MercX154": "伊爾吉夫", + "MercX155": "托斯提克", + "MercX156": "艾爾昆", + "MercX157": "艾蒙特", + "MercX158": "席古德", + "MercX159": "格姆", + "MercX160": "荷理斯", + "MercX161": "拉格那", + "MercX162": "托克爾", + "MercX163": "沃夫斯坦", + "MercX164": "阿爾班", + "MercX165": "巴洛克", + "MercX166": "比爾", + "MercX167": "狄奧多理", "Merciless": "絕情之", "Merman's Speed": "鱼人的链轮", "Meshif": "馬席夫", @@ -5509,6 +5764,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Mosers Blessed Circle": "摩西尔的祝福之环", "MummyGenerator": "木乃伊石棺", "NPCIdentify1": "辨視物品", + "NPCMenuHire": "雇用", "NPCMenuTrade": "交易", "NPCMenuTradeRepair": "交易/修理", "NPCPurchaseItems": "買", @@ -5564,6 +5820,9 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Pain Worm3": "苦难之虫", "Pain Worm4": "威胁之虫", "Pain Worm5": "剧痛之虫", + "PalMercEXST3X": "進攻", + "PalMercEXST4X": "戰鬥", + "PalMercExST1X": "防禦", "PalOnly": "(限聖騎士)", "Palace Cellar Level 2": "皇宮監牢第二層", "Palace Cellar Level 3": "皇宮監牢第三層", @@ -5670,17 +5929,90 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "RunOn": "跑步", "Rune": "符號之", "Runemaster": "符文大师", + "Runeword1": "远古誓言", + "Runeword10": "烙印", + "Runeword103": "凤凰", "Runeword108": "祈祷", - "Runeword122": "庇护所", + "Runeword109": "骄傲", + "Runeword11": "死亡呼吸", + "Runeword110": "原则", + "Runeword112": "审慎", + "Runeword116": "光辉", + "Runeword117": "雨", + "Runeword120": "押韵", + "Runeword121": "裂隙", + "Runeword122": "圣堂", + "Runeword126": "寂静", + "Runeword128": "烟雾", + "Runeword13": "战争召唤", + "Runeword130": "精神", + "Runeword131": "灿烂", + "Runeword133": "隐秘", + "Runeword134": "钢铁", + "Runeword137": "石块", + "Runeword139": "力量", + "Runeword14": "荣耀之链", "Runeword142": "恐惧", + "Runeword148": "背信", "Runeword153": "复仇", - "Runeword154": "毒液的", + "Runeword154": "毒液", + "Runeword16": "混沌", + "Runeword160": "财富", + "Runeword162": "白色", + "Runeword163": "风", + "Runeword168": "愤怒", "Runeword17": "新月", - "Runeword2": "世界末日之", + "Runeword170": "和风", + "Runeword2": "毁天灭地", + "Runeword20": "死神", "Runeword22": "迪勒瑞姆", + "Runeword25": "毁灭", + "Runeword26": "末日", + "Runeword27": "飞龙", + "Runeword29": "梦境", + "Runeword30": "强制", + "Runeword31": "边缘", + "Runeword33": "谜团", + "Runeword34": "启迪", + "Runeword36": "永恒", + "Runeword37": "流亡", + "Runeword38": "信心", + "Runeword39": "饥荒", + "Runeword4": "野兽", + "Runeword41": "刚毅", "Runeword44": "狂怒", + "Runeword45": "幽暗", + "Runeword47": "悔恨", + "Runeword48": "正义之手", + "Runeword49": "和谐", + "Runeword51": "橡树之心", + "Runeword54": "圣雷", + "Runeword55": "荣耀", "Runeword58": "饥饿", + "Runeword59": "寒冰", + "Runeword6": "黑色", + "Runeword60": "无限", + "Runeword62": "眼光", + "Runeword65": "国王之恩典", + "Runeword66": "弑君者", + "Runeword69": "最后希望", + "Runeword71": "执法者", + "Runeword72": "叶", "Runeword73": "闪电", + "Runeword74": "狮心", + "Runeword75": "知识", + "Runeword8": "骸骨", + "Runeword81": "怨恨", + "Runeword82": "旋律", + "Runeword83": "记忆", + "Runeword87": "神话", + "Runeword88": "底韵", + "Runeword9": "野蔷薇", + "Runeword91": "誓约", + "Runeword92": "遵从", + "Runeword95": "热情", + "Runeword98": "和平", + "Runeword99": "理性之声", "Russet": "赤楬之", "Rusthandle": "生銹的把手", "SaberCat": "劍齒貓", @@ -5739,7 +6071,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Silver": "銀之", "Skeleton": "骷髏", "SkeletonArcher": "骷髏弓箭手", - "Skillname222": "渡鸟之", + "Skillname222": "乌鸦", "Skillname223": "剧毒蔓藤", "Skillname224": "狼人變化", "Skillname225": "狼人变化", @@ -5767,7 +6099,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Skillname247": "荆棘之灵", "Skillname248": "召唤灰熊", "Skillname249": "狂怒", - "Skillname250": "世界末日之", + "Skillname250": "毁天灭地", "Skillname251": "飓风", "Skillname252": "火焰爆震", "Skillname253": "支配利爪", @@ -5796,7 +6128,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Skillname276": "飞龙在天", "Skillname277": "亡者守卫", "Skillname278": "刃之盾", - "Skillname279": "毒液的", + "Skillname279": "毒液", "Skillname280": "暗影宗师", "Skillname281": "凤凰击", "Skin of the Flayerd One": "剥皮者之皮", @@ -6317,6 +6649,15 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "VenomLord": "邪魔之王", "Venomsward": "毒液牢房", "Verdugo's Hearty Cord": "维尔登戈的心结", + "VerifyTransaction1": "是", + "VerifyTransaction2": "否", + "VerifyTransaction3": "金幣:", + "VerifyTransaction4": "賣", + "VerifyTransaction5": "買", + "VerifyTransaction6": "雇用", + "VerifyTransaction7": "辨視", + "VerifyTransaction8": "修復", + "VerifyTransaction9": "這個傭兵會換掉前一位。", "Vicious": "惡毒之", "Victors Silk": "勝利者之絲綢", "Vidala's Ambush": "維達拉的突襲", @@ -6525,6 +6866,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "coat": "外袍", "coil": "盤繞", "collar": "項圈", + "colon": ":", "compellingorb": "強制球體", "cord": "捆綁", "cost": "花費:", @@ -6541,6 +6883,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "cudgel": "木棒", "d33": "吉得賓誘餌", "dart": "鏢", + "dash": "-", "dgr": "匕首", "dhn": "暗黑破壞神之角", "dir": "長匕首", @@ -6730,6 +7073,89 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "mbl": "扣帶", "mbr": "曼菲斯托之腦", "mbt": "鎖鍊靴", + "merc01": "阿里撒", + "merc02": "安普利沙", + "merc03": "安諾", + "merc04": "艾伯海亞", + "merc05": "埃利", + "merc06": "派吉", + "merc07": "巴森提", + "merc08": "布萊謝", + "merc09": "京子", + "merc10": "克勞蒂亞", + "merc11": "肯吉", + "merc12": "凱爾", + "merc13": "維沙利亞", + "merc14": "依麗莎", + "merc15": "弗蘿利亞", + "merc16": "菲歐娜", + "merc17": "基溫尼", + "merc18": "蓋爾", + "merc19": "哈娜", + "merc20": "海瑟", + "merc21": "艾蓮娜", + "merc22": "迪亞娜", + "merc23": "依索迪", + "merc24": "迪佛", + "merc25": "依索拉", + "merc26": "衣東亞", + "merc27": "萊茵", + "merc28": "梅可", + "merc29": "馬哈拉", + "merc30": "萊沙", + "merc31": "梅葛哈", + "merc32": "奧蓮娜", + "merc33": "奧麗亞娜", + "merc34": "萊恩", + "merc35": "羅珊妮", + "merc36": "萊紗", + "merc37": "莎倫", + "merc38": "席克哈", + "merc39": "戴比", + "merc40": "泰蓮娜", + "merc41": "溫蒂", + "merca201": "哈撒迪", + "merca202": "艾利希爾", + "merca203": "阿撒爾", + "merca204": "亞斯哈", + "merca205": "克哈蘭", + "merca206": "哈辛", + "merca207": "羅森", + "merca208": "艾米羅", + "merca209": "培撒姆", + "merca210": "費索", + "merca211": "傑馬利", + "merca212": "卡辛姆", + "merca213": "吉爾薩", + "merca214": "米山", + "merca215": "雷哈拉", + "merca216": "迪爾卡", + "merca217": "尼爾亞", + "merca218": "衣利森", + "merca219": "撒那利", + "merca220": "瓦黑德", + "merca221": "維克懷特", + "merca222": "喬安妮", + "merca223": "巴拉尼", + "merca224": "賈巴利", + "merca225": "迪法", + "merca226": "拉爾丁", + "merca227": "鐵拉西", + "merca228": "艾傑希德", + "merca229": "拿費特", + "merca230": "凱立爾", + "merca231": "費特", + "merca232": "杰希夫", + "merca233": "凡吉", + "merca234": "海費特", + "merca235": "撒達", + "merca236": "亞提賈", + "merca237": "拉黑德", + "merca238": "亞席德", + "merca239": "賈魯夫", + "merca240": "弗勒克斯", + "merca241": "斯高屈", + "merclevelup": "我現在覺得強壯多了", "meshif": "馬席夫", "mgl": "鎖鍊手套", "minion": "嘍囉", @@ -7177,6 +7603,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "sml": "小盾牌", "sol": "灵魂之", "song": "歌", + "space": " ", "spawn": "重生的", "spc": "狼牙棒", "spe": "一股邪恶力量", @@ -7254,6 +7681,16 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "strchrstm": "耐力", "strchrstr": "力量", "strchrvit": "體力", + "strhirespecial1": "火箭", + "strhirespecial10": "快速施展火焰", + "strhirespecial2": "冰箭", + "strhirespecial3": "戳刺攻擊", + "strhirespecial4": "抗毒", + "strhirespecial5": "閃電", + "strhirespecial6": "冷", + "strhirespecial7": "火", + "strhirespecial8": "快速施展閃電", + "strhirespecial9": "快速施展冰冷", "stu": "鑲嵌甲", "stun attack": "昏眩攻擊", "suit": "衣", diff --git a/src/common/items/baked-ui-meta.ts b/src/common/items/baked-ui-meta.ts index bfcf300..551492d 100644 --- a/src/common/items/baked-ui-meta.ts +++ b/src/common/items/baked-ui-meta.ts @@ -293595,6 +293595,7 @@ export const BAKED_UI_MANIFEST: BakedUiManifest = { "vendorTabs": "/ui/vendor-tabs.png", "boxPieces": "/ui/boxpieces.png", "focus16": "/ui/focus16.png", + "textslid": "/ui/textslid.png", "popbelt": "/ui/popbelt.png", "minipanel": "/ui/minipanel.png", "minipanelBtns": "/ui/minipanel-btns.png", diff --git a/src/common/units/hireling-table.ts b/src/common/units/hireling-table.ts new file mode 100644 index 0000000..d176861 --- /dev/null +++ b/src/common/units/hireling-table.ts @@ -0,0 +1,4520 @@ +/** + * Canonical Diablo II v1.13c Hireling data table & stat resolver. + * + * Ground Truth References: + * - `Hireling.txt` / `hireling.bin` (120 rows) + * - D2Common.dll ordinal 10988 (0x6fd7c9e0) -> Act index resolution + * - D2Common.dll ordinal 10458 (0x6fd7ccc0) -> 64-bit LCG candidate evaluation + * - D2Common.dll ordinal 10091 (0x6fd7c9c0) -> Ability description string resolution + * - D2Client.dll 0x6faf9440 -> Candidate dialog line formatting + * + * DO NOT EDIT MANUALLY. + */ + +import { lookupTbl } from '../formats/tbl.ts' + +export interface HirelingRecord { + readonly version: number + readonly subTypeId: number + readonly classId: number + readonly act: number + readonly difficulty: number + readonly seller: number + readonly hireCost: number + readonly level: number + readonly expPerLvl: number + readonly hp: number + readonly hpPerLvl: number + readonly defense: number + readonly defPerLvl: number + readonly str: number + readonly strPerLvl: number + readonly dex: number + readonly dexPerLvl: number + readonly ar: number + readonly arPerLvl: number + readonly dmgMin: number + readonly dmgMax: number + readonly dmgPerLvl: number + readonly resist: number + readonly resistPerLvl: number + readonly hireDesc: number + readonly nameFirstKey: string + readonly nameLastKey: string + readonly wNameFirstId: number + readonly wNameLastId: number + readonly skills: readonly number[] +} + +export const HIRELING_RECORDS: readonly HirelingRecord[] = [ + { + "version": 0, + "subTypeId": 0, + "classId": 271, + "act": 1, + "difficulty": 1, + "seller": 150, + "hireCost": 100, + "level": 3, + "expPerLvl": 100, + "hp": 45, + "hpPerLvl": 8, + "defense": 15, + "defPerLvl": 6, + "str": 35, + "strPerLvl": 10, + "dex": 45, + "dexPerLvl": 16, + "ar": 10, + "arPerLvl": 10, + "dmgMin": 1, + "dmgMax": 3, + "dmgPerLvl": 2, + "resist": 0, + "resistPerLvl": 8, + "hireDesc": 1, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 7 + ] + }, + { + "version": 0, + "subTypeId": 0, + "classId": 271, + "act": 1, + "difficulty": 1, + "seller": 150, + "hireCost": 100, + "level": 25, + "expPerLvl": 100, + "hp": 221, + "hpPerLvl": 13, + "defense": 147, + "defPerLvl": 12, + "str": 63, + "strPerLvl": 10, + "dex": 89, + "dexPerLvl": 16, + "ar": 230, + "arPerLvl": 20, + "dmgMin": 7, + "dmgMax": 9, + "dmgPerLvl": 4, + "resist": 44, + "resistPerLvl": 7, + "hireDesc": 1, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 7 + ] + }, + { + "version": 0, + "subTypeId": 0, + "classId": 271, + "act": 1, + "difficulty": 1, + "seller": 150, + "hireCost": 100, + "level": 49, + "expPerLvl": 100, + "hp": 533, + "hpPerLvl": 25, + "defense": 435, + "defPerLvl": 18, + "str": 93, + "strPerLvl": 10, + "dex": 137, + "dexPerLvl": 16, + "ar": 710, + "arPerLvl": 30, + "dmgMin": 19, + "dmgMax": 21, + "dmgPerLvl": 6, + "resist": 86, + "resistPerLvl": 5, + "hireDesc": 1, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 7 + ] + }, + { + "version": 0, + "subTypeId": 1, + "classId": 271, + "act": 1, + "difficulty": 1, + "seller": 150, + "hireCost": 150, + "level": 3, + "expPerLvl": 105, + "hp": 45, + "hpPerLvl": 8, + "defense": 15, + "defPerLvl": 6, + "str": 35, + "strPerLvl": 10, + "dex": 45, + "dexPerLvl": 16, + "ar": 10, + "arPerLvl": 10, + "dmgMin": 1, + "dmgMax": 3, + "dmgPerLvl": 2, + "resist": 0, + "resistPerLvl": 8, + "hireDesc": 2, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 11 + ] + }, + { + "version": 0, + "subTypeId": 1, + "classId": 271, + "act": 1, + "difficulty": 1, + "seller": 150, + "hireCost": 150, + "level": 25, + "expPerLvl": 105, + "hp": 221, + "hpPerLvl": 12, + "defense": 147, + "defPerLvl": 12, + "str": 63, + "strPerLvl": 10, + "dex": 89, + "dexPerLvl": 16, + "ar": 230, + "arPerLvl": 20, + "dmgMin": 7, + "dmgMax": 9, + "dmgPerLvl": 4, + "resist": 44, + "resistPerLvl": 7, + "hireDesc": 2, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 11 + ] + }, + { + "version": 0, + "subTypeId": 1, + "classId": 271, + "act": 1, + "difficulty": 1, + "seller": 150, + "hireCost": 150, + "level": 49, + "expPerLvl": 105, + "hp": 509, + "hpPerLvl": 16, + "defense": 435, + "defPerLvl": 18, + "str": 93, + "strPerLvl": 10, + "dex": 137, + "dexPerLvl": 16, + "ar": 710, + "arPerLvl": 30, + "dmgMin": 19, + "dmgMax": 21, + "dmgPerLvl": 6, + "resist": 86, + "resistPerLvl": 5, + "hireDesc": 2, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 11 + ] + }, + { + "version": 0, + "subTypeId": 2, + "classId": 271, + "act": 1, + "difficulty": 2, + "seller": 150, + "hireCost": 6000, + "level": 25, + "expPerLvl": 110, + "hp": 199, + "hpPerLvl": 12, + "defense": 132, + "defPerLvl": 12, + "str": 60, + "strPerLvl": 10, + "dex": 84, + "dexPerLvl": 16, + "ar": 207, + "arPerLvl": 20, + "dmgMin": 6, + "dmgMax": 8, + "dmgPerLvl": 4, + "resist": 41, + "resistPerLvl": 7, + "hireDesc": 1, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 7 + ] + }, + { + "version": 0, + "subTypeId": 2, + "classId": 271, + "act": 1, + "difficulty": 2, + "seller": 150, + "hireCost": 6000, + "level": 49, + "expPerLvl": 110, + "hp": 487, + "hpPerLvl": 16, + "defense": 420, + "defPerLvl": 18, + "str": 90, + "strPerLvl": 10, + "dex": 132, + "dexPerLvl": 16, + "ar": 687, + "arPerLvl": 30, + "dmgMin": 18, + "dmgMax": 20, + "dmgPerLvl": 6, + "resist": 83, + "resistPerLvl": 5, + "hireDesc": 1, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 7 + ] + }, + { + "version": 0, + "subTypeId": 3, + "classId": 271, + "act": 1, + "difficulty": 2, + "seller": 150, + "hireCost": 7500, + "level": 25, + "expPerLvl": 115, + "hp": 199, + "hpPerLvl": 12, + "defense": 132, + "defPerLvl": 12, + "str": 60, + "strPerLvl": 10, + "dex": 84, + "dexPerLvl": 16, + "ar": 207, + "arPerLvl": 20, + "dmgMin": 6, + "dmgMax": 8, + "dmgPerLvl": 4, + "resist": 41, + "resistPerLvl": 7, + "hireDesc": 2, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 11 + ] + }, + { + "version": 0, + "subTypeId": 3, + "classId": 271, + "act": 1, + "difficulty": 2, + "seller": 150, + "hireCost": 7500, + "level": 49, + "expPerLvl": 115, + "hp": 487, + "hpPerLvl": 16, + "defense": 420, + "defPerLvl": 18, + "str": 90, + "strPerLvl": 10, + "dex": 132, + "dexPerLvl": 16, + "ar": 687, + "arPerLvl": 30, + "dmgMin": 18, + "dmgMax": 20, + "dmgPerLvl": 6, + "resist": 83, + "resistPerLvl": 5, + "hireDesc": 2, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 11 + ] + }, + { + "version": 0, + "subTypeId": 4, + "classId": 271, + "act": 1, + "difficulty": 3, + "seller": 150, + "hireCost": 12500, + "level": 49, + "expPerLvl": 120, + "hp": 438, + "hpPerLvl": 16, + "defense": 378, + "defPerLvl": 18, + "str": 87, + "strPerLvl": 10, + "dex": 127, + "dexPerLvl": 16, + "ar": 618, + "arPerLvl": 30, + "dmgMin": 17, + "dmgMax": 19, + "dmgPerLvl": 6, + "resist": 80, + "resistPerLvl": 5, + "hireDesc": 1, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 7 + ] + }, + { + "version": 0, + "subTypeId": 5, + "classId": 271, + "act": 1, + "difficulty": 3, + "seller": 150, + "hireCost": 14000, + "level": 49, + "expPerLvl": 125, + "hp": 438, + "hpPerLvl": 16, + "defense": 378, + "defPerLvl": 18, + "str": 87, + "strPerLvl": 10, + "dex": 127, + "dexPerLvl": 16, + "ar": 618, + "arPerLvl": 30, + "dmgMin": 17, + "dmgMax": 19, + "dmgPerLvl": 6, + "resist": 80, + "resistPerLvl": 5, + "hireDesc": 2, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 11 + ] + }, + { + "version": 100, + "subTypeId": 0, + "classId": 271, + "act": 1, + "difficulty": 1, + "seller": 150, + "hireCost": 100, + "level": 3, + "expPerLvl": 100, + "hp": 45, + "hpPerLvl": 9, + "defense": 15, + "defPerLvl": 8, + "str": 35, + "strPerLvl": 10, + "dex": 45, + "dexPerLvl": 16, + "ar": 10, + "arPerLvl": 12, + "dmgMin": 1, + "dmgMax": 3, + "dmgPerLvl": 2, + "resist": 0, + "resistPerLvl": 8, + "hireDesc": 1, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 7 + ] + }, + { + "version": 100, + "subTypeId": 0, + "classId": 271, + "act": 1, + "difficulty": 1, + "seller": 150, + "hireCost": 100, + "level": 36, + "expPerLvl": 100, + "hp": 342, + "hpPerLvl": 18, + "defense": 279, + "defPerLvl": 15, + "str": 77, + "strPerLvl": 10, + "dex": 111, + "dexPerLvl": 16, + "ar": 406, + "arPerLvl": 24, + "dmgMin": 9, + "dmgMax": 11, + "dmgPerLvl": 4, + "resist": 66, + "resistPerLvl": 7, + "hireDesc": 1, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 7 + ] + }, + { + "version": 100, + "subTypeId": 0, + "classId": 271, + "act": 1, + "difficulty": 1, + "seller": 150, + "hireCost": 100, + "level": 67, + "expPerLvl": 100, + "hp": 900, + "hpPerLvl": 30, + "defense": 744, + "defPerLvl": 22, + "str": 116, + "strPerLvl": 10, + "dex": 173, + "dexPerLvl": 16, + "ar": 1150, + "arPerLvl": 36, + "dmgMin": 25, + "dmgMax": 27, + "dmgPerLvl": 6, + "resist": 121, + "resistPerLvl": 5, + "hireDesc": 1, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 7 + ] + }, + { + "version": 100, + "subTypeId": 1, + "classId": 271, + "act": 1, + "difficulty": 1, + "seller": 150, + "hireCost": 150, + "level": 3, + "expPerLvl": 105, + "hp": 45, + "hpPerLvl": 9, + "defense": 15, + "defPerLvl": 8, + "str": 35, + "strPerLvl": 10, + "dex": 45, + "dexPerLvl": 16, + "ar": 10, + "arPerLvl": 12, + "dmgMin": 1, + "dmgMax": 3, + "dmgPerLvl": 2, + "resist": 0, + "resistPerLvl": 8, + "hireDesc": 2, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 11 + ] + }, + { + "version": 100, + "subTypeId": 1, + "classId": 271, + "act": 1, + "difficulty": 1, + "seller": 150, + "hireCost": 150, + "level": 36, + "expPerLvl": 105, + "hp": 342, + "hpPerLvl": 18, + "defense": 279, + "defPerLvl": 15, + "str": 77, + "strPerLvl": 10, + "dex": 111, + "dexPerLvl": 16, + "ar": 406, + "arPerLvl": 24, + "dmgMin": 9, + "dmgMax": 11, + "dmgPerLvl": 4, + "resist": 66, + "resistPerLvl": 7, + "hireDesc": 2, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 11 + ] + }, + { + "version": 100, + "subTypeId": 1, + "classId": 271, + "act": 1, + "difficulty": 1, + "seller": 150, + "hireCost": 150, + "level": 67, + "expPerLvl": 105, + "hp": 900, + "hpPerLvl": 30, + "defense": 744, + "defPerLvl": 22, + "str": 116, + "strPerLvl": 10, + "dex": 173, + "dexPerLvl": 16, + "ar": 1150, + "arPerLvl": 36, + "dmgMin": 25, + "dmgMax": 27, + "dmgPerLvl": 6, + "resist": 121, + "resistPerLvl": 5, + "hireDesc": 2, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 11 + ] + }, + { + "version": 100, + "subTypeId": 2, + "classId": 271, + "act": 1, + "difficulty": 2, + "seller": 150, + "hireCost": 6000, + "level": 36, + "expPerLvl": 110, + "hp": 308, + "hpPerLvl": 18, + "defense": 251, + "defPerLvl": 15, + "str": 74, + "strPerLvl": 10, + "dex": 106, + "dexPerLvl": 16, + "ar": 365, + "arPerLvl": 24, + "dmgMin": 8, + "dmgMax": 10, + "dmgPerLvl": 4, + "resist": 63, + "resistPerLvl": 7, + "hireDesc": 1, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 7 + ] + }, + { + "version": 100, + "subTypeId": 2, + "classId": 271, + "act": 1, + "difficulty": 2, + "seller": 150, + "hireCost": 6000, + "level": 67, + "expPerLvl": 110, + "hp": 866, + "hpPerLvl": 30, + "defense": 716, + "defPerLvl": 22, + "str": 113, + "strPerLvl": 10, + "dex": 168, + "dexPerLvl": 16, + "ar": 1109, + "arPerLvl": 36, + "dmgMin": 24, + "dmgMax": 26, + "dmgPerLvl": 6, + "resist": 118, + "resistPerLvl": 5, + "hireDesc": 1, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 7 + ] + }, + { + "version": 100, + "subTypeId": 3, + "classId": 271, + "act": 1, + "difficulty": 2, + "seller": 150, + "hireCost": 7500, + "level": 36, + "expPerLvl": 115, + "hp": 308, + "hpPerLvl": 18, + "defense": 251, + "defPerLvl": 15, + "str": 74, + "strPerLvl": 10, + "dex": 106, + "dexPerLvl": 16, + "ar": 365, + "arPerLvl": 24, + "dmgMin": 8, + "dmgMax": 10, + "dmgPerLvl": 4, + "resist": 63, + "resistPerLvl": 7, + "hireDesc": 2, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 11 + ] + }, + { + "version": 100, + "subTypeId": 3, + "classId": 271, + "act": 1, + "difficulty": 2, + "seller": 150, + "hireCost": 7500, + "level": 67, + "expPerLvl": 115, + "hp": 866, + "hpPerLvl": 30, + "defense": 716, + "defPerLvl": 22, + "str": 113, + "strPerLvl": 10, + "dex": 168, + "dexPerLvl": 16, + "ar": 1109, + "arPerLvl": 36, + "dmgMin": 24, + "dmgMax": 26, + "dmgPerLvl": 6, + "resist": 118, + "resistPerLvl": 5, + "hireDesc": 2, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 11 + ] + }, + { + "version": 100, + "subTypeId": 4, + "classId": 271, + "act": 1, + "difficulty": 3, + "seller": 150, + "hireCost": 12500, + "level": 67, + "expPerLvl": 120, + "hp": 779, + "hpPerLvl": 30, + "defense": 644, + "defPerLvl": 22, + "str": 110, + "strPerLvl": 10, + "dex": 163, + "dexPerLvl": 16, + "ar": 998, + "arPerLvl": 36, + "dmgMin": 23, + "dmgMax": 25, + "dmgPerLvl": 6, + "resist": 115, + "resistPerLvl": 5, + "hireDesc": 1, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 7 + ] + }, + { + "version": 100, + "subTypeId": 5, + "classId": 271, + "act": 1, + "difficulty": 3, + "seller": 150, + "hireCost": 14000, + "level": 67, + "expPerLvl": 125, + "hp": 779, + "hpPerLvl": 30, + "defense": 644, + "defPerLvl": 22, + "str": 110, + "strPerLvl": 10, + "dex": 163, + "dexPerLvl": 16, + "ar": 998, + "arPerLvl": 36, + "dmgMin": 23, + "dmgMax": 25, + "dmgPerLvl": 6, + "resist": 115, + "resistPerLvl": 5, + "hireDesc": 2, + "nameFirstKey": "merc01", + "nameLastKey": "merc41", + "wNameFirstId": 3411, + "wNameLastId": 3451, + "skills": [ + 8, + 11 + ] + }, + { + "version": 0, + "subTypeId": 6, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 9, + "expPerLvl": 110, + "hp": 120, + "hpPerLvl": 10, + "defense": 45, + "defPerLvl": 9, + "str": 57, + "strPerLvl": 14, + "dex": 40, + "dexPerLvl": 12, + "ar": 20, + "arPerLvl": 10, + "dmgMin": 7, + "dmgMax": 14, + "dmgPerLvl": 4, + "resist": 18, + "resistPerLvl": 8, + "hireDesc": 3, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 99 + ] + }, + { + "version": 0, + "subTypeId": 6, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 31, + "expPerLvl": 110, + "hp": 340, + "hpPerLvl": 20, + "defense": 243, + "defPerLvl": 16, + "str": 96, + "strPerLvl": 14, + "dex": 73, + "dexPerLvl": 12, + "ar": 240, + "arPerLvl": 20, + "dmgMin": 18, + "dmgMax": 25, + "dmgPerLvl": 6, + "resist": 62, + "resistPerLvl": 7, + "hireDesc": 3, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 99 + ] + }, + { + "version": 0, + "subTypeId": 6, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 55, + "expPerLvl": 110, + "hp": 820, + "hpPerLvl": 35, + "defense": 627, + "defPerLvl": 24, + "str": 138, + "strPerLvl": 14, + "dex": 109, + "dexPerLvl": 12, + "ar": 720, + "arPerLvl": 30, + "dmgMin": 36, + "dmgMax": 43, + "dmgPerLvl": 8, + "resist": 104, + "resistPerLvl": 4, + "hireDesc": 3, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 99 + ] + }, + { + "version": 0, + "subTypeId": 7, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 9, + "expPerLvl": 110, + "hp": 120, + "hpPerLvl": 10, + "defense": 45, + "defPerLvl": 9, + "str": 57, + "strPerLvl": 14, + "dex": 40, + "dexPerLvl": 12, + "ar": 20, + "arPerLvl": 10, + "dmgMin": 7, + "dmgMax": 14, + "dmgPerLvl": 4, + "resist": 18, + "resistPerLvl": 8, + "hireDesc": 4, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 104 + ] + }, + { + "version": 0, + "subTypeId": 7, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 31, + "expPerLvl": 110, + "hp": 340, + "hpPerLvl": 20, + "defense": 243, + "defPerLvl": 16, + "str": 96, + "strPerLvl": 14, + "dex": 73, + "dexPerLvl": 12, + "ar": 240, + "arPerLvl": 20, + "dmgMin": 18, + "dmgMax": 25, + "dmgPerLvl": 6, + "resist": 62, + "resistPerLvl": 7, + "hireDesc": 4, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 104 + ] + }, + { + "version": 0, + "subTypeId": 7, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 55, + "expPerLvl": 110, + "hp": 820, + "hpPerLvl": 35, + "defense": 627, + "defPerLvl": 24, + "str": 138, + "strPerLvl": 14, + "dex": 109, + "dexPerLvl": 12, + "ar": 720, + "arPerLvl": 30, + "dmgMin": 36, + "dmgMax": 43, + "dmgPerLvl": 8, + "resist": 104, + "resistPerLvl": 4, + "hireDesc": 4, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 104 + ] + }, + { + "version": 0, + "subTypeId": 8, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 9, + "expPerLvl": 110, + "hp": 120, + "hpPerLvl": 10, + "defense": 45, + "defPerLvl": 9, + "str": 57, + "strPerLvl": 14, + "dex": 40, + "dexPerLvl": 12, + "ar": 20, + "arPerLvl": 10, + "dmgMin": 7, + "dmgMax": 14, + "dmgPerLvl": 4, + "resist": 18, + "resistPerLvl": 8, + "hireDesc": 5, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 108 + ] + }, + { + "version": 0, + "subTypeId": 8, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 31, + "expPerLvl": 110, + "hp": 340, + "hpPerLvl": 20, + "defense": 243, + "defPerLvl": 16, + "str": 96, + "strPerLvl": 14, + "dex": 73, + "dexPerLvl": 12, + "ar": 240, + "arPerLvl": 20, + "dmgMin": 18, + "dmgMax": 25, + "dmgPerLvl": 6, + "resist": 62, + "resistPerLvl": 7, + "hireDesc": 5, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 108 + ] + }, + { + "version": 0, + "subTypeId": 8, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 55, + "expPerLvl": 110, + "hp": 820, + "hpPerLvl": 35, + "defense": 627, + "defPerLvl": 24, + "str": 138, + "strPerLvl": 14, + "dex": 109, + "dexPerLvl": 12, + "ar": 720, + "arPerLvl": 30, + "dmgMin": 36, + "dmgMax": 43, + "dmgPerLvl": 8, + "resist": 104, + "resistPerLvl": 4, + "hireDesc": 5, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 108 + ] + }, + { + "version": 0, + "subTypeId": 9, + "classId": 338, + "act": 2, + "difficulty": 2, + "seller": 198, + "hireCost": 7900, + "level": 31, + "expPerLvl": 120, + "hp": 306, + "hpPerLvl": 20, + "defense": 219, + "defPerLvl": 16, + "str": 92, + "strPerLvl": 14, + "dex": 69, + "dexPerLvl": 12, + "ar": 216, + "arPerLvl": 20, + "dmgMin": 16, + "dmgMax": 23, + "dmgPerLvl": 6, + "resist": 59, + "resistPerLvl": 7, + "hireDesc": 3, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 103 + ] + }, + { + "version": 0, + "subTypeId": 9, + "classId": 338, + "act": 2, + "difficulty": 2, + "seller": 198, + "hireCost": 7900, + "level": 55, + "expPerLvl": 120, + "hp": 786, + "hpPerLvl": 35, + "defense": 603, + "defPerLvl": 24, + "str": 134, + "strPerLvl": 14, + "dex": 105, + "dexPerLvl": 12, + "ar": 696, + "arPerLvl": 30, + "dmgMin": 34, + "dmgMax": 41, + "dmgPerLvl": 8, + "resist": 101, + "resistPerLvl": 4, + "hireDesc": 3, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 103 + ] + }, + { + "version": 0, + "subTypeId": 10, + "classId": 338, + "act": 2, + "difficulty": 2, + "seller": 198, + "hireCost": 7900, + "level": 31, + "expPerLvl": 120, + "hp": 306, + "hpPerLvl": 20, + "defense": 219, + "defPerLvl": 16, + "str": 92, + "strPerLvl": 14, + "dex": 69, + "dexPerLvl": 12, + "ar": 216, + "arPerLvl": 20, + "dmgMin": 16, + "dmgMax": 23, + "dmgPerLvl": 6, + "resist": 59, + "resistPerLvl": 7, + "hireDesc": 4, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 114 + ] + }, + { + "version": 0, + "subTypeId": 10, + "classId": 338, + "act": 2, + "difficulty": 2, + "seller": 198, + "hireCost": 7900, + "level": 55, + "expPerLvl": 120, + "hp": 786, + "hpPerLvl": 35, + "defense": 603, + "defPerLvl": 24, + "str": 134, + "strPerLvl": 14, + "dex": 105, + "dexPerLvl": 12, + "ar": 696, + "arPerLvl": 30, + "dmgMin": 34, + "dmgMax": 41, + "dmgPerLvl": 8, + "resist": 101, + "resistPerLvl": 4, + "hireDesc": 4, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 114 + ] + }, + { + "version": 0, + "subTypeId": 11, + "classId": 338, + "act": 2, + "difficulty": 2, + "seller": 198, + "hireCost": 7900, + "level": 31, + "expPerLvl": 120, + "hp": 306, + "hpPerLvl": 20, + "defense": 219, + "defPerLvl": 16, + "str": 92, + "strPerLvl": 14, + "dex": 69, + "dexPerLvl": 12, + "ar": 216, + "arPerLvl": 20, + "dmgMin": 16, + "dmgMax": 23, + "dmgPerLvl": 6, + "resist": 59, + "resistPerLvl": 7, + "hireDesc": 5, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 98 + ] + }, + { + "version": 0, + "subTypeId": 11, + "classId": 338, + "act": 2, + "difficulty": 2, + "seller": 198, + "hireCost": 7900, + "level": 55, + "expPerLvl": 120, + "hp": 786, + "hpPerLvl": 35, + "defense": 603, + "defPerLvl": 24, + "str": 134, + "strPerLvl": 14, + "dex": 105, + "dexPerLvl": 12, + "ar": 696, + "arPerLvl": 30, + "dmgMin": 34, + "dmgMax": 41, + "dmgPerLvl": 8, + "resist": 101, + "resistPerLvl": 4, + "hireDesc": 5, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 98 + ] + }, + { + "version": 0, + "subTypeId": 12, + "classId": 338, + "act": 2, + "difficulty": 3, + "seller": 198, + "hireCost": 15000, + "level": 55, + "expPerLvl": 130, + "hp": 707, + "hpPerLvl": 35, + "defense": 543, + "defPerLvl": 24, + "str": 130, + "strPerLvl": 14, + "dex": 101, + "dexPerLvl": 12, + "ar": 626, + "arPerLvl": 30, + "dmgMin": 32, + "dmgMax": 39, + "dmgPerLvl": 8, + "resist": 98, + "resistPerLvl": 4, + "hireDesc": 3, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 99 + ] + }, + { + "version": 0, + "subTypeId": 13, + "classId": 338, + "act": 2, + "difficulty": 3, + "seller": 198, + "hireCost": 15000, + "level": 55, + "expPerLvl": 130, + "hp": 707, + "hpPerLvl": 35, + "defense": 543, + "defPerLvl": 24, + "str": 130, + "strPerLvl": 14, + "dex": 101, + "dexPerLvl": 12, + "ar": 626, + "arPerLvl": 30, + "dmgMin": 32, + "dmgMax": 39, + "dmgPerLvl": 8, + "resist": 98, + "resistPerLvl": 4, + "hireDesc": 4, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 104 + ] + }, + { + "version": 0, + "subTypeId": 14, + "classId": 338, + "act": 2, + "difficulty": 3, + "seller": 198, + "hireCost": 15000, + "level": 55, + "expPerLvl": 130, + "hp": 707, + "hpPerLvl": 35, + "defense": 543, + "defPerLvl": 24, + "str": 130, + "strPerLvl": 14, + "dex": 101, + "dexPerLvl": 12, + "ar": 626, + "arPerLvl": 30, + "dmgMin": 32, + "dmgMax": 39, + "dmgPerLvl": 8, + "resist": 98, + "resistPerLvl": 4, + "hireDesc": 5, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 108 + ] + }, + { + "version": 100, + "subTypeId": 6, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 9, + "expPerLvl": 110, + "hp": 120, + "hpPerLvl": 15, + "defense": 45, + "defPerLvl": 11, + "str": 57, + "strPerLvl": 14, + "dex": 40, + "dexPerLvl": 12, + "ar": 20, + "arPerLvl": 12, + "dmgMin": 7, + "dmgMax": 14, + "dmgPerLvl": 4, + "resist": 18, + "resistPerLvl": 8, + "hireDesc": 3, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 99 + ] + }, + { + "version": 100, + "subTypeId": 6, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 43, + "expPerLvl": 110, + "hp": 630, + "hpPerLvl": 25, + "defense": 419, + "defPerLvl": 19, + "str": 117, + "strPerLvl": 14, + "dex": 91, + "dexPerLvl": 12, + "ar": 428, + "arPerLvl": 24, + "dmgMin": 24, + "dmgMax": 31, + "dmgPerLvl": 6, + "resist": 86, + "resistPerLvl": 7, + "hireDesc": 3, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 99 + ] + }, + { + "version": 100, + "subTypeId": 6, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 75, + "expPerLvl": 110, + "hp": 1430, + "hpPerLvl": 40, + "defense": 1027, + "defPerLvl": 28, + "str": 173, + "strPerLvl": 14, + "dex": 139, + "dexPerLvl": 12, + "ar": 1196, + "arPerLvl": 36, + "dmgMin": 48, + "dmgMax": 55, + "dmgPerLvl": 8, + "resist": 142, + "resistPerLvl": 4, + "hireDesc": 3, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 99 + ] + }, + { + "version": 100, + "subTypeId": 7, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 9, + "expPerLvl": 110, + "hp": 120, + "hpPerLvl": 15, + "defense": 45, + "defPerLvl": 11, + "str": 57, + "strPerLvl": 14, + "dex": 40, + "dexPerLvl": 12, + "ar": 20, + "arPerLvl": 12, + "dmgMin": 7, + "dmgMax": 14, + "dmgPerLvl": 4, + "resist": 18, + "resistPerLvl": 8, + "hireDesc": 4, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 104 + ] + }, + { + "version": 100, + "subTypeId": 7, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 43, + "expPerLvl": 110, + "hp": 630, + "hpPerLvl": 25, + "defense": 419, + "defPerLvl": 19, + "str": 117, + "strPerLvl": 14, + "dex": 91, + "dexPerLvl": 12, + "ar": 428, + "arPerLvl": 24, + "dmgMin": 24, + "dmgMax": 31, + "dmgPerLvl": 6, + "resist": 86, + "resistPerLvl": 7, + "hireDesc": 4, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 104 + ] + }, + { + "version": 100, + "subTypeId": 7, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 75, + "expPerLvl": 110, + "hp": 1430, + "hpPerLvl": 40, + "defense": 1027, + "defPerLvl": 28, + "str": 173, + "strPerLvl": 14, + "dex": 139, + "dexPerLvl": 12, + "ar": 1196, + "arPerLvl": 36, + "dmgMin": 48, + "dmgMax": 55, + "dmgPerLvl": 8, + "resist": 142, + "resistPerLvl": 4, + "hireDesc": 4, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 104 + ] + }, + { + "version": 100, + "subTypeId": 8, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 9, + "expPerLvl": 110, + "hp": 120, + "hpPerLvl": 15, + "defense": 45, + "defPerLvl": 11, + "str": 57, + "strPerLvl": 14, + "dex": 40, + "dexPerLvl": 12, + "ar": 20, + "arPerLvl": 12, + "dmgMin": 7, + "dmgMax": 14, + "dmgPerLvl": 4, + "resist": 18, + "resistPerLvl": 8, + "hireDesc": 5, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 108 + ] + }, + { + "version": 100, + "subTypeId": 8, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 43, + "expPerLvl": 110, + "hp": 630, + "hpPerLvl": 25, + "defense": 419, + "defPerLvl": 19, + "str": 117, + "strPerLvl": 14, + "dex": 91, + "dexPerLvl": 12, + "ar": 428, + "arPerLvl": 24, + "dmgMin": 24, + "dmgMax": 31, + "dmgPerLvl": 6, + "resist": 86, + "resistPerLvl": 7, + "hireDesc": 5, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 108 + ] + }, + { + "version": 100, + "subTypeId": 8, + "classId": 338, + "act": 2, + "difficulty": 1, + "seller": 198, + "hireCost": 350, + "level": 75, + "expPerLvl": 110, + "hp": 1430, + "hpPerLvl": 40, + "defense": 1027, + "defPerLvl": 28, + "str": 173, + "strPerLvl": 14, + "dex": 139, + "dexPerLvl": 12, + "ar": 1196, + "arPerLvl": 36, + "dmgMin": 48, + "dmgMax": 55, + "dmgPerLvl": 8, + "resist": 142, + "resistPerLvl": 4, + "hireDesc": 5, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 108 + ] + }, + { + "version": 100, + "subTypeId": 9, + "classId": 338, + "act": 2, + "difficulty": 2, + "seller": 198, + "hireCost": 7900, + "level": 43, + "expPerLvl": 120, + "hp": 567, + "hpPerLvl": 25, + "defense": 377, + "defPerLvl": 19, + "str": 113, + "strPerLvl": 14, + "dex": 87, + "dexPerLvl": 12, + "ar": 385, + "arPerLvl": 24, + "dmgMin": 22, + "dmgMax": 29, + "dmgPerLvl": 6, + "resist": 83, + "resistPerLvl": 7, + "hireDesc": 3, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 103 + ] + }, + { + "version": 100, + "subTypeId": 9, + "classId": 338, + "act": 2, + "difficulty": 2, + "seller": 198, + "hireCost": 7900, + "level": 75, + "expPerLvl": 120, + "hp": 1367, + "hpPerLvl": 40, + "defense": 985, + "defPerLvl": 28, + "str": 169, + "strPerLvl": 14, + "dex": 135, + "dexPerLvl": 12, + "ar": 1153, + "arPerLvl": 36, + "dmgMin": 46, + "dmgMax": 53, + "dmgPerLvl": 8, + "resist": 139, + "resistPerLvl": 4, + "hireDesc": 3, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 103 + ] + }, + { + "version": 100, + "subTypeId": 10, + "classId": 338, + "act": 2, + "difficulty": 2, + "seller": 198, + "hireCost": 7900, + "level": 43, + "expPerLvl": 120, + "hp": 567, + "hpPerLvl": 25, + "defense": 377, + "defPerLvl": 19, + "str": 113, + "strPerLvl": 14, + "dex": 87, + "dexPerLvl": 12, + "ar": 385, + "arPerLvl": 24, + "dmgMin": 22, + "dmgMax": 29, + "dmgPerLvl": 6, + "resist": 83, + "resistPerLvl": 7, + "hireDesc": 4, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 114 + ] + }, + { + "version": 100, + "subTypeId": 10, + "classId": 338, + "act": 2, + "difficulty": 2, + "seller": 198, + "hireCost": 7900, + "level": 75, + "expPerLvl": 120, + "hp": 1367, + "hpPerLvl": 40, + "defense": 985, + "defPerLvl": 28, + "str": 169, + "strPerLvl": 14, + "dex": 135, + "dexPerLvl": 12, + "ar": 1153, + "arPerLvl": 36, + "dmgMin": 46, + "dmgMax": 53, + "dmgPerLvl": 8, + "resist": 139, + "resistPerLvl": 4, + "hireDesc": 4, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 114 + ] + }, + { + "version": 100, + "subTypeId": 11, + "classId": 338, + "act": 2, + "difficulty": 2, + "seller": 198, + "hireCost": 7900, + "level": 43, + "expPerLvl": 120, + "hp": 567, + "hpPerLvl": 25, + "defense": 377, + "defPerLvl": 19, + "str": 113, + "strPerLvl": 14, + "dex": 87, + "dexPerLvl": 12, + "ar": 385, + "arPerLvl": 24, + "dmgMin": 22, + "dmgMax": 29, + "dmgPerLvl": 6, + "resist": 83, + "resistPerLvl": 7, + "hireDesc": 5, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 98 + ] + }, + { + "version": 100, + "subTypeId": 11, + "classId": 338, + "act": 2, + "difficulty": 2, + "seller": 198, + "hireCost": 7900, + "level": 75, + "expPerLvl": 120, + "hp": 1367, + "hpPerLvl": 40, + "defense": 985, + "defPerLvl": 28, + "str": 169, + "strPerLvl": 14, + "dex": 135, + "dexPerLvl": 12, + "ar": 1153, + "arPerLvl": 36, + "dmgMin": 46, + "dmgMax": 53, + "dmgPerLvl": 8, + "resist": 139, + "resistPerLvl": 4, + "hireDesc": 5, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 98 + ] + }, + { + "version": 100, + "subTypeId": 12, + "classId": 338, + "act": 2, + "difficulty": 3, + "seller": 198, + "hireCost": 15000, + "level": 75, + "expPerLvl": 130, + "hp": 1230, + "hpPerLvl": 40, + "defense": 887, + "defPerLvl": 28, + "str": 165, + "strPerLvl": 14, + "dex": 131, + "dexPerLvl": 12, + "ar": 1038, + "arPerLvl": 36, + "dmgMin": 44, + "dmgMax": 51, + "dmgPerLvl": 8, + "resist": 136, + "resistPerLvl": 4, + "hireDesc": 3, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 99 + ] + }, + { + "version": 100, + "subTypeId": 13, + "classId": 338, + "act": 2, + "difficulty": 3, + "seller": 198, + "hireCost": 15000, + "level": 75, + "expPerLvl": 130, + "hp": 1230, + "hpPerLvl": 40, + "defense": 887, + "defPerLvl": 28, + "str": 165, + "strPerLvl": 14, + "dex": 131, + "dexPerLvl": 12, + "ar": 1038, + "arPerLvl": 36, + "dmgMin": 44, + "dmgMax": 51, + "dmgPerLvl": 8, + "resist": 136, + "resistPerLvl": 4, + "hireDesc": 4, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 104 + ] + }, + { + "version": 100, + "subTypeId": 14, + "classId": 338, + "act": 2, + "difficulty": 3, + "seller": 198, + "hireCost": 15000, + "level": 75, + "expPerLvl": 130, + "hp": 1230, + "hpPerLvl": 40, + "defense": 887, + "defPerLvl": 28, + "str": 165, + "strPerLvl": 14, + "dex": 131, + "dexPerLvl": 12, + "ar": 1038, + "arPerLvl": 36, + "dmgMin": 44, + "dmgMax": 51, + "dmgPerLvl": 8, + "resist": 136, + "resistPerLvl": 4, + "hireDesc": 5, + "nameFirstKey": "merca201", + "nameLastKey": "merca221", + "wNameFirstId": 1019, + "wNameLastId": 1039, + "skills": [ + 10, + 108 + ] + }, + { + "version": 0, + "subTypeId": 15, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1000, + "level": 15, + "expPerLvl": 110, + "hp": 160, + "hpPerLvl": 8, + "defense": 80, + "defPerLvl": 4, + "str": 49, + "strPerLvl": 10, + "dex": 40, + "dexPerLvl": 8, + "ar": 15, + "arPerLvl": 10, + "dmgMin": 1, + "dmgMax": 7, + "dmgPerLvl": 4, + "resist": 25, + "resistPerLvl": 7, + "hireDesc": 6, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 41, + 47 + ] + }, + { + "version": 0, + "subTypeId": 15, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1000, + "level": 37, + "expPerLvl": 110, + "hp": 336, + "hpPerLvl": 14, + "defense": 168, + "defPerLvl": 10, + "str": 77, + "strPerLvl": 10, + "dex": 62, + "dexPerLvl": 8, + "ar": 235, + "arPerLvl": 20, + "dmgMin": 12, + "dmgMax": 18, + "dmgPerLvl": 4, + "resist": 64, + "resistPerLvl": 7, + "hireDesc": 6, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 41, + 47 + ] + }, + { + "version": 0, + "subTypeId": 15, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1000, + "level": 61, + "expPerLvl": 110, + "hp": 672, + "hpPerLvl": 20, + "defense": 408, + "defPerLvl": 18, + "str": 107, + "strPerLvl": 10, + "dex": 86, + "dexPerLvl": 8, + "ar": 715, + "arPerLvl": 30, + "dmgMin": 24, + "dmgMax": 30, + "dmgPerLvl": 4, + "resist": 106, + "resistPerLvl": 6, + "hireDesc": 6, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 41, + 47 + ] + }, + { + "version": 0, + "subTypeId": 16, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1500, + "level": 15, + "expPerLvl": 120, + "hp": 160, + "hpPerLvl": 8, + "defense": 80, + "defPerLvl": 4, + "str": 49, + "strPerLvl": 10, + "dex": 40, + "dexPerLvl": 8, + "ar": 15, + "arPerLvl": 10, + "dmgMin": 1, + "dmgMax": 7, + "dmgPerLvl": 4, + "resist": 25, + "resistPerLvl": 7, + "hireDesc": 7, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 55, + 40, + 45 + ] + }, + { + "version": 0, + "subTypeId": 16, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1500, + "level": 37, + "expPerLvl": 120, + "hp": 336, + "hpPerLvl": 14, + "defense": 168, + "defPerLvl": 10, + "str": 77, + "strPerLvl": 10, + "dex": 62, + "dexPerLvl": 8, + "ar": 235, + "arPerLvl": 20, + "dmgMin": 12, + "dmgMax": 18, + "dmgPerLvl": 4, + "resist": 64, + "resistPerLvl": 7, + "hireDesc": 7, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 55, + 40, + 45 + ] + }, + { + "version": 0, + "subTypeId": 16, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1500, + "level": 61, + "expPerLvl": 120, + "hp": 672, + "hpPerLvl": 20, + "defense": 408, + "defPerLvl": 16, + "str": 107, + "strPerLvl": 10, + "dex": 86, + "dexPerLvl": 8, + "ar": 715, + "arPerLvl": 30, + "dmgMin": 24, + "dmgMax": 30, + "dmgPerLvl": 4, + "resist": 106, + "resistPerLvl": 4, + "hireDesc": 7, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 55, + 40, + 45 + ] + }, + { + "version": 0, + "subTypeId": 17, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1000, + "level": 15, + "expPerLvl": 110, + "hp": 160, + "hpPerLvl": 8, + "defense": 80, + "defPerLvl": 4, + "str": 49, + "strPerLvl": 10, + "dex": 40, + "dexPerLvl": 8, + "ar": 15, + "arPerLvl": 10, + "dmgMin": 1, + "dmgMax": 7, + "dmgPerLvl": 4, + "resist": 25, + "resistPerLvl": 7, + "hireDesc": 8, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 38, + 49 + ] + }, + { + "version": 0, + "subTypeId": 17, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1000, + "level": 37, + "expPerLvl": 110, + "hp": 336, + "hpPerLvl": 14, + "defense": 168, + "defPerLvl": 10, + "str": 77, + "strPerLvl": 10, + "dex": 62, + "dexPerLvl": 8, + "ar": 235, + "arPerLvl": 20, + "dmgMin": 12, + "dmgMax": 18, + "dmgPerLvl": 4, + "resist": 64, + "resistPerLvl": 7, + "hireDesc": 8, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 38, + 49 + ] + }, + { + "version": 0, + "subTypeId": 17, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1000, + "level": 61, + "expPerLvl": 110, + "hp": 672, + "hpPerLvl": 20, + "defense": 408, + "defPerLvl": 16, + "str": 107, + "strPerLvl": 10, + "dex": 86, + "dexPerLvl": 8, + "ar": 715, + "arPerLvl": 30, + "dmgMin": 24, + "dmgMax": 30, + "dmgPerLvl": 4, + "resist": 106, + "resistPerLvl": 4, + "hireDesc": 8, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 38, + 49 + ] + }, + { + "version": 0, + "subTypeId": 18, + "classId": 359, + "act": 3, + "difficulty": 2, + "seller": 252, + "hireCost": 9500, + "level": 37, + "expPerLvl": 120, + "hp": 302, + "hpPerLvl": 14, + "defense": 151, + "defPerLvl": 10, + "str": 73, + "strPerLvl": 10, + "dex": 58, + "dexPerLvl": 8, + "ar": 212, + "arPerLvl": 20, + "dmgMin": 10, + "dmgMax": 16, + "dmgPerLvl": 4, + "resist": 61, + "resistPerLvl": 7, + "hireDesc": 6, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 41, + 47 + ] + }, + { + "version": 0, + "subTypeId": 18, + "classId": 359, + "act": 3, + "difficulty": 2, + "seller": 252, + "hireCost": 9500, + "level": 61, + "expPerLvl": 120, + "hp": 638, + "hpPerLvl": 20, + "defense": 391, + "defPerLvl": 16, + "str": 103, + "strPerLvl": 10, + "dex": 82, + "dexPerLvl": 8, + "ar": 692, + "arPerLvl": 30, + "dmgMin": 22, + "dmgMax": 28, + "dmgPerLvl": 4, + "resist": 103, + "resistPerLvl": 4, + "hireDesc": 6, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 41, + 47 + ] + }, + { + "version": 0, + "subTypeId": 19, + "classId": 359, + "act": 3, + "difficulty": 2, + "seller": 252, + "hireCost": 12000, + "level": 37, + "expPerLvl": 130, + "hp": 302, + "hpPerLvl": 14, + "defense": 151, + "defPerLvl": 10, + "str": 73, + "strPerLvl": 10, + "dex": 58, + "dexPerLvl": 8, + "ar": 212, + "arPerLvl": 20, + "dmgMin": 10, + "dmgMax": 16, + "dmgPerLvl": 4, + "resist": 61, + "resistPerLvl": 7, + "hireDesc": 7, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 55, + 40, + 45 + ] + }, + { + "version": 0, + "subTypeId": 19, + "classId": 359, + "act": 3, + "difficulty": 2, + "seller": 252, + "hireCost": 12000, + "level": 61, + "expPerLvl": 130, + "hp": 638, + "hpPerLvl": 20, + "defense": 391, + "defPerLvl": 16, + "str": 103, + "strPerLvl": 10, + "dex": 82, + "dexPerLvl": 8, + "ar": 692, + "arPerLvl": 30, + "dmgMin": 22, + "dmgMax": 28, + "dmgPerLvl": 4, + "resist": 103, + "resistPerLvl": 4, + "hireDesc": 7, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 55, + 40, + 45 + ] + }, + { + "version": 0, + "subTypeId": 20, + "classId": 359, + "act": 3, + "difficulty": 2, + "seller": 252, + "hireCost": 10000, + "level": 37, + "expPerLvl": 120, + "hp": 302, + "hpPerLvl": 14, + "defense": 151, + "defPerLvl": 10, + "str": 73, + "strPerLvl": 10, + "dex": 58, + "dexPerLvl": 8, + "ar": 212, + "arPerLvl": 20, + "dmgMin": 10, + "dmgMax": 16, + "dmgPerLvl": 4, + "resist": 61, + "resistPerLvl": 7, + "hireDesc": 8, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 38, + 49 + ] + }, + { + "version": 0, + "subTypeId": 20, + "classId": 359, + "act": 3, + "difficulty": 2, + "seller": 252, + "hireCost": 10000, + "level": 61, + "expPerLvl": 120, + "hp": 638, + "hpPerLvl": 20, + "defense": 391, + "defPerLvl": 16, + "str": 103, + "strPerLvl": 10, + "dex": 82, + "dexPerLvl": 8, + "ar": 692, + "arPerLvl": 30, + "dmgMin": 22, + "dmgMax": 28, + "dmgPerLvl": 4, + "resist": 103, + "resistPerLvl": 4, + "hireDesc": 8, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 38, + 49 + ] + }, + { + "version": 0, + "subTypeId": 21, + "classId": 359, + "act": 3, + "difficulty": 3, + "seller": 252, + "hireCost": 21000, + "level": 61, + "expPerLvl": 130, + "hp": 574, + "hpPerLvl": 20, + "defense": 352, + "defPerLvl": 16, + "str": 99, + "strPerLvl": 10, + "dex": 78, + "dexPerLvl": 8, + "ar": 623, + "arPerLvl": 30, + "dmgMin": 20, + "dmgMax": 26, + "dmgPerLvl": 4, + "resist": 100, + "resistPerLvl": 4, + "hireDesc": 6, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 41, + 47 + ] + }, + { + "version": 0, + "subTypeId": 22, + "classId": 359, + "act": 3, + "difficulty": 3, + "seller": 252, + "hireCost": 27000, + "level": 61, + "expPerLvl": 140, + "hp": 574, + "hpPerLvl": 20, + "defense": 352, + "defPerLvl": 16, + "str": 99, + "strPerLvl": 10, + "dex": 78, + "dexPerLvl": 8, + "ar": 623, + "arPerLvl": 30, + "dmgMin": 20, + "dmgMax": 26, + "dmgPerLvl": 4, + "resist": 100, + "resistPerLvl": 4, + "hireDesc": 7, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 55, + 40, + 45 + ] + }, + { + "version": 0, + "subTypeId": 23, + "classId": 359, + "act": 3, + "difficulty": 3, + "seller": 252, + "hireCost": 21000, + "level": 61, + "expPerLvl": 130, + "hp": 574, + "hpPerLvl": 20, + "defense": 352, + "defPerLvl": 16, + "str": 99, + "strPerLvl": 10, + "dex": 78, + "dexPerLvl": 8, + "ar": 623, + "arPerLvl": 30, + "dmgMin": 20, + "dmgMax": 27, + "dmgPerLvl": 4, + "resist": 100, + "resistPerLvl": 4, + "hireDesc": 8, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 38, + 49 + ] + }, + { + "version": 100, + "subTypeId": 15, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1000, + "level": 15, + "expPerLvl": 110, + "hp": 160, + "hpPerLvl": 9, + "defense": 80, + "defPerLvl": 5, + "str": 49, + "strPerLvl": 10, + "dex": 40, + "dexPerLvl": 8, + "ar": 15, + "arPerLvl": 12, + "dmgMin": 1, + "dmgMax": 7, + "dmgPerLvl": 4, + "resist": 25, + "resistPerLvl": 7, + "hireDesc": 6, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 41, + 47 + ] + }, + { + "version": 100, + "subTypeId": 15, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1000, + "level": 49, + "expPerLvl": 110, + "hp": 466, + "hpPerLvl": 18, + "defense": 250, + "defPerLvl": 13, + "str": 92, + "strPerLvl": 10, + "dex": 74, + "dexPerLvl": 8, + "ar": 423, + "arPerLvl": 24, + "dmgMin": 18, + "dmgMax": 24, + "dmgPerLvl": 4, + "resist": 85, + "resistPerLvl": 7, + "hireDesc": 6, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 41, + 47 + ] + }, + { + "version": 100, + "subTypeId": 15, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1000, + "level": 79, + "expPerLvl": 110, + "hp": 1006, + "hpPerLvl": 27, + "defense": 640, + "defPerLvl": 25, + "str": 130, + "strPerLvl": 10, + "dex": 104, + "dexPerLvl": 8, + "ar": 1143, + "arPerLvl": 36, + "dmgMin": 33, + "dmgMax": 39, + "dmgPerLvl": 4, + "resist": 138, + "resistPerLvl": 6, + "hireDesc": 6, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 41, + 47 + ] + }, + { + "version": 100, + "subTypeId": 16, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1500, + "level": 15, + "expPerLvl": 120, + "hp": 160, + "hpPerLvl": 9, + "defense": 80, + "defPerLvl": 5, + "str": 49, + "strPerLvl": 10, + "dex": 40, + "dexPerLvl": 8, + "ar": 15, + "arPerLvl": 12, + "dmgMin": 1, + "dmgMax": 7, + "dmgPerLvl": 4, + "resist": 25, + "resistPerLvl": 7, + "hireDesc": 7, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 55, + 40, + 45 + ] + }, + { + "version": 100, + "subTypeId": 16, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1500, + "level": 49, + "expPerLvl": 120, + "hp": 466, + "hpPerLvl": 18, + "defense": 250, + "defPerLvl": 13, + "str": 92, + "strPerLvl": 10, + "dex": 74, + "dexPerLvl": 8, + "ar": 423, + "arPerLvl": 24, + "dmgMin": 18, + "dmgMax": 24, + "dmgPerLvl": 4, + "resist": 85, + "resistPerLvl": 7, + "hireDesc": 7, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 55, + 40, + 45 + ] + }, + { + "version": 100, + "subTypeId": 16, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1500, + "level": 79, + "expPerLvl": 120, + "hp": 1006, + "hpPerLvl": 27, + "defense": 640, + "defPerLvl": 25, + "str": 130, + "strPerLvl": 10, + "dex": 104, + "dexPerLvl": 8, + "ar": 1143, + "arPerLvl": 36, + "dmgMin": 33, + "dmgMax": 39, + "dmgPerLvl": 4, + "resist": 138, + "resistPerLvl": 4, + "hireDesc": 7, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 55, + 40, + 45 + ] + }, + { + "version": 100, + "subTypeId": 17, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1000, + "level": 15, + "expPerLvl": 110, + "hp": 160, + "hpPerLvl": 9, + "defense": 80, + "defPerLvl": 5, + "str": 49, + "strPerLvl": 10, + "dex": 40, + "dexPerLvl": 8, + "ar": 15, + "arPerLvl": 12, + "dmgMin": 1, + "dmgMax": 7, + "dmgPerLvl": 4, + "resist": 25, + "resistPerLvl": 7, + "hireDesc": 8, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 38, + 49 + ] + }, + { + "version": 100, + "subTypeId": 17, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1000, + "level": 49, + "expPerLvl": 110, + "hp": 466, + "hpPerLvl": 18, + "defense": 250, + "defPerLvl": 13, + "str": 92, + "strPerLvl": 10, + "dex": 74, + "dexPerLvl": 8, + "ar": 423, + "arPerLvl": 24, + "dmgMin": 18, + "dmgMax": 24, + "dmgPerLvl": 4, + "resist": 85, + "resistPerLvl": 7, + "hireDesc": 8, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 38, + 49 + ] + }, + { + "version": 100, + "subTypeId": 17, + "classId": 359, + "act": 3, + "difficulty": 1, + "seller": 252, + "hireCost": 1000, + "level": 79, + "expPerLvl": 110, + "hp": 1006, + "hpPerLvl": 27, + "defense": 640, + "defPerLvl": 25, + "str": 130, + "strPerLvl": 10, + "dex": 104, + "dexPerLvl": 8, + "ar": 1143, + "arPerLvl": 36, + "dmgMin": 33, + "dmgMax": 39, + "dmgPerLvl": 4, + "resist": 138, + "resistPerLvl": 4, + "hireDesc": 8, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 38, + 49 + ] + }, + { + "version": 100, + "subTypeId": 18, + "classId": 359, + "act": 3, + "difficulty": 2, + "seller": 252, + "hireCost": 9500, + "level": 49, + "expPerLvl": 120, + "hp": 419, + "hpPerLvl": 18, + "defense": 225, + "defPerLvl": 13, + "str": 88, + "strPerLvl": 10, + "dex": 70, + "dexPerLvl": 8, + "ar": 381, + "arPerLvl": 24, + "dmgMin": 16, + "dmgMax": 22, + "dmgPerLvl": 4, + "resist": 82, + "resistPerLvl": 7, + "hireDesc": 6, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 41, + 47 + ] + }, + { + "version": 100, + "subTypeId": 18, + "classId": 359, + "act": 3, + "difficulty": 2, + "seller": 252, + "hireCost": 9500, + "level": 79, + "expPerLvl": 120, + "hp": 959, + "hpPerLvl": 27, + "defense": 615, + "defPerLvl": 25, + "str": 126, + "strPerLvl": 10, + "dex": 100, + "dexPerLvl": 8, + "ar": 1101, + "arPerLvl": 36, + "dmgMin": 31, + "dmgMax": 37, + "dmgPerLvl": 4, + "resist": 135, + "resistPerLvl": 4, + "hireDesc": 6, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 41, + 47 + ] + }, + { + "version": 100, + "subTypeId": 19, + "classId": 359, + "act": 3, + "difficulty": 2, + "seller": 252, + "hireCost": 12000, + "level": 49, + "expPerLvl": 130, + "hp": 419, + "hpPerLvl": 18, + "defense": 225, + "defPerLvl": 13, + "str": 88, + "strPerLvl": 10, + "dex": 70, + "dexPerLvl": 8, + "ar": 381, + "arPerLvl": 24, + "dmgMin": 16, + "dmgMax": 22, + "dmgPerLvl": 4, + "resist": 82, + "resistPerLvl": 7, + "hireDesc": 7, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 55, + 40, + 45 + ] + }, + { + "version": 100, + "subTypeId": 19, + "classId": 359, + "act": 3, + "difficulty": 2, + "seller": 252, + "hireCost": 12000, + "level": 79, + "expPerLvl": 130, + "hp": 959, + "hpPerLvl": 27, + "defense": 615, + "defPerLvl": 25, + "str": 126, + "strPerLvl": 10, + "dex": 100, + "dexPerLvl": 8, + "ar": 1101, + "arPerLvl": 36, + "dmgMin": 31, + "dmgMax": 37, + "dmgPerLvl": 4, + "resist": 135, + "resistPerLvl": 4, + "hireDesc": 7, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 55, + 40, + 45 + ] + }, + { + "version": 100, + "subTypeId": 20, + "classId": 359, + "act": 3, + "difficulty": 2, + "seller": 252, + "hireCost": 10000, + "level": 49, + "expPerLvl": 120, + "hp": 419, + "hpPerLvl": 18, + "defense": 225, + "defPerLvl": 13, + "str": 88, + "strPerLvl": 10, + "dex": 70, + "dexPerLvl": 8, + "ar": 381, + "arPerLvl": 24, + "dmgMin": 16, + "dmgMax": 22, + "dmgPerLvl": 4, + "resist": 82, + "resistPerLvl": 7, + "hireDesc": 8, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 38, + 49 + ] + }, + { + "version": 100, + "subTypeId": 20, + "classId": 359, + "act": 3, + "difficulty": 2, + "seller": 252, + "hireCost": 10000, + "level": 79, + "expPerLvl": 120, + "hp": 959, + "hpPerLvl": 27, + "defense": 615, + "defPerLvl": 25, + "str": 126, + "strPerLvl": 10, + "dex": 100, + "dexPerLvl": 8, + "ar": 1101, + "arPerLvl": 36, + "dmgMin": 31, + "dmgMax": 37, + "dmgPerLvl": 4, + "resist": 135, + "resistPerLvl": 4, + "hireDesc": 8, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 38, + 49 + ] + }, + { + "version": 100, + "subTypeId": 21, + "classId": 359, + "act": 3, + "difficulty": 3, + "seller": 252, + "hireCost": 21000, + "level": 79, + "expPerLvl": 130, + "hp": 863, + "hpPerLvl": 27, + "defense": 554, + "defPerLvl": 25, + "str": 122, + "strPerLvl": 10, + "dex": 96, + "dexPerLvl": 8, + "ar": 991, + "arPerLvl": 36, + "dmgMin": 29, + "dmgMax": 35, + "dmgPerLvl": 4, + "resist": 132, + "resistPerLvl": 4, + "hireDesc": 6, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 41, + 47 + ] + }, + { + "version": 100, + "subTypeId": 22, + "classId": 359, + "act": 3, + "difficulty": 3, + "seller": 252, + "hireCost": 27000, + "level": 79, + "expPerLvl": 140, + "hp": 863, + "hpPerLvl": 27, + "defense": 554, + "defPerLvl": 25, + "str": 122, + "strPerLvl": 10, + "dex": 96, + "dexPerLvl": 8, + "ar": 991, + "arPerLvl": 36, + "dmgMin": 29, + "dmgMax": 35, + "dmgPerLvl": 4, + "resist": 132, + "resistPerLvl": 4, + "hireDesc": 7, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 55, + 40, + 45 + ] + }, + { + "version": 100, + "subTypeId": 23, + "classId": 359, + "act": 3, + "difficulty": 3, + "seller": 252, + "hireCost": 21000, + "level": 79, + "expPerLvl": 130, + "hp": 863, + "hpPerLvl": 27, + "defense": 554, + "defPerLvl": 25, + "str": 122, + "strPerLvl": 10, + "dex": 96, + "dexPerLvl": 8, + "ar": 991, + "arPerLvl": 36, + "dmgMin": 29, + "dmgMax": 36, + "dmgPerLvl": 4, + "resist": 132, + "resistPerLvl": 4, + "hireDesc": 8, + "nameFirstKey": "merca222", + "nameLastKey": "merca241", + "wNameFirstId": 1040, + "wNameLastId": 1059, + "skills": [ + 38, + 49 + ] + }, + { + "version": 0, + "subTypeId": 24, + "classId": 561, + "act": 5, + "difficulty": 1, + "seller": 515, + "hireCost": 9000, + "level": 28, + "expPerLvl": 120, + "hp": 288, + "hpPerLvl": 14, + "defense": 180, + "defPerLvl": 7, + "str": 101, + "strPerLvl": 15, + "dex": 63, + "dexPerLvl": 10, + "ar": 150, + "arPerLvl": 17, + "dmgMin": 16, + "dmgMax": 20, + "dmgPerLvl": 6, + "resist": 56, + "resistPerLvl": 7, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 0, + "subTypeId": 24, + "classId": 561, + "act": 5, + "difficulty": 1, + "seller": 515, + "hireCost": 9000, + "level": 42, + "expPerLvl": 120, + "hp": 484, + "hpPerLvl": 21, + "defense": 278, + "defPerLvl": 20, + "str": 128, + "strPerLvl": 15, + "dex": 81, + "dexPerLvl": 10, + "ar": 388, + "arPerLvl": 27, + "dmgMin": 27, + "dmgMax": 31, + "dmgPerLvl": 8, + "resist": 81, + "resistPerLvl": 7, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 0, + "subTypeId": 24, + "classId": 561, + "act": 5, + "difficulty": 1, + "seller": 515, + "hireCost": 9000, + "level": 75, + "expPerLvl": 120, + "hp": 1177, + "hpPerLvl": 35, + "defense": 938, + "defPerLvl": 40, + "str": 190, + "strPerLvl": 15, + "dex": 123, + "dexPerLvl": 10, + "ar": 1279, + "arPerLvl": 37, + "dmgMin": 60, + "dmgMax": 64, + "dmgPerLvl": 8, + "resist": 139, + "resistPerLvl": 4, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 0, + "subTypeId": 25, + "classId": 561, + "act": 5, + "difficulty": 1, + "seller": 515, + "hireCost": 9000, + "level": 28, + "expPerLvl": 120, + "hp": 288, + "hpPerLvl": 14, + "defense": 180, + "defPerLvl": 7, + "str": 101, + "strPerLvl": 15, + "dex": 63, + "dexPerLvl": 10, + "ar": 150, + "arPerLvl": 17, + "dmgMin": 16, + "dmgMax": 20, + "dmgPerLvl": 6, + "resist": 56, + "resistPerLvl": 7, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 0, + "subTypeId": 25, + "classId": 561, + "act": 5, + "difficulty": 1, + "seller": 515, + "hireCost": 9000, + "level": 42, + "expPerLvl": 120, + "hp": 484, + "hpPerLvl": 21, + "defense": 278, + "defPerLvl": 20, + "str": 128, + "strPerLvl": 15, + "dex": 81, + "dexPerLvl": 10, + "ar": 388, + "arPerLvl": 27, + "dmgMin": 27, + "dmgMax": 31, + "dmgPerLvl": 8, + "resist": 81, + "resistPerLvl": 7, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 0, + "subTypeId": 25, + "classId": 561, + "act": 5, + "difficulty": 1, + "seller": 515, + "hireCost": 9000, + "level": 75, + "expPerLvl": 120, + "hp": 1177, + "hpPerLvl": 35, + "defense": 938, + "defPerLvl": 40, + "str": 190, + "strPerLvl": 15, + "dex": 123, + "dexPerLvl": 10, + "ar": 1279, + "arPerLvl": 37, + "dmgMin": 60, + "dmgMax": 64, + "dmgPerLvl": 8, + "resist": 139, + "resistPerLvl": 4, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 0, + "subTypeId": 26, + "classId": 561, + "act": 5, + "difficulty": 2, + "seller": 515, + "hireCost": 18000, + "level": 42, + "expPerLvl": 130, + "hp": 436, + "hpPerLvl": 21, + "defense": 250, + "defPerLvl": 20, + "str": 125, + "strPerLvl": 15, + "dex": 76, + "dexPerLvl": 10, + "ar": 349, + "arPerLvl": 27, + "dmgMin": 25, + "dmgMax": 29, + "dmgPerLvl": 8, + "resist": 78, + "resistPerLvl": 7, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 0, + "subTypeId": 26, + "classId": 561, + "act": 5, + "difficulty": 2, + "seller": 515, + "hireCost": 18000, + "level": 75, + "expPerLvl": 130, + "hp": 1129, + "hpPerLvl": 35, + "defense": 910, + "defPerLvl": 40, + "str": 187, + "strPerLvl": 15, + "dex": 118, + "dexPerLvl": 10, + "ar": 1240, + "arPerLvl": 37, + "dmgMin": 58, + "dmgMax": 62, + "dmgPerLvl": 8, + "resist": 136, + "resistPerLvl": 4, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 0, + "subTypeId": 27, + "classId": 561, + "act": 5, + "difficulty": 2, + "seller": 515, + "hireCost": 18000, + "level": 42, + "expPerLvl": 130, + "hp": 436, + "hpPerLvl": 21, + "defense": 250, + "defPerLvl": 20, + "str": 125, + "strPerLvl": 15, + "dex": 76, + "dexPerLvl": 10, + "ar": 349, + "arPerLvl": 27, + "dmgMin": 25, + "dmgMax": 29, + "dmgPerLvl": 8, + "resist": 78, + "resistPerLvl": 7, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 0, + "subTypeId": 27, + "classId": 561, + "act": 5, + "difficulty": 2, + "seller": 515, + "hireCost": 18000, + "level": 75, + "expPerLvl": 130, + "hp": 1129, + "hpPerLvl": 35, + "defense": 910, + "defPerLvl": 40, + "str": 187, + "strPerLvl": 15, + "dex": 118, + "dexPerLvl": 10, + "ar": 1240, + "arPerLvl": 37, + "dmgMin": 58, + "dmgMax": 62, + "dmgPerLvl": 8, + "resist": 136, + "resistPerLvl": 4, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 0, + "subTypeId": 28, + "classId": 561, + "act": 5, + "difficulty": 3, + "seller": 515, + "hireCost": 32000, + "level": 75, + "expPerLvl": 140, + "hp": 1016, + "hpPerLvl": 35, + "defense": 819, + "defPerLvl": 40, + "str": 184, + "strPerLvl": 15, + "dex": 113, + "dexPerLvl": 10, + "ar": 1116, + "arPerLvl": 37, + "dmgMin": 56, + "dmgMax": 60, + "dmgPerLvl": 8, + "resist": 133, + "resistPerLvl": 4, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 0, + "subTypeId": 29, + "classId": 561, + "act": 5, + "difficulty": 3, + "seller": 515, + "hireCost": 32000, + "level": 75, + "expPerLvl": 140, + "hp": 1016, + "hpPerLvl": 35, + "defense": 819, + "defPerLvl": 40, + "str": 184, + "strPerLvl": 15, + "dex": 113, + "dexPerLvl": 10, + "ar": 1116, + "arPerLvl": 37, + "dmgMin": 56, + "dmgMax": 60, + "dmgPerLvl": 8, + "resist": 133, + "resistPerLvl": 4, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 100, + "subTypeId": 24, + "classId": 561, + "act": 5, + "difficulty": 1, + "seller": 515, + "hireCost": 9000, + "level": 28, + "expPerLvl": 120, + "hp": 288, + "hpPerLvl": 18, + "defense": 180, + "defPerLvl": 10, + "str": 101, + "strPerLvl": 15, + "dex": 63, + "dexPerLvl": 10, + "ar": 150, + "arPerLvl": 20, + "dmgMin": 16, + "dmgMax": 20, + "dmgPerLvl": 6, + "resist": 56, + "resistPerLvl": 7, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 100, + "subTypeId": 24, + "classId": 561, + "act": 5, + "difficulty": 1, + "seller": 515, + "hireCost": 9000, + "level": 58, + "expPerLvl": 120, + "hp": 828, + "hpPerLvl": 27, + "defense": 480, + "defPerLvl": 35, + "str": 158, + "strPerLvl": 15, + "dex": 101, + "dexPerLvl": 10, + "ar": 750, + "arPerLvl": 35, + "dmgMin": 39, + "dmgMax": 43, + "dmgPerLvl": 8, + "resist": 109, + "resistPerLvl": 7, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 100, + "subTypeId": 24, + "classId": 561, + "act": 5, + "difficulty": 1, + "seller": 515, + "hireCost": 9000, + "level": 80, + "expPerLvl": 120, + "hp": 1422, + "hpPerLvl": 45, + "defense": 1250, + "defPerLvl": 50, + "str": 200, + "strPerLvl": 15, + "dex": 129, + "dexPerLvl": 10, + "ar": 1520, + "arPerLvl": 45, + "dmgMin": 61, + "dmgMax": 65, + "dmgPerLvl": 8, + "resist": 148, + "resistPerLvl": 4, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 100, + "subTypeId": 25, + "classId": 561, + "act": 5, + "difficulty": 1, + "seller": 515, + "hireCost": 9000, + "level": 28, + "expPerLvl": 120, + "hp": 288, + "hpPerLvl": 18, + "defense": 180, + "defPerLvl": 10, + "str": 101, + "strPerLvl": 15, + "dex": 63, + "dexPerLvl": 10, + "ar": 150, + "arPerLvl": 20, + "dmgMin": 16, + "dmgMax": 20, + "dmgPerLvl": 6, + "resist": 56, + "resistPerLvl": 7, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 100, + "subTypeId": 25, + "classId": 561, + "act": 5, + "difficulty": 1, + "seller": 515, + "hireCost": 9000, + "level": 58, + "expPerLvl": 120, + "hp": 828, + "hpPerLvl": 27, + "defense": 480, + "defPerLvl": 35, + "str": 158, + "strPerLvl": 15, + "dex": 101, + "dexPerLvl": 10, + "ar": 750, + "arPerLvl": 35, + "dmgMin": 39, + "dmgMax": 43, + "dmgPerLvl": 8, + "resist": 109, + "resistPerLvl": 7, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 100, + "subTypeId": 25, + "classId": 561, + "act": 5, + "difficulty": 1, + "seller": 515, + "hireCost": 9000, + "level": 80, + "expPerLvl": 120, + "hp": 1422, + "hpPerLvl": 45, + "defense": 1250, + "defPerLvl": 50, + "str": 200, + "strPerLvl": 15, + "dex": 129, + "dexPerLvl": 10, + "ar": 1520, + "arPerLvl": 45, + "dmgMin": 61, + "dmgMax": 65, + "dmgPerLvl": 8, + "resist": 148, + "resistPerLvl": 4, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 100, + "subTypeId": 26, + "classId": 561, + "act": 5, + "difficulty": 2, + "seller": 515, + "hireCost": 18000, + "level": 58, + "expPerLvl": 130, + "hp": 745, + "hpPerLvl": 27, + "defense": 432, + "defPerLvl": 35, + "str": 155, + "strPerLvl": 15, + "dex": 96, + "dexPerLvl": 10, + "ar": 675, + "arPerLvl": 35, + "dmgMin": 37, + "dmgMax": 41, + "dmgPerLvl": 8, + "resist": 106, + "resistPerLvl": 7, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 100, + "subTypeId": 26, + "classId": 561, + "act": 5, + "difficulty": 2, + "seller": 515, + "hireCost": 18000, + "level": 80, + "expPerLvl": 130, + "hp": 1339, + "hpPerLvl": 45, + "defense": 1202, + "defPerLvl": 50, + "str": 197, + "strPerLvl": 15, + "dex": 124, + "dexPerLvl": 10, + "ar": 1445, + "arPerLvl": 45, + "dmgMin": 59, + "dmgMax": 63, + "dmgPerLvl": 8, + "resist": 145, + "resistPerLvl": 4, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 100, + "subTypeId": 27, + "classId": 561, + "act": 5, + "difficulty": 2, + "seller": 515, + "hireCost": 18000, + "level": 58, + "expPerLvl": 130, + "hp": 745, + "hpPerLvl": 27, + "defense": 432, + "defPerLvl": 35, + "str": 155, + "strPerLvl": 15, + "dex": 96, + "dexPerLvl": 10, + "ar": 675, + "arPerLvl": 35, + "dmgMin": 37, + "dmgMax": 41, + "dmgPerLvl": 8, + "resist": 106, + "resistPerLvl": 7, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 100, + "subTypeId": 27, + "classId": 561, + "act": 5, + "difficulty": 2, + "seller": 515, + "hireCost": 18000, + "level": 80, + "expPerLvl": 130, + "hp": 1339, + "hpPerLvl": 45, + "defense": 1202, + "defPerLvl": 50, + "str": 197, + "strPerLvl": 15, + "dex": 124, + "dexPerLvl": 10, + "ar": 1445, + "arPerLvl": 45, + "dmgMin": 59, + "dmgMax": 63, + "dmgPerLvl": 8, + "resist": 145, + "resistPerLvl": 4, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 100, + "subTypeId": 28, + "classId": 561, + "act": 5, + "difficulty": 3, + "seller": 515, + "hireCost": 32000, + "level": 80, + "expPerLvl": 140, + "hp": 1205, + "hpPerLvl": 45, + "defense": 1082, + "defPerLvl": 50, + "str": 194, + "strPerLvl": 15, + "dex": 119, + "dexPerLvl": 10, + "ar": 1301, + "arPerLvl": 45, + "dmgMin": 57, + "dmgMax": 61, + "dmgPerLvl": 8, + "resist": 142, + "resistPerLvl": 4, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + }, + { + "version": 100, + "subTypeId": 29, + "classId": 561, + "act": 5, + "difficulty": 3, + "seller": 515, + "hireCost": 32000, + "level": 80, + "expPerLvl": 140, + "hp": 1205, + "hpPerLvl": 45, + "defense": 1082, + "defPerLvl": 50, + "str": 194, + "strPerLvl": 15, + "dex": 119, + "dexPerLvl": 10, + "ar": 1301, + "arPerLvl": 45, + "dmgMin": 57, + "dmgMax": 61, + "dmgPerLvl": 8, + "resist": 142, + "resistPerLvl": 4, + "hireDesc": 0, + "nameFirstKey": "MercX101", + "nameLastKey": "MercX167", + "wNameFirstId": 10835, + "wNameLastId": 10901, + "skills": [ + 126, + 139 + ] + } +] + +export interface HirelingCandidateDesc { + readonly mercId: number + readonly nameId: number + readonly seed: number + readonly actIndex: number + readonly classId: number + readonly subTypeId: number + readonly name: string + readonly level: number + readonly hp: number + readonly defense: number + readonly strength: number + readonly dexterity: number + readonly cost: number + readonly resist: number + readonly hireDescId: number + readonly hireDesc: string + readonly skills: readonly number[] + readonly line1: string + readonly line2: string +} + +/** + * Maps a mercenary name ID to their act index (0..3) per D2Common.dll ordinal 10988 (0x6fd7c9e0). + */ +export function resolveHirelingAct(wMercName: number, expansion = true): number { + const version = expansion ? 100 : 0 + for (const r of HIRELING_RECORDS) { + if (r.version === version && r.wNameFirstId <= wMercName && wMercName <= r.wNameLastId) { + return r.act - 1 + } + } + // Fallbacks by known name ranges + if (wMercName >= 3411 && wMercName <= 3451) return 0 + if (wMercName >= 1019 && wMercName <= 1039) return 1 + if (wMercName >= 1040 && wMercName <= 1059) return 2 + if (wMercName === 11021 || wMercName === 0x0421) return 2 + if (wMercName >= 10835 && wMercName <= 10901) return 4 + return 0 +} + +/** + * Resolves a mercenary name string from their TBL index per D2Client.dll 0x6faf9783. + */ +export function resolveHirelingName(wMercName: number, lang: 'ENG' | 'CHI' = 'CHI'): string { + // D2Client.dll 0x6faf978c: if wMercName == 0x0421 (1057), replace with 0x2b0d (11021 = "Jarulf") + const resolvedNameId = wMercName === 0x0421 ? 0x2b0d : wMercName + + if (resolvedNameId === 0x2b0d) { + return lookupTbl('merca239', lang) ?? (lang === 'ENG' ? 'Jarulf' : '賈魯夫') + } + + // Act 1 Rogue (string.tbl 3411..3444 -> merc01..merc34) + if (resolvedNameId >= 3411 && resolvedNameId <= 3451) { + const idx = resolvedNameId - 3411 + 1 + const key = `merc${idx < 10 ? `0${idx}` : idx}` + return lookupTbl(key, lang) ?? (lang === 'ENG' ? `Rogue #${idx}` : `羅格 #${idx}`) + } + + // Act 2 Desert Mercenary (patchstring.tbl 1019..1039 -> merca201..merca221) + if (resolvedNameId >= 1019 && resolvedNameId <= 1039) { + const idx = resolvedNameId - 1019 + 1 + const key = `merca2${idx < 10 ? `0${idx}` : idx}` + return lookupTbl(key, lang) ?? (lang === 'ENG' ? `Desert Mercenary #${idx}` : `沙漠傭兵 #${idx}`) + } + + // Act 3 Iron Wolf (patchstring.tbl 1040..1059 -> merca222..merca241) + if (resolvedNameId >= 1040 && resolvedNameId <= 1059) { + const idx = resolvedNameId - 1040 + 22 + const key = `merca2${idx}` + return lookupTbl(key, lang) ?? (lang === 'ENG' ? `Iron Wolf #${idx}` : `鐵狼 #${idx}`) + } + + // Act 5 Barbarian (patchstring.tbl 835..901 / global 10835..10901 -> MercX101..MercX167) + const barbId = resolvedNameId >= 10000 ? resolvedNameId - 10000 : resolvedNameId + if (barbId >= 835 && barbId <= 901) { + const idx = barbId - 835 + 101 + const key = `MercX${idx}` + return lookupTbl(key, lang) ?? (lang === 'ENG' ? `Barbarian #${idx}` : `野蠻人 #${idx}`) + } + + return lookupTbl(`merc${resolvedNameId}`, lang) ?? `Mercenary #${resolvedNameId}` +} + +/** + * Resolves the hireling ability description string per D2Common.dll ordinal 10091 (0x6fd7c9c0). + */ +export function resolveHirelingDesc(hireDescId: number, lang: 'ENG' | 'CHI' = 'CHI'): string { + switch (hireDescId) { + case 1: + return lookupTbl('strhirespecial1', lang) ?? (lang === 'ENG' ? 'Fire Arrow' : '火箭') + case 2: + return lookupTbl('strhirespecial2', lang) ?? (lang === 'ENG' ? 'Cold Arrow' : '冰箭') + case 3: + return lookupTbl('PalMercEXST4X', lang) ?? (lang === 'ENG' ? 'Combat' : '戰鬥') + case 4: + return lookupTbl('PalMercExST1X', lang) ?? (lang === 'ENG' ? 'Defensive' : '防禦') + case 5: + return lookupTbl('PalMercEXST3X', lang) ?? (lang === 'ENG' ? 'Offensive' : '進攻') + case 6: + return lookupTbl('strhirespecial7', lang) ?? (lang === 'ENG' ? 'Fire' : '火') + case 7: + return lookupTbl('strhirespecial6', lang) ?? (lang === 'ENG' ? 'Cold' : '冷') + case 8: + return lookupTbl('strhirespecial5', lang) ?? (lang === 'ENG' ? 'Lightning' : '閃電') + default: + return '' + } +} + +/** + * Formats a candidate into 2 lines for the scrollable list control per D2Client.dll 0x6faf9440. + */ +export function formatHirelingCandidateLines( + name: string, + level: number, + hp: number, + defense: number, + cost: number, + hireDesc: string, + lang: 'ENG' | 'CHI' = 'CHI', +): { line1: string; line2: string } { + const lvlLabel = lookupTbl('Level', lang) ?? (lang === 'ENG' ? 'Lvl' : '等級') + const lifeLabel = lookupTbl('HP', lang) ?? (lang === 'ENG' ? 'Life' : '生命') + const defLabel = lookupTbl('AC', lang) ?? (lang === 'ENG' ? 'Def' : '防禦') + const costLabel = lookupTbl('Cost', lang) ?? (lang === 'ENG' ? 'Cost' : '花費') + + // Line 1: - Lvl: Life: Def: Cost: + const line1 = `${name} - ${lvlLabel}: ${level} ${lifeLabel}: ${hp} ${defLabel}: ${defense} ${costLabel}: ${cost}` + + // Line 2: 2 leading spaces + HireDesc (or blank if no special ability) + const line2 = hireDesc ? ` ${hireDesc}` : ' ' + + return { line1, line2 } +} + +/** + * Evaluates a candidate's authentic stats, level, and hire cost using Diablo II's 64-bit LCG + * per D2Common.dll ordinal 10458 (0x6fd7ccc0). + */ +export function resolveHirelingCandidate( + candidate: { readonly nameId: number; readonly seed: number }, + playerLevel: number, + difficulty: number, + actIndex: number, + expansion = true, + lang: 'ENG' | 'CHI' = 'CHI', +): HirelingCandidateDesc { + const version = expansion ? 100 : 0 + const targetAct = actIndex + 1 + const targetDiff = difficulty + 1 + + // 1. Collect all base rows matching (version, act, difficulty) per 0x6fda3050 + const actDiffRows = HIRELING_RECORDS.filter( + (r) => r.version === version && r.act === targetAct && r.difficulty === targetDiff, + ) + + let matchingRows = actDiffRows + if (actDiffRows.length > 0) { + const baseLevel = actDiffRows[0]!.level + const levelFiltered = actDiffRows.filter((r) => r.level === baseLevel) + if (levelFiltered.length > 0) { + matchingRows = levelFiltered + } + } + + if (matchingRows.length === 0) { + matchingRows = HIRELING_RECORDS.filter((r) => r.act === targetAct) + } + if (matchingRows.length === 0) { + matchingRows = [HIRELING_RECORDS[0]!] + } + + // 2. D2Common 64-bit LCG simulation (0x6fd7cd20..0x6fd7cd95) + let seedLo = candidate.seed >>> 0 + let seedHi = 0x29a // 666 + + // Step 1: Subtype / row selection + const step1 = BigInt(seedLo) * 0x6ac690c5n + BigInt(seedHi) + seedLo = Number(step1 & 0xffffffffn) >>> 0 + seedHi = Number((step1 >> 32n) & 0xffffffffn) >>> 0 + + const rowCount = matchingRows.length + const isPowerOfTwo = (rowCount & (rowCount - 1)) === 0 + const rowIndex = isPowerOfTwo ? (seedLo & (rowCount - 1)) : (seedLo % rowCount) + const pRow = matchingRows[rowIndex] ?? matchingRows[0]! + + // Step 2: Level selection + const step2 = BigInt(seedLo) * 0x6ac690c5n + BigInt(seedHi) + const seedLo2 = Number(step2 & 0xffffffffn) >>> 0 + const levelOffset = (seedLo2 >>> 0) % 5 + + const effPlayerLevel = Math.max(1, playerLevel) + const mercLevel = Math.max(2, effPlayerLevel + levelOffset - 5) + const levelDelta = mercLevel - pRow.level + + // Step 3: Compute exact stats + const hp = Math.max(40, pRow.hp + levelDelta * pRow.hpPerLvl) + const defense = Math.max(0, pRow.defense + levelDelta * pRow.defPerLvl) + const strength = Math.max(10, pRow.str + ((levelDelta * pRow.strPerLvl) >> 3)) + const dexterity = Math.max(10, pRow.dex + ((levelDelta * pRow.dexPerLvl) >> 3)) + const cost = Math.max( + pRow.hireCost, + Math.trunc((pRow.hireCost * (100 + 15 * levelDelta)) / 100), + ) + const resist = Math.max(0, pRow.resist + ((levelDelta * pRow.resistPerLvl) >> 3)) + + const name = resolveHirelingName(candidate.nameId, lang) + const hireDesc = resolveHirelingDesc(pRow.hireDesc, lang) + const { line1, line2 } = formatHirelingCandidateLines( + name, + mercLevel, + hp, + defense, + cost, + hireDesc, + lang, + ) + + return { + mercId: candidate.nameId, // client uses wMercName as mercId for 0x36 + nameId: candidate.nameId, + seed: candidate.seed, + actIndex, + classId: pRow.classId, + subTypeId: pRow.subTypeId, + name, + level: mercLevel, + hp, + defense, + strength, + dexterity, + cost, + resist, + hireDescId: pRow.hireDesc, + hireDesc, + skills: pRow.skills, + line1, + line2, + } +} + +/** + * Resolves a list of candidates from raw { nameId, seed } pairs. + */ +export function resolveHirelingCandidates( + candidates: readonly { readonly nameId: number; readonly seed: number }[], + playerLevel: number, + difficulty: number, + actIndex: number, + expansion = true, + lang: 'ENG' | 'CHI' = 'CHI', +): readonly HirelingCandidateDesc[] { + return candidates.map((c) => + resolveHirelingCandidate(c, playerLevel, difficulty, actIndex, expansion, lang), + ) +} diff --git a/tests/client/hire-dialog.test.ts b/tests/client/hire-dialog.test.ts new file mode 100644 index 0000000..db10963 --- /dev/null +++ b/tests/client/hire-dialog.test.ts @@ -0,0 +1,191 @@ +import { describe, it, expect, vi } from 'vitest' +import { + WorldPanelsHud, + HIRE_DIALOG_WIDTH, + HIRE_DIALOG_HEIGHT, + HIRE_CONFIRM_WIDTH, + HIRE_CONFIRM_HEIGHT, +} from '../../src/client/ui/world-panels.ts' +import { resolveHirelingCandidates } from '../../src/common/units/hireling-table.ts' + +function createMockCtx() { + const drawnTexts: string[] = [] + let fillRectCount = 0 + let strokeRectCount = 0 + return { + drawnTexts, + get fillRectCount() { + return fillRectCount + }, + get strokeRectCount() { + return strokeRectCount + }, + fillStyle: '', + strokeStyle: '', + fillRect: vi.fn(() => fillRectCount++), + strokeRect: vi.fn(() => strokeRectCount++), + fillText: vi.fn((text: string) => drawnTexts.push(text)), + drawImage: vi.fn(), + save: vi.fn(), + restore: vi.fn(), + } as unknown as CanvasRenderingContext2D & { drawnTexts: string[]; fillRectCount: number; strokeRectCount: number } +} + +describe('WorldPanelsHud — Mercenary Hire Stone Dialog (0x6faf9440 & 0x6faf7500 parity)', () => { + it('identifies dialog hitboxes accurately per D2Client.dll 0x6faf9440', () => { + const wp = new WorldPanelsHud() + wp.activePanel = 'hire' + + // Centered at 155, 105 (490x350) + expect(wp.isPointInHireMercDialog(100, 50)).toBe(false) + expect(wp.isPointInHireMercDialog(155, 105)).toBe(true) + expect(wp.isPointInHireMercDialog(400, 300)).toBe(true) + expect(wp.isPointInHireMercDialog(644, 454)).toBe(true) + expect(wp.isPointInHireMercDialog(650, 460)).toBe(false) + + // Confirmation dialog bounds (250, 190, 300, 160) + wp.replaceMercConfirmOpen = true + expect(wp.isPointInHireMercDialog(160, 110)).toBe(false) + expect(wp.isPointInHireMercDialog(250, 190)).toBe(true) + expect(wp.isPointInHireMercDialog(400, 250)).toBe(true) + expect(wp.isPointInHireMercDialog(550, 350)).toBe(true) + }) + + it('draws authentic 490x350 stone dialog with ItemDesc1s gold header and candidate list', () => { + const wp = new WorldPanelsHud() + wp.lang = 'ENG' + wp.activePanel = 'hire' + wp.playerGold = 5000 + wp.stashGold = 12000 + + const rawCandidates = [ + { nameId: 3411, seed: 0x1234 }, + { nameId: 3412, seed: 0x5678 }, + ] + wp.mercHireList = resolveHirelingCandidates(rawCandidates, 10, 0, 0, true, 'ENG') + + const ctx = createMockCtx() + wp.drawHireMercDialog(ctx) + + expect(ctx.fillRectCount).toBeGreaterThan(0) + // Title with total gold 17000 + expect(ctx.drawnTexts.some((t) => t.includes('17000') && t.includes('Hire which Mercenary?'))).toBe(true) + // Candidates rendered with Lvl, Life, Def, Cost + expect(ctx.drawnTexts.some((t) => t.includes('Aliza - Lvl: 8'))).toBe(true) + expect(ctx.drawnTexts.some((t) => t.includes('Fire Arrow'))).toBe(true) + // Bottom cancel button + expect(ctx.drawnTexts.some((t) => t.includes('cancel'))).toBe(true) + }) + + it('draws ItemDesc1t when hire list is empty', () => { + const wp = new WorldPanelsHud() + wp.lang = 'ENG' + wp.activePanel = 'hire' + wp.mercHireList = [] + + const ctx = createMockCtx() + wp.drawHireMercDialog(ctx) + + expect(ctx.drawnTexts.some((t) => t.includes('There are no Mercenaries left to hire.'))).toBe(true) + }) + + it('handles candidate hover, scrolling, and direct hire when player has no existing mercenary', () => { + const wp = new WorldPanelsHud() + wp.activePanel = 'hire' + wp.activeNpcId = 150 + + const rawCandidates = Array.from({ length: 10 }, (_, i) => ({ + nameId: 3411 + i, + seed: 0x1000 + i * 0x100, + })) + wp.mercHireList = resolveHirelingCandidates(rawCandidates, 15, 0, 0, true, 'ENG') + + // 1. Wheel scroll + expect(wp.handleHireWheel(1)).toBe(true) + expect(wp.hireScrollOffset).toBe(1) + expect(wp.handleHireWheel(-1)).toBe(true) + expect(wp.hireScrollOffset).toBe(0) + + // 2. Mouse move hover + wp.handleHireMercMove(200, 160) // over Candidate 0 + expect(wp.hireHoveredIndex).toBe(0) + + // 3. Direct click to hire when player has no existing mercenary + let hiredNpcId = 0 + let hiredMercId = 0 + const onHireMerc = vi.fn((npcId: number, mercId: number) => { + hiredNpcId = npcId + hiredMercId = mercId + }) + + const handled = wp.handleHireMercClick(200, 160, { onHireMerc }) + expect(handled).toBe(true) + expect(onHireMerc).toHaveBeenCalledWith(150, 3411) + expect(wp.activePanel).toBe('none') + }) + + it('prompts confirmation dialog when player already possesses a mercenary (0x6faf7500)', () => { + const wp = new WorldPanelsHud() + wp.lang = 'ENG' + wp.activePanel = 'hire' + wp.activeNpcId = 150 + wp.merc = { mercId: 42, isDead: false } // Existing active mercenary + + const rawCandidates = [{ nameId: 3411, seed: 0x1234 }] + wp.mercHireList = resolveHirelingCandidates(rawCandidates, 15, 0, 0, true, 'ENG') + + const onHireMerc = vi.fn() + + // First click: selects candidate + wp.handleHireMercClick(200, 160, { onHireMerc }) + expect(wp.hireSelectedIndex).toBe(0) + expect(wp.replaceMercConfirmOpen).toBe(false) + expect(onHireMerc).not.toHaveBeenCalled() + + // Second click on selected candidate: triggers replacement warning dialog + wp.handleHireMercClick(200, 160, { onHireMerc }) + expect(wp.replaceMercConfirmOpen).toBe(true) + expect(wp.pendingHireCandidate?.nameId).toBe(3411) + + // Render with confirm dialog open + const ctx = createMockCtx() + wp.drawHireMercDialog(ctx) + expect(ctx.drawnTexts.some((t) => t.includes('This Mercenary will replace your current one.'))).toBe(true) + expect(ctx.drawnTexts.some((t) => t.includes('Yes'))).toBe(true) + expect(ctx.drawnTexts.some((t) => t.includes('No'))).toBe(true) + + // Click "No" (cx + 150 = 400, cy + 128 = 318) + wp.handleHireMercClick(400, 318, { onHireMerc }) + expect(wp.replaceMercConfirmOpen).toBe(false) + expect(wp.activePanel).toBe('hire') + expect(onHireMerc).not.toHaveBeenCalled() + + // Open again and click "Yes" (cx + 150 = 400, cy + 104 = 294) + wp.handleHireMercClick(200, 160, { onHireMerc }) + expect(wp.replaceMercConfirmOpen).toBe(true) + + wp.handleHireMercClick(400, 294, { onHireMerc }) + expect(wp.replaceMercConfirmOpen).toBe(false) + expect(onHireMerc).toHaveBeenCalledWith(150, 3411) + expect(wp.activePanel).toBe('none') + }) + + it('clicking cancel button closes hire dialog and re-opens NPC menu (0x6faf9090)', () => { + const wp = new WorldPanelsHud() + wp.openNpcMenuByName('Kashya', 400, 220) + expect(wp.npcMenu).not.toBeNull() + + wp.clickNpcMenuOption('hire') + expect(wp.activePanel).toBe('hire') + expect(wp.npcMenu).toBeNull() + + // Click Cancel button at dialogX + 245 = 400, dialogY + 315 = 420 + const onClose = vi.fn() + wp.handleHireMercClick(400, 420, { onClose }) + + expect(wp.activePanel).toBe('none') + expect(onClose).toHaveBeenCalled() + expect(wp.npcMenu).not.toBeNull() + expect(wp.npcMenu?.npcName).toContain('Kashya') + }) +}) diff --git a/tests/common/hireling-table.test.ts b/tests/common/hireling-table.test.ts new file mode 100644 index 0000000..7a3577c --- /dev/null +++ b/tests/common/hireling-table.test.ts @@ -0,0 +1,162 @@ +import { describe, it, expect } from 'vitest' +import { + HIRELING_RECORDS, + resolveHirelingAct, + resolveHirelingName, + resolveHirelingDesc, + resolveHirelingCandidate, + resolveHirelingCandidates, + formatHirelingCandidateLines, +} from '../../src/common/units/hireling-table.ts' + +describe('hireling-table (authoritative Diablo II v1.13c Hireling.txt / D2Common parity)', () => { + it('loads all 120 compiled hireling records with valid attributes', () => { + expect(HIRELING_RECORDS).toHaveLength(120) + for (const r of HIRELING_RECORDS) { + expect([0, 100]).toContain(r.version) + expect([271, 338, 359, 561]).toContain(r.classId) + expect([1, 2, 3, 5]).toContain(r.act) + expect([1, 2, 3]).toContain(r.difficulty) + expect([150, 198, 252, 515]).toContain(r.seller) + expect(r.level).toBeGreaterThan(0) + expect(r.hireCost).toBeGreaterThan(0) + expect(r.hp).toBeGreaterThan(0) + expect(r.skills.length).toBeGreaterThan(0) + } + }) + + it('resolves act index from mercenary name ID matching D2Common_10988 (0x6fd7c9e0)', () => { + // Act 1 Rogue (merc01..merc34 / global 3411..3444) -> act 0 + expect(resolveHirelingAct(3411, true)).toBe(0) + expect(resolveHirelingAct(3444, true)).toBe(0) + + // Act 2 Desert Mercenary (merca201..merca221 / global 1019..1039) -> act 1 + expect(resolveHirelingAct(1019, true)).toBe(1) + expect(resolveHirelingAct(1039, true)).toBe(1) + + // Act 3 Iron Wolf (merca222..merca241 / global 1040..1059) -> act 2 + expect(resolveHirelingAct(1040, true)).toBe(2) + expect(resolveHirelingAct(1059, true)).toBe(2) + expect(resolveHirelingAct(11021, true)).toBe(2) // Jarulf + expect(resolveHirelingAct(0x0421, true)).toBe(2) // 1057 Jarulf legacy + + // Act 5 Barbarian (MercX101..MercX167 / global 10835..10901) -> act 4 + expect(resolveHirelingAct(10835, true)).toBe(4) + expect(resolveHirelingAct(10901, true)).toBe(4) + }) + + it('resolves localized mercenary names in English and Chinese per D2Client.dll 0x6faf9783', () => { + // Act 1 + expect(resolveHirelingName(3411, 'ENG')).toBe('Aliza') + expect(resolveHirelingName(3411, 'CHI')).toBe('阿里撒') + expect(resolveHirelingName(3444, 'ENG')).toBe('Ryann') + expect(resolveHirelingName(3444, 'CHI')).toBe('萊恩') + + // Act 2 + expect(resolveHirelingName(1019, 'ENG')).toBe('Hazade') + expect(resolveHirelingName(1019, 'CHI')).toBe('哈撒迪') + expect(resolveHirelingName(1039, 'ENG')).toBe('Vikhyat') + expect(resolveHirelingName(1039, 'CHI')).toBe('維克懷特') + + // Act 3 + expect(resolveHirelingName(1040, 'ENG')).toBe('Jelani') + expect(resolveHirelingName(1040, 'CHI')).toBe('喬安妮') + expect(resolveHirelingName(11021, 'ENG')).toBe('Jarulf') + expect(resolveHirelingName(0x0421, 'ENG')).toBe('Jarulf') + + // Act 5 + expect(resolveHirelingName(10835, 'ENG')).toBe('Varaya') + expect(resolveHirelingName(10835, 'CHI')).toBe('伐德哈卡') + expect(resolveHirelingName(10901, 'ENG')).toBe('Theodoric') + expect(resolveHirelingName(10901, 'CHI')).toBe('狄奧多理') + }) + + it('resolves ability descriptions matching D2Common_10091 (0x6fd7c9c0) in ENG and CHI', () => { + expect(resolveHirelingDesc(1, 'ENG')).toBe('Fire Arrow') + expect(resolveHirelingDesc(1, 'CHI')).toBe('火箭') + + expect(resolveHirelingDesc(2, 'ENG')).toBe('Cold Arrow') + expect(resolveHirelingDesc(2, 'CHI')).toBe('冰箭') + + expect(resolveHirelingDesc(3, 'ENG')).toBe('Combat') + expect(resolveHirelingDesc(3, 'CHI')).toBe('戰鬥') + + expect(resolveHirelingDesc(4, 'ENG')).toBe('Defensive') + expect(resolveHirelingDesc(4, 'CHI')).toBe('防禦') + + expect(resolveHirelingDesc(5, 'ENG')).toBe('Offensive') + expect(resolveHirelingDesc(5, 'CHI')).toBe('進攻') + + expect(resolveHirelingDesc(6, 'ENG')).toBe('Fire') + expect(resolveHirelingDesc(6, 'CHI')).toBe('火') + + expect(resolveHirelingDesc(7, 'ENG')).toBe('Cold') + expect(resolveHirelingDesc(7, 'CHI')).toBe('冷') + + expect(resolveHirelingDesc(8, 'ENG')).toBe('Lightning') + expect(resolveHirelingDesc(8, 'CHI')).toBe('閃電') + + expect(resolveHirelingDesc(0, 'ENG')).toBe('') + expect(resolveHirelingDesc(0, 'CHI')).toBe('') + }) + + it('formats candidate lines per D2Client.dll 0x6faf9440', () => { + const eng = formatHirelingCandidateLines('Aliza', 8, 90, 55, 175, 'Fire Arrow', 'ENG') + expect(eng.line1).toBe('Aliza - Lvl: 8 Life: 90 Def: 55 Cost: 175') + expect(eng.line2).toBe(' Fire Arrow') + + const chi = formatHirelingCandidateLines('阿里撒', 8, 90, 55, 175, '火箭', 'CHI') + expect(chi.line1).toBe('阿里撒 - 等級: 8 生命: 90 防禦: 55 花費: 175') + expect(chi.line2).toBe(' 火箭') + + const blankSpecial = formatHirelingCandidateLines('Varaya', 30, 320, 200, 9200, '', 'ENG') + expect(blankSpecial.line2).toBe(' ') + }) + + it('computes exact 64-bit LCG stats matching D2Common_10458 across difficulties', () => { + // Act 1 Rogue (Normal, seed 0x1234, playerLevel 10) + const c1 = resolveHirelingCandidate({ nameId: 3411, seed: 0x1234 }, 10, 0, 0, true, 'ENG') + expect(c1.name).toBe('Aliza') + expect(c1.actIndex).toBe(0) + expect(c1.classId).toBe(271) + expect(c1.level).toBe(8) + expect(c1.hp).toBe(90) + expect(c1.defense).toBe(55) + expect(c1.strength).toBe(41) + expect(c1.dexterity).toBe(55) + expect(c1.cost).toBe(175) + expect(c1.hireDesc).toBe('Fire Arrow') + + // Act 2 Desert Mercenary (Nightmare, seed 0x5678, playerLevel 50) + const c2 = resolveHirelingCandidate({ nameId: 1019, seed: 0x5678 }, 50, 1, 1, true, 'ENG') + expect(c2.name).toBe('Hazade') + expect(c2.actIndex).toBe(1) + expect(c2.classId).toBe(338) + expect(c2.level).toBeGreaterThanOrEqual(45) + expect(c2.cost).toBeGreaterThan(5000) + expect(['Combat', 'Defensive', 'Offensive']).toContain(c2.hireDesc) + + // Act 5 Barbarian (Normal, seed 0x9abc, playerLevel 35) + const c5 = resolveHirelingCandidate({ nameId: 10835, seed: 0x9abc }, 35, 0, 4, true, 'ENG') + expect(c5.name).toBe('Varaya') + expect(c5.actIndex).toBe(4) + expect(c5.classId).toBe(561) + expect(c5.level).toBeGreaterThanOrEqual(30) + expect(c5.cost).toBeGreaterThanOrEqual(9000) + expect(c5.hireDesc).toBe('') + }) + + it('resolveHirelingCandidates maps candidate list cleanly', () => { + const raw = [ + { nameId: 3411, seed: 0x1111 }, + { nameId: 3412, seed: 0x2222 }, + { nameId: 3413, seed: 0x3333 }, + ] + const res = resolveHirelingCandidates(raw, 12, 0, 0, true, 'CHI') + expect(res).toHaveLength(3) + expect(res[0]!.nameId).toBe(3411) + expect(res[1]!.nameId).toBe(3412) + expect(res[2]!.nameId).toBe(3413) + expect(res[0]!.line1).toContain('等級:') + }) +})