Compare commits
No commits in common. "ffda82d6507060936c78fc35b4d2d1951338728d" and "8c5ae5c623069f8ba1e0ae4049696467b0e3f282" have entirely different histories.
ffda82d650
...
8c5ae5c623
|
|
@ -505,12 +505,6 @@ export class HudManager {
|
||||||
difficulty: 0 | 1 | 2 = 0,
|
difficulty: 0 | 1 | 2 = 0,
|
||||||
): boolean {
|
): boolean {
|
||||||
this.worldPanels.activePlayerInventory = this.inventory
|
this.worldPanels.activePlayerInventory = this.inventory
|
||||||
this.worldPanels.setPlayerContext({
|
|
||||||
level: this.charSheet.attrs.level,
|
|
||||||
str: this.charSheet.attrs.str,
|
|
||||||
dex: this.charSheet.attrs.dex,
|
|
||||||
classCode: this.currentClass ?? 'sor',
|
|
||||||
})
|
|
||||||
const ok = this.worldPanels.openVendorForNpc(
|
const ok = this.worldPanels.openVendorForNpc(
|
||||||
descriptorOrName,
|
descriptorOrName,
|
||||||
mode,
|
mode,
|
||||||
|
|
@ -1112,12 +1106,6 @@ export class HudManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
syncPublishedState(): void {
|
syncPublishedState(): void {
|
||||||
this.worldPanels.setPlayerContext({
|
|
||||||
level: this.charSheet.attrs.level,
|
|
||||||
str: this.charSheet.attrs.str,
|
|
||||||
dex: this.charSheet.attrs.dex,
|
|
||||||
classCode: this.currentClass ?? 'sor',
|
|
||||||
})
|
|
||||||
this.syncCursorMode(this.cursor.lastNowMs)
|
this.syncCursorMode(this.cursor.lastNowMs)
|
||||||
const cursorInfo = this.cursor.getCurrentFrame(this.cursor.lastNowMs)
|
const cursorInfo = this.cursor.getCurrentFrame(this.cursor.lastNowMs)
|
||||||
this.state.dc6DecodeFailures = this.dc6DecodeFailures
|
this.state.dc6DecodeFailures = this.dc6DecodeFailures
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@
|
||||||
import type { D2ColorCode, D2FontRenderer } from './font.ts'
|
import type { D2ColorCode, D2FontRenderer } from './font.ts'
|
||||||
import { BAKED_UI_MANIFEST, type SpriteRect } from './baked-ui-meta.ts'
|
import { BAKED_UI_MANIFEST, type SpriteRect } from './baked-ui-meta.ts'
|
||||||
import type { Item } from '../game/items.ts'
|
import type { Item } from '../game/items.ts'
|
||||||
import type { CharacterClassCode } from '../game/classes.ts'
|
|
||||||
|
|
||||||
import { itemToUiInventoryItem, resolveAllowedSlots, resolveItemInvFile } from './item-bridge.ts'
|
import { itemToUiInventoryItem, resolveAllowedSlots, resolveItemInvFile } from './item-bridge.ts'
|
||||||
|
|
||||||
|
|
@ -52,8 +51,6 @@ export interface UiInventoryItem {
|
||||||
readonly damage?: string | undefined
|
readonly damage?: string | undefined
|
||||||
readonly reqLevel?: number | undefined
|
readonly reqLevel?: number | undefined
|
||||||
readonly reqStr?: number | undefined
|
readonly reqStr?: number | undefined
|
||||||
readonly reqDex?: number | undefined
|
|
||||||
readonly reqClass?: CharacterClassCode | undefined
|
|
||||||
readonly stats: readonly { text: string; color?: D2ColorCode }[]
|
readonly stats: readonly { text: string; color?: D2ColorCode }[]
|
||||||
readonly invtransform?: string | undefined
|
readonly invtransform?: string | undefined
|
||||||
readonly chrtransform?: string | undefined
|
readonly chrtransform?: string | undefined
|
||||||
|
|
@ -1094,90 +1091,6 @@ export function isUiItemUnidentified(item: UiInventoryItem): boolean {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Ground Truth (D2Client.dll 1.13c @ 0x6fb3c600..0x6fb3c710 & 0x6fbbb330..0x6fbbb334):
|
|
||||||
* Palette tint indices used by `INVENTORY_DrawGridItems` (`0x6fb458a0..0x6fb45b58`):
|
|
||||||
* - Index 0 (`RGB(128, 0, 0)`): Red backdrop — unusable, unmet requirements, unidentified, or gamble mystery item
|
|
||||||
* - Index 1 (`RGB(0, 128, 0)`): Green backdrop — hovered cell only when holding an item on cursor (`pCursorItem != NULL` at `0x6fb45a04`)
|
|
||||||
* - Index 2 (`RGB(0, 0, 128)`): Blue backdrop — usable / equippable item
|
|
||||||
*/
|
|
||||||
export const GRID_ITEM_BG_RED = 'rgba(72, 18, 18, 0.50)'
|
|
||||||
export const GRID_ITEM_BG_GREEN = 'rgba(46, 68, 36, 0.55)'
|
|
||||||
export const GRID_ITEM_BG_BLUE = 'rgba(20, 32, 54, 0.45)'
|
|
||||||
|
|
||||||
export interface PlayerItemRequirementContext {
|
|
||||||
readonly level: number
|
|
||||||
readonly str: number
|
|
||||||
readonly dex: number
|
|
||||||
readonly classCode?: CharacterClassCode | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ground Truth (D2Client.dll 1.13c @ 0x6fb45ab8..0x6fb45b54 & D2Common.dll @ 0x6fd76db0 `ITEMS_CheckItemRequirements` #10244):
|
|
||||||
* Checks whether the player meets all requirements to equip/use an item in a vendor or inventory grid:
|
|
||||||
* 1. Gamble mode or Unidentified (`bIdentified == 0` at `0x6fd76e23` / `0x6fd77006`) -> false (Red)
|
|
||||||
* 2. Broken durability (`durability.max > 0 && durability.current <= 0`) -> false (Red)
|
|
||||||
* 3. Empty Tome (`tbk` / `ibk` with `quantity <= 0` at `0x6fd77072`) -> false (Red)
|
|
||||||
* 4. Class restriction (`0x6fd74280` `ITEMS_GetClassOfClassSpecificItem` #10202) -> false if `item.reqClass !== player.classCode` (Red)
|
|
||||||
* 5. Level requirement (`0x6fd76fc7`) -> false if `player.level < item.reqLevel` (Red)
|
|
||||||
* 6. Strength requirement (`0x6fd76eab`) -> false if `player.str < item.reqStr` (Red)
|
|
||||||
* 7. Dexterity requirement (`0x6fd76f83`) -> false if `player.dex < item.reqDex` (Red)
|
|
||||||
*/
|
|
||||||
export function canPlayerUseUiItem(
|
|
||||||
item: UiInventoryItem,
|
|
||||||
player: PlayerItemRequirementContext,
|
|
||||||
vendorMode: 'trade' | 'gamble' = 'trade',
|
|
||||||
): boolean {
|
|
||||||
if (vendorMode === 'gamble' || isUiItemUnidentified(item)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (item.durability !== undefined && item.durability.max > 0 && item.durability.current <= 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const code = (item.code ?? '').trim().toLowerCase()
|
|
||||||
if ((code === 'tbk' || code === 'ibk') && getTomeQuantity(item) <= 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (item.reqClass !== undefined && player.classCode !== undefined && item.reqClass !== player.classCode) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (item.reqLevel !== undefined && item.reqLevel > 0 && player.level < item.reqLevel) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (item.reqStr !== undefined && item.reqStr > 0 && player.str < item.reqStr) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (item.reqDex !== undefined && item.reqDex > 0 && player.dex < item.reqDex) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ground Truth (D2Client.dll 1.13c @ 0x6fb459f7..0x6fb45b58 `INVENTORY_DrawGridItems`):
|
|
||||||
* Resolves the exact cell background fill style for an item in a grid:
|
|
||||||
* - At `0x6fb45a04..0x6fb45a0c`: Hovering turns the backdrop GREEN (`index 1`) ONLY when `hasCursorItem` is true (`ds:0x6fbcbc28 != 0`).
|
|
||||||
* Hovering with an empty cursor jumps to `0x6fb45ab8` and preserves the item's RED (`index 0`) or BLUE (`index 2`) requirement color.
|
|
||||||
* - In Gamble mode (`vendorMode === 'gamble'`), all items are unidentified mystery items -> RED (`index 0`).
|
|
||||||
* - In Trade mode, items that fail `canPlayerUseUiItem` -> RED (`index 0`), usable items -> BLUE (`index 2`).
|
|
||||||
*/
|
|
||||||
export function resolveGridItemBackgroundFill(
|
|
||||||
item: UiInventoryItem,
|
|
||||||
player: PlayerItemRequirementContext,
|
|
||||||
options?: {
|
|
||||||
readonly isHovered?: boolean | undefined
|
|
||||||
readonly hasCursorItem?: boolean | undefined
|
|
||||||
readonly vendorMode?: 'trade' | 'gamble' | undefined
|
|
||||||
},
|
|
||||||
): string {
|
|
||||||
if (options?.isHovered && options?.hasCursorItem) {
|
|
||||||
return GRID_ITEM_BG_GREEN
|
|
||||||
}
|
|
||||||
return canPlayerUseUiItem(item, player, options?.vendorMode ?? 'trade')
|
|
||||||
? GRID_ITEM_BG_BLUE
|
|
||||||
: GRID_ITEM_BG_RED
|
|
||||||
}
|
|
||||||
|
|
||||||
export function identifyUiItem(item: UiInventoryItem): UiInventoryItem {
|
export function identifyUiItem(item: UiInventoryItem): UiInventoryItem {
|
||||||
;(item as { identified?: boolean; isGambleMystery?: boolean }).identified = true
|
;(item as { identified?: boolean; isGambleMystery?: boolean }).identified = true
|
||||||
;(item as { identified?: boolean; isGambleMystery?: boolean }).isGambleMystery = false
|
;(item as { identified?: boolean; isGambleMystery?: boolean }).isGambleMystery = false
|
||||||
|
|
@ -1211,8 +1124,6 @@ export function identifyUiItem(item: UiInventoryItem): UiInventoryItem {
|
||||||
...(rebuilt.damage !== undefined ? { damage: rebuilt.damage } : {}),
|
...(rebuilt.damage !== undefined ? { damage: rebuilt.damage } : {}),
|
||||||
...(rebuilt.reqLevel !== undefined ? { reqLevel: rebuilt.reqLevel } : {}),
|
...(rebuilt.reqLevel !== undefined ? { reqLevel: rebuilt.reqLevel } : {}),
|
||||||
...(rebuilt.reqStr !== undefined ? { reqStr: rebuilt.reqStr } : {}),
|
...(rebuilt.reqStr !== undefined ? { reqStr: rebuilt.reqStr } : {}),
|
||||||
...(rebuilt.reqDex !== undefined ? { reqDex: rebuilt.reqDex } : {}),
|
|
||||||
...(rebuilt.reqClass !== undefined ? { reqClass: rebuilt.reqClass } : {}),
|
|
||||||
...(rebuilt.durability !== undefined ? { durability: rebuilt.durability } : {}),
|
...(rebuilt.durability !== undefined ? { durability: rebuilt.durability } : {}),
|
||||||
...(rebuilt.sockets !== undefined ? { sockets: rebuilt.sockets } : {}),
|
...(rebuilt.sockets !== undefined ? { sockets: rebuilt.sockets } : {}),
|
||||||
...(rebuilt.invtransform !== undefined ? { invtransform: rebuilt.invtransform } : {}),
|
...(rebuilt.invtransform !== undefined ? { invtransform: rebuilt.invtransform } : {}),
|
||||||
|
|
|
||||||
|
|
@ -24,83 +24,6 @@ import { translateItemName, translateRareName, translateMagicName } from '../gam
|
||||||
import { isRuneCode } from '../game/gems-data.ts'
|
import { isRuneCode } from '../game/gems-data.ts'
|
||||||
import type { D2ColorCode } from './font.ts'
|
import type { D2ColorCode } from './font.ts'
|
||||||
import { isA } from '../game/item-types.ts'
|
import { isA } from '../game/item-types.ts'
|
||||||
import { inferItemClassFromType } from '../game/affixes.ts'
|
|
||||||
import { CHARACTER_CLASS_CODES, type CharacterClassCode } from '../game/classes.ts'
|
|
||||||
|
|
||||||
const VALID_CLASS_CODES = new Set<string>(CHARACTER_CLASS_CODES)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ground Truth (D2Common.dll 1.13c @ 0x6fd74280 `ITEMS_GetClassOfClassSpecificItem` #10202):
|
|
||||||
* Resolves the required playable class (`'ama' | 'sor' | 'nec' | 'pal' | 'bar' | 'dru' | 'ass'`)
|
|
||||||
* for class-specific items from `ItemTypes.txt` `Class` column and type hierarchy (`Equiv1`/`Equiv2`),
|
|
||||||
* or `undefined` if any class can equip/use the item.
|
|
||||||
*/
|
|
||||||
export function resolveItemRequiredClass(
|
|
||||||
base: {
|
|
||||||
readonly type?: string | undefined
|
|
||||||
readonly type2?: string | undefined
|
|
||||||
readonly tags?: readonly string[] | undefined
|
|
||||||
readonly class?: string | undefined
|
|
||||||
readonly reqClass?: string | undefined
|
|
||||||
readonly itemClass?: string | undefined
|
|
||||||
},
|
|
||||||
rawItem?: {
|
|
||||||
readonly reqClass?: string | undefined
|
|
||||||
readonly itemClass?: string | undefined
|
|
||||||
},
|
|
||||||
dropTables?: DropTables,
|
|
||||||
): CharacterClassCode | undefined {
|
|
||||||
const explicit =
|
|
||||||
rawItem?.reqClass ?? rawItem?.itemClass ?? base.reqClass ?? base.itemClass ?? base.class
|
|
||||||
if (typeof explicit === 'string') {
|
|
||||||
const norm = explicit.trim().toLowerCase()
|
|
||||||
if (VALID_CLASS_CODES.has(norm)) return norm as CharacterClassCode
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAPred = (type: string, target: string): boolean => {
|
|
||||||
const t = type.trim().toLowerCase()
|
|
||||||
if (!t) return false
|
|
||||||
if (t === target) return true
|
|
||||||
if (dropTables?.isA) {
|
|
||||||
try {
|
|
||||||
if (dropTables.isA(t, target)) return true
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dropTables?.itemTypes) {
|
|
||||||
try {
|
|
||||||
if (isA(t, target, dropTables.itemTypes)) return true
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const candidateTypes: string[] = []
|
|
||||||
if (base.type) candidateTypes.push(base.type)
|
|
||||||
if (base.type2) candidateTypes.push(base.type2)
|
|
||||||
if (base.tags) candidateTypes.push(...base.tags)
|
|
||||||
|
|
||||||
for (const rawType of candidateTypes) {
|
|
||||||
const typeCode = rawType.trim().toLowerCase()
|
|
||||||
if (!typeCode) continue
|
|
||||||
if (dropTables?.itemTypes) {
|
|
||||||
const def = dropTables.itemTypes.byCode.get(typeCode)
|
|
||||||
const defClass = def?.class?.trim().toLowerCase()
|
|
||||||
if (defClass && VALID_CLASS_CODES.has(defClass)) {
|
|
||||||
return defClass as CharacterClassCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const inferred = inferItemClassFromType(typeCode, isAPred)
|
|
||||||
if (inferred && VALID_CLASS_CODES.has(inferred)) {
|
|
||||||
return inferred as CharacterClassCode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the allowable paperdoll equipment slot(s) for an item base based on
|
* Resolves the allowable paperdoll equipment slot(s) for an item base based on
|
||||||
|
|
@ -573,31 +496,6 @@ export function itemToUiInventoryItem(
|
||||||
const dropSfxFrame = (rawItem as any).dropSfxFrame ?? (rawItem as any).dropsfxframe ?? (base as any).dropsfxframe
|
const dropSfxFrame = (rawItem as any).dropSfxFrame ?? (rawItem as any).dropsfxframe ?? (base as any).dropsfxframe
|
||||||
const permStore = Boolean(rawItem.permStore ?? base.permStoreItem)
|
const permStore = Boolean(rawItem.permStore ?? base.permStoreItem)
|
||||||
|
|
||||||
const reqClass = !isGambleMystery
|
|
||||||
? resolveItemRequiredClass(base as any, rawItem as any, dropTables)
|
|
||||||
: undefined
|
|
||||||
const effectiveReqLevel = !isGambleMystery
|
|
||||||
? Math.max(
|
|
||||||
tt.reqLevel ?? 0,
|
|
||||||
typeof (rawItem as any).reqLevel === 'number' ? (rawItem as any).reqLevel : 0,
|
|
||||||
typeof (base as any).levelreq === 'number' ? (base as any).levelreq : 0,
|
|
||||||
)
|
|
||||||
: 0
|
|
||||||
const effectiveReqStr = !isGambleMystery
|
|
||||||
? Math.max(
|
|
||||||
tt.reqStr ?? 0,
|
|
||||||
typeof (rawItem as any).reqStr === 'number' ? (rawItem as any).reqStr : 0,
|
|
||||||
typeof (base as any).reqstr === 'number' && tt.reqStr === undefined ? (base as any).reqstr : 0,
|
|
||||||
)
|
|
||||||
: 0
|
|
||||||
const effectiveReqDex = !isGambleMystery
|
|
||||||
? Math.max(
|
|
||||||
tt.reqDex ?? 0,
|
|
||||||
typeof (rawItem as any).reqDex === 'number' ? (rawItem as any).reqDex : 0,
|
|
||||||
typeof (base as any).reqdex === 'number' && tt.reqDex === undefined ? (base as any).reqdex : 0,
|
|
||||||
)
|
|
||||||
: 0
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
code,
|
code,
|
||||||
|
|
@ -616,10 +514,16 @@ export function itemToUiInventoryItem(
|
||||||
? { defense: rawItem.defense ?? base.defense }
|
? { defense: rawItem.defense ?? base.defense }
|
||||||
: {}),
|
: {}),
|
||||||
...(damage !== undefined ? { damage } : {}),
|
...(damage !== undefined ? { damage } : {}),
|
||||||
...(effectiveReqLevel > 0 ? { reqLevel: effectiveReqLevel } : {}),
|
...(!isGambleMystery && tt.reqLevel !== undefined
|
||||||
...(effectiveReqStr > 0 ? { reqStr: effectiveReqStr } : {}),
|
? { reqLevel: tt.reqLevel }
|
||||||
...(effectiveReqDex > 0 ? { reqDex: effectiveReqDex } : {}),
|
: !isGambleMystery && (rawItem.level || base.level)
|
||||||
...(reqClass !== undefined ? { reqClass } : {}),
|
? { reqLevel: rawItem.level || base.level }
|
||||||
|
: {}),
|
||||||
|
...(!isGambleMystery && tt.reqStr !== undefined
|
||||||
|
? { reqStr: tt.reqStr }
|
||||||
|
: !isGambleMystery && (base as any).reqstr
|
||||||
|
? { reqStr: (base as any).reqstr }
|
||||||
|
: {}),
|
||||||
stats,
|
stats,
|
||||||
...(!isGambleMystery && invtransform ? { invtransform } : {}),
|
...(!isGambleMystery && invtransform ? { invtransform } : {}),
|
||||||
...(!isGambleMystery && chrtransform ? { chrtransform } : {}),
|
...(!isGambleMystery && chrtransform ? { chrtransform } : {}),
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,7 @@
|
||||||
|
|
||||||
import type { D2ColorCode, D2FontRenderer } from './font.ts'
|
import type { D2ColorCode, D2FontRenderer } from './font.ts'
|
||||||
import {
|
import {
|
||||||
GRID_ITEM_BG_BLUE,
|
|
||||||
GRID_ITEM_BG_GREEN,
|
|
||||||
GRID_ITEM_BG_RED,
|
|
||||||
TOME_MAX_QUANTITY,
|
TOME_MAX_QUANTITY,
|
||||||
canPlayerUseUiItem,
|
|
||||||
findFreeGridSlot,
|
findFreeGridSlot,
|
||||||
getTomeQuantity,
|
getTomeQuantity,
|
||||||
identifyUiItem,
|
identifyUiItem,
|
||||||
|
|
@ -22,16 +18,13 @@ import {
|
||||||
isUiItemUnidentified,
|
isUiItemUnidentified,
|
||||||
isUsableRightClickItem,
|
isUsableRightClickItem,
|
||||||
itemToUiInventoryItem,
|
itemToUiInventoryItem,
|
||||||
resolveGridItemBackgroundFill,
|
|
||||||
resolveItemSpriteRect,
|
resolveItemSpriteRect,
|
||||||
setTomeQuantity,
|
setTomeQuantity,
|
||||||
type EquipSlotId,
|
type EquipSlotId,
|
||||||
type GridPlacement,
|
type GridPlacement,
|
||||||
type InventoryPanel,
|
type InventoryPanel,
|
||||||
type PlayerItemRequirementContext,
|
|
||||||
type UiInventoryItem,
|
type UiInventoryItem,
|
||||||
} from './inventory.ts'
|
} from './inventory.ts'
|
||||||
import type { CharacterClassCode } from '../game/classes.ts'
|
|
||||||
import { BAKED_UI_MANIFEST } from './baked-ui-meta.ts'
|
import { BAKED_UI_MANIFEST } from './baked-ui-meta.ts'
|
||||||
import {
|
import {
|
||||||
type TownNpcServiceDescriptor,
|
type TownNpcServiceDescriptor,
|
||||||
|
|
@ -44,15 +37,6 @@ import { VendorSessionManager } from '../game/vendor-service.ts'
|
||||||
import { getEmbeddedDropTables } from '../game/embedded-drop-tables.ts'
|
import { getEmbeddedDropTables } from '../game/embedded-drop-tables.ts'
|
||||||
import { isItemRepairable } from '../game/item-cost.ts'
|
import { isItemRepairable } from '../game/item-cost.ts'
|
||||||
|
|
||||||
export {
|
|
||||||
GRID_ITEM_BG_BLUE,
|
|
||||||
GRID_ITEM_BG_GREEN,
|
|
||||||
GRID_ITEM_BG_RED,
|
|
||||||
canPlayerUseUiItem,
|
|
||||||
resolveGridItemBackgroundFill,
|
|
||||||
type PlayerItemRequirementContext,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LeftDockPanelKind = 'none' | 'char' | 'quest' | 'waypoint' | 'stash' | 'cube' | 'vendor'
|
export type LeftDockPanelKind = 'none' | 'char' | 'quest' | 'waypoint' | 'stash' | 'cube' | 'vendor'
|
||||||
|
|
||||||
export const STASH_PANEL_ORIGIN = { x: 80, y: 60, width: 320, height: 432, w: 320, h: 432 } as const
|
export const STASH_PANEL_ORIGIN = { x: 80, y: 60, width: 320, height: 432, w: 320, h: 432 } as const
|
||||||
|
|
@ -578,12 +562,6 @@ export class WorldPanelsHud {
|
||||||
activeVendorTab: VendorTabId = 'armor'
|
activeVendorTab: VendorTabId = 'armor'
|
||||||
vendorDifficulty: 0 | 1 | 2 = 0
|
vendorDifficulty: 0 | 1 | 2 = 0
|
||||||
vendorCharLevel = 85
|
vendorCharLevel = 85
|
||||||
vendorPlayerStats: PlayerItemRequirementContext = {
|
|
||||||
level: 85,
|
|
||||||
str: 156,
|
|
||||||
dex: 75,
|
|
||||||
classCode: 'sor',
|
|
||||||
}
|
|
||||||
vendorTradeState: VendorTradeState = 'idle'
|
vendorTradeState: VendorTradeState = 'idle'
|
||||||
hoveredVendorButtonSlot: 0 | 1 | 2 | 3 | null = null
|
hoveredVendorButtonSlot: 0 | 1 | 2 | 3 | null = null
|
||||||
pressedMomentarySlot: 0 | 1 | 2 | 3 | null = null
|
pressedMomentarySlot: 0 | 1 | 2 | 3 | null = null
|
||||||
|
|
@ -598,28 +576,6 @@ export class WorldPanelsHud {
|
||||||
}
|
}
|
||||||
gamblePlacements: GridPlacement[] = []
|
gamblePlacements: GridPlacement[] = []
|
||||||
|
|
||||||
setPlayerContext(ctx: Partial<PlayerItemRequirementContext>): void {
|
|
||||||
const nextLevel = ctx.level !== undefined ? Math.max(1, ctx.level) : this.vendorCharLevel
|
|
||||||
this.vendorCharLevel = nextLevel
|
|
||||||
this.vendorPlayerStats = {
|
|
||||||
level: nextLevel,
|
|
||||||
str: ctx.str !== undefined ? Math.max(0, ctx.str) : this.vendorPlayerStats.str,
|
|
||||||
dex: ctx.dex !== undefined ? Math.max(0, ctx.dex) : this.vendorPlayerStats.dex,
|
|
||||||
classCode: ctx.classCode ?? this.vendorPlayerStats.classCode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getEffectivePlayerContext(): PlayerItemRequirementContext {
|
|
||||||
return {
|
|
||||||
...this.vendorPlayerStats,
|
|
||||||
level: this.vendorCharLevel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
canPlayerUseVendorItem(item: UiInventoryItem): boolean {
|
|
||||||
return canPlayerUseUiItem(item, this.getEffectivePlayerContext(), this.vendorMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
get repairCursorMode(): boolean {
|
get repairCursorMode(): boolean {
|
||||||
return this.vendorTradeState === 'repair'
|
return this.vendorTradeState === 'repair'
|
||||||
}
|
}
|
||||||
|
|
@ -2396,11 +2352,9 @@ export class WorldPanelsHud {
|
||||||
// 1. D2Client.dll `0x6fb45c8a–0x6fb45d63`: `buysell.dc6` 320x432 stitched background
|
// 1. D2Client.dll `0x6fb45c8a–0x6fb45d63`: `buysell.dc6` 320x432 stitched background
|
||||||
ctx.drawImage(assets.vendorBgImg, ox, oy)
|
ctx.drawImage(assets.vendorBgImg, ox, oy)
|
||||||
|
|
||||||
// 2. D2Client.dll `0x6fb458a0..0x6fb45b58`: Items on the active store page (`Inventory.txt` Monster2 grid at `ox+16, oy+63`)
|
// 2. D2Client.dll `0x6fb458a0`: Items on the active store page (`Inventory.txt` Monster2 grid at `ox+16, oy+63`)
|
||||||
const atlas = assets.itemsAtlasImg ?? null
|
const atlas = assets.itemsAtlasImg ?? null
|
||||||
const placements = this.getActiveVendorPlacements()
|
const placements = this.getActiveVendorPlacements()
|
||||||
const playerCtx = this.getEffectivePlayerContext()
|
|
||||||
const hasCursorItem = Boolean((assets.inventory ?? this.activePlayerInventory)?.cursorItem)
|
|
||||||
for (const placed of placements) {
|
for (const placed of placements) {
|
||||||
const gx = VENDOR_GRID_ORIGIN.x + placed.col * VENDOR_GRID_ORIGIN.cellPx
|
const gx = VENDOR_GRID_ORIGIN.x + placed.col * VENDOR_GRID_ORIGIN.cellPx
|
||||||
const gy = VENDOR_GRID_ORIGIN.y + placed.row * VENDOR_GRID_ORIGIN.cellPx
|
const gy = VENDOR_GRID_ORIGIN.y + placed.row * VENDOR_GRID_ORIGIN.cellPx
|
||||||
|
|
@ -2408,11 +2362,11 @@ export class WorldPanelsHud {
|
||||||
const gh = placed.item.invHeight * VENDOR_GRID_ORIGIN.cellPx
|
const gh = placed.item.invHeight * VENDOR_GRID_ORIGIN.cellPx
|
||||||
const isHovered = this.hoveredVendorItem?.item.id === placed.item.id
|
const isHovered = this.hoveredVendorItem?.item.id === placed.item.id
|
||||||
|
|
||||||
ctx.fillStyle = resolveGridItemBackgroundFill(placed.item, playerCtx, {
|
ctx.fillStyle = isHovered
|
||||||
isHovered,
|
? 'rgba(46, 68, 36, 0.55)'
|
||||||
hasCursorItem,
|
: placed.item.quality === 'magic'
|
||||||
vendorMode: this.vendorMode,
|
? 'rgba(20, 34, 62, 0.42)'
|
||||||
})
|
: 'rgba(28, 24, 18, 0.42)'
|
||||||
ctx.fillRect(gx + 1, gy + 1, gw - 2, gh - 2)
|
ctx.fillRect(gx + 1, gy + 1, gw - 2, gh - 2)
|
||||||
|
|
||||||
const rect = resolveItemSpriteRect(placed.item, BAKED_UI_MANIFEST.itemRects)
|
const rect = resolveItemSpriteRect(placed.item, BAKED_UI_MANIFEST.itemRects)
|
||||||
|
|
@ -2769,39 +2723,8 @@ export class WorldPanelsHud {
|
||||||
if (item.speedText) {
|
if (item.speedText) {
|
||||||
lines.push({ text: item.speedText, color: 'white', font: 'font8' })
|
lines.push({ text: item.speedText, color: 'white', font: 'font8' })
|
||||||
}
|
}
|
||||||
const playerCtx = this.getEffectivePlayerContext()
|
|
||||||
if (!unid && item.reqClass !== undefined) {
|
|
||||||
const classLabelMap: Record<CharacterClassCode, string> = {
|
|
||||||
ama: '(限亚马逊使用)',
|
|
||||||
sor: '(限法师使用)',
|
|
||||||
nec: '(限死灵法师使用)',
|
|
||||||
pal: '(限圣骑士使用)',
|
|
||||||
bar: '(限野蛮人使用)',
|
|
||||||
dru: '(限德鲁伊使用)',
|
|
||||||
ass: '(限刺客使用)',
|
|
||||||
}
|
|
||||||
const classText = classLabelMap[item.reqClass]
|
|
||||||
if (classText) {
|
|
||||||
lines.push({
|
|
||||||
text: classText,
|
|
||||||
color: playerCtx.classCode && playerCtx.classCode !== item.reqClass ? 'red' : 'white',
|
|
||||||
font: 'font8',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (item.reqDex !== undefined && item.reqDex > 0) {
|
|
||||||
lines.push({
|
|
||||||
text: '需要敏捷: ' + item.reqDex,
|
|
||||||
color: playerCtx.dex < item.reqDex ? 'red' : 'white',
|
|
||||||
font: 'font8',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (item.reqStr !== undefined) {
|
if (item.reqStr !== undefined) {
|
||||||
lines.push({
|
lines.push({ text: '需要力量: ' + item.reqStr, color: 'white', font: 'font8' })
|
||||||
text: '需要力量: ' + item.reqStr,
|
|
||||||
color: playerCtx.str < item.reqStr ? 'red' : 'white',
|
|
||||||
font: 'font8',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const displayReqLevel = unid
|
const displayReqLevel = unid
|
||||||
? item.rawItem?.base?.level && item.rawItem.base.level > 1
|
? item.rawItem?.base?.level && item.rawItem.base.level > 1
|
||||||
|
|
@ -2809,11 +2732,7 @@ export class WorldPanelsHud {
|
||||||
: undefined
|
: undefined
|
||||||
: item.reqLevel
|
: item.reqLevel
|
||||||
if (displayReqLevel !== undefined) {
|
if (displayReqLevel !== undefined) {
|
||||||
lines.push({
|
lines.push({ text: '需要等级: ' + displayReqLevel, color: 'white', font: 'font8' })
|
||||||
text: '需要等级: ' + displayReqLevel,
|
|
||||||
color: playerCtx.level < displayReqLevel ? 'red' : 'white',
|
|
||||||
font: 'font8',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unid) {
|
if (unid) {
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,9 @@ import { fileSource } from '../src/mpq/file-source.ts'
|
||||||
import { MountedArchives } from '../src/mpq/mount.ts'
|
import { MountedArchives } from '../src/mpq/mount.ts'
|
||||||
import { BAKED_UI_MANIFEST } from '../src/ui/baked-ui-meta.ts'
|
import { BAKED_UI_MANIFEST } from '../src/ui/baked-ui-meta.ts'
|
||||||
import type { D2ColorCode, D2FontRenderer, D2FontName } from '../src/ui/font.ts'
|
import type { D2ColorCode, D2FontRenderer, D2FontName } from '../src/ui/font.ts'
|
||||||
import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts'
|
import { InventoryPanel, type UiInventoryItem } from '../src/ui/inventory.ts'
|
||||||
import { InventoryPanel, itemToUiInventoryItem, type UiInventoryItem } from '../src/ui/inventory.ts'
|
|
||||||
import {
|
import {
|
||||||
BLACKSMITH_CLASS_IDS,
|
BLACKSMITH_CLASS_IDS,
|
||||||
GRID_ITEM_BG_BLUE,
|
|
||||||
GRID_ITEM_BG_GREEN,
|
|
||||||
GRID_ITEM_BG_RED,
|
|
||||||
TRADER_CLASS_IDS,
|
TRADER_CLASS_IDS,
|
||||||
VENDOR_BUTTON_DRAW_Y,
|
VENDOR_BUTTON_DRAW_Y,
|
||||||
VENDOR_BUTTON_HIT_W,
|
VENDOR_BUTTON_HIT_W,
|
||||||
|
|
@ -102,7 +98,6 @@ interface FontCall {
|
||||||
function createMockCanvasAndFont() {
|
function createMockCanvasAndFont() {
|
||||||
const drawImageCalls: DrawImageCall[] = []
|
const drawImageCalls: DrawImageCall[] = []
|
||||||
const fillRectCalls: number[][] = []
|
const fillRectCalls: number[][] = []
|
||||||
const fillRectRecords: Array<{ fillStyle: string; args: number[] }> = []
|
|
||||||
const strokeRectCalls: number[][] = []
|
const strokeRectCalls: number[][] = []
|
||||||
const fontCalls: FontCall[] = []
|
const fontCalls: FontCall[] = []
|
||||||
|
|
||||||
|
|
@ -114,7 +109,6 @@ function createMockCanvasAndFont() {
|
||||||
}),
|
}),
|
||||||
fillRect: vi.fn((...nums: number[]) => {
|
fillRect: vi.fn((...nums: number[]) => {
|
||||||
fillRectCalls.push(nums)
|
fillRectCalls.push(nums)
|
||||||
fillRectRecords.push({ fillStyle: String(ctx.fillStyle), args: nums })
|
|
||||||
}),
|
}),
|
||||||
strokeRect: vi.fn((...nums: number[]) => {
|
strokeRect: vi.fn((...nums: number[]) => {
|
||||||
strokeRectCalls.push(nums)
|
strokeRectCalls.push(nums)
|
||||||
|
|
@ -181,7 +175,7 @@ function createMockCanvasAndFont() {
|
||||||
),
|
),
|
||||||
} as unknown as D2FontRenderer
|
} as unknown as D2FontRenderer
|
||||||
|
|
||||||
return { ctx, font, drawImageCalls, fillRectCalls, fillRectRecords, strokeRectCalls, fontCalls }
|
return { ctx, font, drawImageCalls, fillRectCalls, strokeRectCalls, fontCalls }
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Diablo II 1.13c Vendor / BuySell Panel UI Parity', () => {
|
describe('Diablo II 1.13c Vendor / BuySell Panel UI Parity', () => {
|
||||||
|
|
@ -648,238 +642,5 @@ describe('Diablo II 1.13c Vendor / BuySell Panel UI Parity', () => {
|
||||||
expect(openedVendorMode).toBe('trade')
|
expect(openedVendorMode).toBe('trade')
|
||||||
expect(wp.npcMenu).toBeNull()
|
expect(wp.npcMenu).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('draws Vendor Trade items with BLUE background when usable and RED background when failing class, level, str, or dex requirements (D2Client.dll 0x6fb458a0..0x6fb45b58 & D2Common #10244)', () => {
|
|
||||||
const tables = getEmbeddedDropTables()
|
|
||||||
const wp = new WorldPanelsHud()
|
|
||||||
const inv = new InventoryPanel()
|
|
||||||
wp.activePlayerInventory = inv
|
|
||||||
expect(wp.openVendorForNpc('Charsi', 'trade', 10, 0)).toBe(true)
|
|
||||||
|
|
||||||
// Set player context: Level 10 Sorceress with 25 Str, 25 Dex
|
|
||||||
wp.setPlayerContext({
|
|
||||||
level: 10,
|
|
||||||
str: 25,
|
|
||||||
dex: 25,
|
|
||||||
classCode: 'sor',
|
|
||||||
})
|
|
||||||
|
|
||||||
// 1. Usable Normal Cap (levelreq=0, reqstr=0, high ilvl=12 must NOT falsely set reqLevel=12!)
|
|
||||||
const capBase = tables.getBase('cap')!
|
|
||||||
const usableNormalCap = itemToUiInventoryItem(
|
|
||||||
{
|
|
||||||
base: capBase,
|
|
||||||
code: 'cap',
|
|
||||||
name: 'Cap',
|
|
||||||
level: 12,
|
|
||||||
ilvl: 12,
|
|
||||||
quality: 2,
|
|
||||||
rarity: 'normal',
|
|
||||||
identified: true,
|
|
||||||
invWidth: capBase.invWidth,
|
|
||||||
invHeight: capBase.invHeight,
|
|
||||||
stack: 1,
|
|
||||||
value: 10,
|
|
||||||
stats: {},
|
|
||||||
} as any,
|
|
||||||
tables,
|
|
||||||
)
|
|
||||||
expect(usableNormalCap.reqLevel).toBeUndefined()
|
|
||||||
|
|
||||||
// 2. Usable Magic Leather Armor (reqstr=15 <= 25, reqLevel=3 <= 10)
|
|
||||||
const leaBase = tables.getBase('lea')!
|
|
||||||
const usableMagicArmor = itemToUiInventoryItem(
|
|
||||||
{
|
|
||||||
base: leaBase,
|
|
||||||
code: 'lea',
|
|
||||||
name: 'Sturdy Leather Armor',
|
|
||||||
level: 12,
|
|
||||||
ilvl: 12,
|
|
||||||
quality: 4,
|
|
||||||
rarity: 'magic',
|
|
||||||
identified: true,
|
|
||||||
reqLevel: 3,
|
|
||||||
invWidth: leaBase.invWidth,
|
|
||||||
invHeight: leaBase.invHeight,
|
|
||||||
stack: 1,
|
|
||||||
value: 50,
|
|
||||||
stats: {},
|
|
||||||
} as any,
|
|
||||||
tables,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 3. Unusable due to Class Restriction: Assassin Katar ('ktr', type 'h2h' -> reqClass 'ass' !== 'sor')
|
|
||||||
const ktrBase = tables.getBase('ktr')!
|
|
||||||
const classRestrictedKatar = itemToUiInventoryItem(
|
|
||||||
{
|
|
||||||
base: ktrBase,
|
|
||||||
code: 'ktr',
|
|
||||||
name: 'Katar',
|
|
||||||
level: 12,
|
|
||||||
ilvl: 12,
|
|
||||||
quality: 2,
|
|
||||||
rarity: 'normal',
|
|
||||||
identified: true,
|
|
||||||
invWidth: ktrBase.invWidth,
|
|
||||||
invHeight: ktrBase.invHeight,
|
|
||||||
stack: 1,
|
|
||||||
value: 40,
|
|
||||||
stats: {},
|
|
||||||
} as any,
|
|
||||||
tables,
|
|
||||||
)
|
|
||||||
expect(classRestrictedKatar.reqClass).toBe('ass')
|
|
||||||
|
|
||||||
// 4. Unusable due to Strength Requirement: Full Plate Mail ('ful', reqstr=80 > 25)
|
|
||||||
const fulBase = tables.getBase('ful')!
|
|
||||||
const highStrArmor = itemToUiInventoryItem(
|
|
||||||
{
|
|
||||||
base: fulBase,
|
|
||||||
code: 'ful',
|
|
||||||
name: 'Full Plate Mail',
|
|
||||||
level: 12,
|
|
||||||
ilvl: 12,
|
|
||||||
quality: 2,
|
|
||||||
rarity: 'normal',
|
|
||||||
identified: true,
|
|
||||||
invWidth: fulBase.invWidth,
|
|
||||||
invHeight: fulBase.invHeight,
|
|
||||||
stack: 1,
|
|
||||||
value: 500,
|
|
||||||
stats: {},
|
|
||||||
} as any,
|
|
||||||
tables,
|
|
||||||
)
|
|
||||||
expect(highStrArmor.reqStr).toBe(80)
|
|
||||||
|
|
||||||
// 5. Unusable due to Dexterity Requirement: Long Bow ('lbw', reqdex=19, overridden to reqDex=40 > 25)
|
|
||||||
const lbwBase = tables.getBase('lbw')!
|
|
||||||
const highDexBow = itemToUiInventoryItem(
|
|
||||||
{
|
|
||||||
base: lbwBase,
|
|
||||||
code: 'lbw',
|
|
||||||
name: 'Long Bow',
|
|
||||||
level: 12,
|
|
||||||
ilvl: 12,
|
|
||||||
quality: 2,
|
|
||||||
rarity: 'normal',
|
|
||||||
identified: true,
|
|
||||||
reqDex: 40,
|
|
||||||
invWidth: lbwBase.invWidth,
|
|
||||||
invHeight: lbwBase.invHeight,
|
|
||||||
stack: 1,
|
|
||||||
value: 200,
|
|
||||||
stats: {},
|
|
||||||
} as any,
|
|
||||||
tables,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 6. Unusable due to Level Requirement: Magic Ring requiring level 25 > 10
|
|
||||||
const rinBase = tables.getBase('rin')!
|
|
||||||
const highLevelRing = itemToUiInventoryItem(
|
|
||||||
{
|
|
||||||
base: rinBase,
|
|
||||||
code: 'rin',
|
|
||||||
name: 'Fortuitous Ring',
|
|
||||||
level: 30,
|
|
||||||
ilvl: 30,
|
|
||||||
quality: 4,
|
|
||||||
rarity: 'magic',
|
|
||||||
identified: true,
|
|
||||||
reqLevel: 25,
|
|
||||||
invWidth: rinBase.invWidth,
|
|
||||||
invHeight: rinBase.invHeight,
|
|
||||||
stack: 1,
|
|
||||||
value: 1000,
|
|
||||||
stats: {},
|
|
||||||
} as any,
|
|
||||||
tables,
|
|
||||||
)
|
|
||||||
|
|
||||||
wp.vendorTabPlacements.armor = [
|
|
||||||
{ item: usableNormalCap, col: 0, row: 0 },
|
|
||||||
{ item: usableMagicArmor, col: 2, row: 0 },
|
|
||||||
{ item: classRestrictedKatar, col: 4, row: 0 },
|
|
||||||
{ item: highStrArmor, col: 5, row: 0 },
|
|
||||||
{ item: highDexBow, col: 7, row: 0 },
|
|
||||||
{ item: highLevelRing, col: 9, row: 0 },
|
|
||||||
]
|
|
||||||
wp.activeVendorTab = 'armor'
|
|
||||||
|
|
||||||
// Hover over the usable normal cap WITHOUT holding a cursorItem:
|
|
||||||
// Per D2Client.dll 0x6fb45a04..0x6fb45a0c, hovering with empty cursor must NOT turn green!
|
|
||||||
wp.hoveredVendorItem = { item: usableNormalCap, x: 100, y: 130, col: 0, row: 0 }
|
|
||||||
inv.cursorItem = null
|
|
||||||
|
|
||||||
const { ctx, font, fillRectRecords } = createMockCanvasAndFont()
|
|
||||||
wp.drawLeftDockPanel(ctx, 'vendor', { ...createFakeVendorAssets(), inventory: inv }, font)
|
|
||||||
|
|
||||||
expect(fillRectRecords.map(r => r.fillStyle)).toEqual([
|
|
||||||
GRID_ITEM_BG_BLUE, // usableNormalCap (normal quality, usable -> BLUE, never brown!)
|
|
||||||
GRID_ITEM_BG_BLUE, // usableMagicArmor (magic quality, usable -> BLUE)
|
|
||||||
GRID_ITEM_BG_RED, // classRestrictedKatar ('ass' !== 'sor' -> RED)
|
|
||||||
GRID_ITEM_BG_RED, // highStrArmor (reqStr 80 > 25 -> RED)
|
|
||||||
GRID_ITEM_BG_RED, // highDexBow (reqDex 40 > 25 -> RED)
|
|
||||||
GRID_ITEM_BG_RED, // highLevelRing (reqLevel 25 > 10 -> RED)
|
|
||||||
])
|
|
||||||
|
|
||||||
// When holding a cursorItem and hovering over an item, its cell backdrop turns GREEN per 0x6fb45a04
|
|
||||||
inv.cursorItem = usableNormalCap
|
|
||||||
const { ctx: ctx2, font: font2, fillRectRecords: recordsWithCursor } = createMockCanvasAndFont()
|
|
||||||
wp.drawLeftDockPanel(ctx2, 'vendor', { ...createFakeVendorAssets(), inventory: inv }, font2)
|
|
||||||
expect(recordsWithCursor[0]!.fillStyle).toBe(GRID_ITEM_BG_GREEN)
|
|
||||||
|
|
||||||
// When switching to level 30 Assassin with 100 Str and 100 Dex, all 6 items become usable -> ALL BLUE!
|
|
||||||
inv.cursorItem = null
|
|
||||||
wp.hoveredVendorItem = null
|
|
||||||
wp.setPlayerContext({
|
|
||||||
level: 30,
|
|
||||||
str: 100,
|
|
||||||
dex: 100,
|
|
||||||
classCode: 'ass',
|
|
||||||
})
|
|
||||||
const { ctx: ctx3, font: font3, fillRectRecords: recordsAllMet } = createMockCanvasAndFont()
|
|
||||||
wp.drawLeftDockPanel(ctx3, 'vendor', { ...createFakeVendorAssets(), inventory: inv }, font3)
|
|
||||||
expect(recordsAllMet.map(r => r.fillStyle)).toEqual([
|
|
||||||
GRID_ITEM_BG_BLUE,
|
|
||||||
GRID_ITEM_BG_BLUE,
|
|
||||||
GRID_ITEM_BG_BLUE,
|
|
||||||
GRID_ITEM_BG_BLUE,
|
|
||||||
GRID_ITEM_BG_BLUE,
|
|
||||||
GRID_ITEM_BG_BLUE,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it('draws ALL items in Gamble mode with RED background regardless of player level, stats, or class (D2Common.dll 0x6fd76e23 / 0x6fd77006)', () => {
|
|
||||||
const wp = new WorldPanelsHud()
|
|
||||||
const inv = new InventoryPanel()
|
|
||||||
wp.activePlayerInventory = inv
|
|
||||||
wp.setPlayerContext({
|
|
||||||
level: 99,
|
|
||||||
str: 500,
|
|
||||||
dex: 500,
|
|
||||||
classCode: 'sor',
|
|
||||||
})
|
|
||||||
expect(wp.openVendorForNpc('Gheed', 'gamble', 99, 0)).toBe(true)
|
|
||||||
expect(wp.gamblePlacements.length).toBeGreaterThan(0)
|
|
||||||
|
|
||||||
// Even when hovering a gamble item with an empty cursor, every gamble item background must be RED
|
|
||||||
wp.hoveredVendorItem = {
|
|
||||||
item: wp.gamblePlacements[0]!.item,
|
|
||||||
x: 100,
|
|
||||||
y: 130,
|
|
||||||
col: wp.gamblePlacements[0]!.col,
|
|
||||||
row: wp.gamblePlacements[0]!.row,
|
|
||||||
}
|
|
||||||
|
|
||||||
const { ctx, font, fillRectRecords } = createMockCanvasAndFont()
|
|
||||||
wp.drawLeftDockPanel(ctx, 'vendor', { ...createFakeVendorAssets(), inventory: inv }, font)
|
|
||||||
|
|
||||||
expect(fillRectRecords).toHaveLength(wp.gamblePlacements.length)
|
|
||||||
for (const rec of fillRectRecords) {
|
|
||||||
expect(rec.fillStyle).toBe(GRID_ITEM_BG_RED)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue