Merge branch 'fix/vendor-item-bg-color' into main
This commit is contained in:
commit
ffda82d650
|
|
@ -505,6 +505,12 @@ 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,
|
||||||
|
|
@ -1106,6 +1112,12 @@ 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,6 +14,7 @@
|
||||||
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'
|
||||||
|
|
||||||
|
|
@ -51,6 +52,8 @@ 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
|
||||||
|
|
@ -1091,6 +1094,90 @@ 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
|
||||||
|
|
@ -1124,6 +1211,8 @@ 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,6 +24,83 @@ 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
|
||||||
|
|
@ -496,6 +573,31 @@ 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,
|
||||||
|
|
@ -514,16 +616,10 @@ export function itemToUiInventoryItem(
|
||||||
? { defense: rawItem.defense ?? base.defense }
|
? { defense: rawItem.defense ?? base.defense }
|
||||||
: {}),
|
: {}),
|
||||||
...(damage !== undefined ? { damage } : {}),
|
...(damage !== undefined ? { damage } : {}),
|
||||||
...(!isGambleMystery && tt.reqLevel !== undefined
|
...(effectiveReqLevel > 0 ? { reqLevel: effectiveReqLevel } : {}),
|
||||||
? { reqLevel: tt.reqLevel }
|
...(effectiveReqStr > 0 ? { reqStr: effectiveReqStr } : {}),
|
||||||
: !isGambleMystery && (rawItem.level || base.level)
|
...(effectiveReqDex > 0 ? { reqDex: effectiveReqDex } : {}),
|
||||||
? { reqLevel: rawItem.level || base.level }
|
...(reqClass !== undefined ? { reqClass } : {}),
|
||||||
: {}),
|
|
||||||
...(!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,7 +10,11 @@
|
||||||
|
|
||||||
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,
|
||||||
|
|
@ -18,13 +22,16 @@ 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,
|
||||||
|
|
@ -37,6 +44,15 @@ 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
|
||||||
|
|
@ -562,6 +578,12 @@ 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
|
||||||
|
|
@ -576,6 +598,28 @@ 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'
|
||||||
}
|
}
|
||||||
|
|
@ -2352,9 +2396,11 @@ 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`: Items on the active store page (`Inventory.txt` Monster2 grid at `ox+16, oy+63`)
|
// 2. D2Client.dll `0x6fb458a0..0x6fb45b58`: 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
|
||||||
|
|
@ -2362,11 +2408,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 = isHovered
|
ctx.fillStyle = resolveGridItemBackgroundFill(placed.item, playerCtx, {
|
||||||
? 'rgba(46, 68, 36, 0.55)'
|
isHovered,
|
||||||
: placed.item.quality === 'magic'
|
hasCursorItem,
|
||||||
? 'rgba(20, 34, 62, 0.42)'
|
vendorMode: this.vendorMode,
|
||||||
: '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)
|
||||||
|
|
@ -2723,8 +2769,39 @@ 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({ text: '需要力量: ' + item.reqStr, color: 'white', font: 'font8' })
|
lines.push({
|
||||||
|
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
|
||||||
|
|
@ -2732,7 +2809,11 @@ export class WorldPanelsHud {
|
||||||
: undefined
|
: undefined
|
||||||
: item.reqLevel
|
: item.reqLevel
|
||||||
if (displayReqLevel !== undefined) {
|
if (displayReqLevel !== undefined) {
|
||||||
lines.push({ text: '需要等级: ' + displayReqLevel, color: 'white', font: 'font8' })
|
lines.push({
|
||||||
|
text: '需要等级: ' + displayReqLevel,
|
||||||
|
color: playerCtx.level < displayReqLevel ? 'red' : 'white',
|
||||||
|
font: 'font8',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unid) {
|
if (unid) {
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,13 @@ 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 { InventoryPanel, type UiInventoryItem } from '../src/ui/inventory.ts'
|
import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.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,
|
||||||
|
|
@ -98,6 +102,7 @@ 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[] = []
|
||||||
|
|
||||||
|
|
@ -109,6 +114,7 @@ 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)
|
||||||
|
|
@ -175,7 +181,7 @@ function createMockCanvasAndFont() {
|
||||||
),
|
),
|
||||||
} as unknown as D2FontRenderer
|
} as unknown as D2FontRenderer
|
||||||
|
|
||||||
return { ctx, font, drawImageCalls, fillRectCalls, strokeRectCalls, fontCalls }
|
return { ctx, font, drawImageCalls, fillRectCalls, fillRectRecords, strokeRectCalls, fontCalls }
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Diablo II 1.13c Vendor / BuySell Panel UI Parity', () => {
|
describe('Diablo II 1.13c Vendor / BuySell Panel UI Parity', () => {
|
||||||
|
|
@ -642,5 +648,238 @@ 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