fix(ground): hover ground item reveals label & fix grand charm flippy sprite (Fixes #447, Fixes #452)
- Hover ground item icon reveals authentic name label without needing Alt key (Fixes #447) - Connect SceneMouseController pointermove/pointerleave with GroundLabelOverlay hover callback and ladder layout rendering - Correct Grand Charm (cm3) and Gheed's Fortune ground flippy sprite mapping to flpchm3 (Fixes #452) - Correct Small Charm (cm1) and Large Charm (cm2) default flippy mappings to flpchm1 and flpchm2 - Add 1.13c charm ground flippy resolution tier and Chinese semantic fallbacks in resolveGroundItemSpriteRect - Add comprehensive vitest suites in tests/charm-ground-flippy-parity.test.ts and tests/ground-items-alt-labels.test.ts
This commit is contained in:
parent
af2671c14a
commit
e3a7918a16
|
|
@ -736,11 +736,11 @@ async function main(): Promise<void> {
|
||||||
codeToFlippyFile['gold'] = 'flpgld'
|
codeToFlippyFile['gold'] = 'flpgld'
|
||||||
codeToFlippyFile['box'] = 'flpbox'
|
codeToFlippyFile['box'] = 'flpbox'
|
||||||
codeToFlippyFile['Horadric Cube'] = 'flpbox'
|
codeToFlippyFile['Horadric Cube'] = 'flpbox'
|
||||||
codeToFlippyFile['cm1'] = 'flpmss'
|
codeToFlippyFile['cm1'] = 'flpchm1'
|
||||||
codeToFlippyFile['cm2'] = 'flptrch'
|
codeToFlippyFile['cm2'] = 'flpchm2'
|
||||||
codeToFlippyFile['cm3'] = 'flpmss'
|
codeToFlippyFile['cm3'] = 'flpchm3'
|
||||||
codeToFlippyFile['gheeds'] = 'flpmss'
|
codeToFlippyFile['gheeds'] = 'flpchm3'
|
||||||
codeToFlippyFile["Gheed's Fortune"] = 'flpmss'
|
codeToFlippyFile["Gheed's Fortune"] = 'flpchm3'
|
||||||
codeToFlippyFile['anni'] = 'flpmss'
|
codeToFlippyFile['anni'] = 'flpmss'
|
||||||
codeToFlippyFile['Annihilus'] = 'flpmss'
|
codeToFlippyFile['Annihilus'] = 'flpmss'
|
||||||
codeToFlippyFile['torch'] = 'flptrch'
|
codeToFlippyFile['torch'] = 'flptrch'
|
||||||
|
|
|
||||||
|
|
@ -2896,6 +2896,7 @@ export class SceneMouseController {
|
||||||
isLeftMouseDown = false
|
isLeftMouseDown = false
|
||||||
isRightMouseDown = false
|
isRightMouseDown = false
|
||||||
lastClientPos: { x: number; y: number } | null = null
|
lastClientPos: { x: number; y: number } | null = null
|
||||||
|
hoveredGroundItem: GroundItemEntity | null = null
|
||||||
|
|
||||||
private stuckTicks = 0
|
private stuckTicks = 0
|
||||||
private lastDist = -1
|
private lastDist = -1
|
||||||
|
|
@ -2948,6 +2949,9 @@ export class SceneMouseController {
|
||||||
if (dist <= 48) {
|
if (dist <= 48) {
|
||||||
this.navTarget = null
|
this.navTarget = null
|
||||||
this.pendingInteraction = null
|
this.pendingInteraction = null
|
||||||
|
if (this.hoveredGroundItem?.id === item.id) {
|
||||||
|
this.hoveredGroundItem = null
|
||||||
|
}
|
||||||
player.facing = facingOf(item.x - player.x, item.y - player.y)
|
player.facing = facingOf(item.x - player.x, item.y - player.y)
|
||||||
|
|
||||||
if (item.isGold) {
|
if (item.isGold) {
|
||||||
|
|
@ -3159,22 +3163,32 @@ export class SceneMouseController {
|
||||||
if (this.hudManager !== null) {
|
if (this.hudManager !== null) {
|
||||||
const logical = this.hudManager.clientToLogical(e.clientX, e.clientY)
|
const logical = this.hudManager.clientToLogical(e.clientX, e.clientY)
|
||||||
if (this.hudManager.isPointInterceptedByHud(logical.x, logical.y)) {
|
if (this.hudManager.isPointInterceptedByHud(logical.x, logical.y)) {
|
||||||
|
this.hoveredGroundItem = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isLeftMouseDown && !this.input.shiftHeld && this.activeClickType === 'ground') {
|
|
||||||
const { x: worldX, y: worldY } = this.getPointerWorldCoords(e.clientX, e.clientY)
|
const { x: worldX, y: worldY } = this.getPointerWorldCoords(e.clientX, e.clientY)
|
||||||
|
this.hoveredGroundItem = this.engine.groundItems.findAtPoint(worldX, worldY, 32) ?? null
|
||||||
|
|
||||||
|
if (this.isLeftMouseDown && !this.input.shiftHeld && this.activeClickType === 'ground') {
|
||||||
this.navTarget = { x: worldX, y: worldY }
|
this.navTarget = { x: worldX, y: worldY }
|
||||||
} else if (this.isRightMouseDown) {
|
} else if (this.isRightMouseDown) {
|
||||||
const rightSkillId = this.hudManager ? this.hudManager.hotkeys.rightSkillId : 64
|
const rightSkillId = this.hudManager ? this.hudManager.hotkeys.rightSkillId : 64
|
||||||
if (isPassiveSkill(rightSkillId) || isAuraSkill(rightSkillId) || (rightSkillId === 0 && !this.input.shiftHeld && this.activeClickType === 'ground')) {
|
if (isPassiveSkill(rightSkillId) || isAuraSkill(rightSkillId) || (rightSkillId === 0 && !this.input.shiftHeld && this.activeClickType === 'ground')) {
|
||||||
const { x: worldX, y: worldY } = this.getPointerWorldCoords(e.clientX, e.clientY)
|
|
||||||
this.navTarget = { x: worldX, y: worldY }
|
this.navTarget = { x: worldX, y: worldY }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly onPointerLeave = (e: PointerEvent): void => {
|
||||||
|
const target = e.relatedTarget as HTMLElement | null
|
||||||
|
if (target && target.closest?.('.d2-ground-label')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.hoveredGroundItem = null
|
||||||
|
}
|
||||||
|
|
||||||
readonly onPointerUp = (e: PointerEvent): void => {
|
readonly onPointerUp = (e: PointerEvent): void => {
|
||||||
if (e.button === 0) {
|
if (e.button === 0) {
|
||||||
this.isLeftMouseDown = false
|
this.isLeftMouseDown = false
|
||||||
|
|
@ -3437,6 +3451,7 @@ export class SceneMouseController {
|
||||||
this.isLeftMouseDown = false
|
this.isLeftMouseDown = false
|
||||||
this.isRightMouseDown = false
|
this.isRightMouseDown = false
|
||||||
this.lastClientPos = null
|
this.lastClientPos = null
|
||||||
|
this.hoveredGroundItem = null
|
||||||
this.stuckTicks = 0
|
this.stuckTicks = 0
|
||||||
this.lastDist = -1
|
this.lastDist = -1
|
||||||
;(this.engine as any).dialog = null
|
;(this.engine as any).dialog = null
|
||||||
|
|
@ -3625,6 +3640,7 @@ export class SceneMouseController {
|
||||||
this.canvas.addEventListener('contextmenu', this.onContextMenu)
|
this.canvas.addEventListener('contextmenu', this.onContextMenu)
|
||||||
this.canvas.addEventListener('pointerdown', this.onPointerDown)
|
this.canvas.addEventListener('pointerdown', this.onPointerDown)
|
||||||
this.canvas.addEventListener('pointermove', this.onPointerMove)
|
this.canvas.addEventListener('pointermove', this.onPointerMove)
|
||||||
|
this.canvas.addEventListener('pointerleave', this.onPointerLeave)
|
||||||
this.canvas.addEventListener('pointerup', this.onPointerUp)
|
this.canvas.addEventListener('pointerup', this.onPointerUp)
|
||||||
this.canvas.addEventListener('pointercancel', this.onPointerUp)
|
this.canvas.addEventListener('pointercancel', this.onPointerUp)
|
||||||
|
|
||||||
|
|
@ -3638,6 +3654,7 @@ export class SceneMouseController {
|
||||||
this.canvas.removeEventListener('contextmenu', this.onContextMenu)
|
this.canvas.removeEventListener('contextmenu', this.onContextMenu)
|
||||||
this.canvas.removeEventListener('pointerdown', this.onPointerDown)
|
this.canvas.removeEventListener('pointerdown', this.onPointerDown)
|
||||||
this.canvas.removeEventListener('pointermove', this.onPointerMove)
|
this.canvas.removeEventListener('pointermove', this.onPointerMove)
|
||||||
|
this.canvas.removeEventListener('pointerleave', this.onPointerLeave)
|
||||||
this.canvas.removeEventListener('pointerup', this.onPointerUp)
|
this.canvas.removeEventListener('pointerup', this.onPointerUp)
|
||||||
this.canvas.removeEventListener('pointercancel', this.onPointerUp)
|
this.canvas.removeEventListener('pointercancel', this.onPointerUp)
|
||||||
|
|
||||||
|
|
@ -5855,6 +5872,9 @@ if (typeof window !== 'undefined') {
|
||||||
groundLabelOverlay.setOnPickupCallback((item) => {
|
groundLabelOverlay.setOnPickupCallback((item) => {
|
||||||
mouseController.pickupGroundItem(item)
|
mouseController.pickupGroundItem(item)
|
||||||
})
|
})
|
||||||
|
groundLabelOverlay.setOnHoverCallback((item) => {
|
||||||
|
mouseController.hoveredGroundItem = item
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
(window as any).__d2webMouseController = mouseController
|
(window as any).__d2webMouseController = mouseController
|
||||||
|
|
@ -6984,7 +7004,13 @@ if (typeof window !== 'undefined') {
|
||||||
|
|
||||||
if (groundLabelOverlay !== null) {
|
if (groundLabelOverlay !== null) {
|
||||||
const showLabels = Boolean(hudManager?.showGroundLabels)
|
const showLabels = Boolean(hudManager?.showGroundLabels)
|
||||||
if (showLabels && engine.groundItems.count > 0) {
|
let hoveredItem = mouseController?.hoveredGroundItem ?? null
|
||||||
|
if (hoveredItem !== null && !engine.groundItems.get(hoveredItem.id)) {
|
||||||
|
if (mouseController) mouseController.hoveredGroundItem = null
|
||||||
|
hoveredItem = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (engine.groundItems.count > 0 && (showLabels || hoveredItem !== null)) {
|
||||||
const rect = canvas.getBoundingClientRect()
|
const rect = canvas.getBoundingClientRect()
|
||||||
const scaleX = canvas.width > 0 ? rect.width / canvas.width : 1
|
const scaleX = canvas.width > 0 ? rect.width / canvas.width : 1
|
||||||
const scaleY = canvas.height > 0 ? rect.height / canvas.height : 1
|
const scaleY = canvas.height > 0 ? rect.height / canvas.height : 1
|
||||||
|
|
@ -6996,9 +7022,14 @@ if (typeof window !== 'undefined') {
|
||||||
}
|
}
|
||||||
return { x: domX, y: domY }
|
return { x: domX, y: domY }
|
||||||
}
|
}
|
||||||
|
if (showLabels) {
|
||||||
const initialLayouts = computeInitialGroundLabelLayouts(engine.groundItems.all, projectToScreen)
|
const initialLayouts = computeInitialGroundLabelLayouts(engine.groundItems.all, projectToScreen)
|
||||||
const resolvedLayouts = resolveLadderCollisions(initialLayouts)
|
const resolvedLayouts = resolveLadderCollisions(initialLayouts)
|
||||||
groundLabelOverlay.update(true, resolvedLayouts)
|
groundLabelOverlay.update(true, resolvedLayouts)
|
||||||
|
} else if (hoveredItem !== null) {
|
||||||
|
const initialLayouts = computeInitialGroundLabelLayouts([hoveredItem], projectToScreen)
|
||||||
|
groundLabelOverlay.update(true, initialLayouts)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
groundLabelOverlay.update(false, [])
|
groundLabelOverlay.update(false, [])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2586,9 +2586,9 @@ export const BAKED_UI_MANIFEST: BakedUiManifest = {
|
||||||
"skl": "flpskl",
|
"skl": "flpskl",
|
||||||
"skz": "flpskl",
|
"skz": "flpskl",
|
||||||
"hrb": "flphrb",
|
"hrb": "flphrb",
|
||||||
"cm1": "flpmss",
|
"cm1": "flpchm1",
|
||||||
"cm2": "flptrch",
|
"cm2": "flpchm2",
|
||||||
"cm3": "flpmss",
|
"cm3": "flpchm3",
|
||||||
"rps": "flprps",
|
"rps": "flprps",
|
||||||
"rpl": "flprpl",
|
"rpl": "flprpl",
|
||||||
"bps": "flpbps",
|
"bps": "flpbps",
|
||||||
|
|
@ -3294,7 +3294,7 @@ export const BAKED_UI_MANIFEST: BakedUiManifest = {
|
||||||
"flpci1": "flpci1",
|
"flpci1": "flpci1",
|
||||||
"Ormus' Robes": "flpqlt_blac",
|
"Ormus' Robes": "flpqlt_blac",
|
||||||
"flpqlt_blac": "flpqlt_blac",
|
"flpqlt_blac": "flpqlt_blac",
|
||||||
"Gheed's Fortune": "flpmss",
|
"Gheed's Fortune": "flpchm3",
|
||||||
"Stormlash": "flpfla_dgry",
|
"Stormlash": "flpfla_dgry",
|
||||||
"flpfla_dgry": "flpfla_dgry",
|
"flpfla_dgry": "flpfla_dgry",
|
||||||
"Halaberd's Reign": "flpba5",
|
"Halaberd's Reign": "flpba5",
|
||||||
|
|
@ -3565,7 +3565,7 @@ export const BAKED_UI_MANIFEST: BakedUiManifest = {
|
||||||
"Sander's Superstition": "flpbwn",
|
"Sander's Superstition": "flpbwn",
|
||||||
"gold": "flpgld",
|
"gold": "flpgld",
|
||||||
"Horadric Cube": "flpbox",
|
"Horadric Cube": "flpbox",
|
||||||
"gheeds": "flpmss",
|
"gheeds": "flpchm3",
|
||||||
"anni": "flpmss",
|
"anni": "flpmss",
|
||||||
"torch": "flptrch",
|
"torch": "flptrch",
|
||||||
"flphax": "flphax",
|
"flphax": "flphax",
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,7 @@ export class GroundLabelOverlay {
|
||||||
private readonly container: HTMLElement
|
private readonly container: HTMLElement
|
||||||
private readonly labelPool: HTMLElement[] = []
|
private readonly labelPool: HTMLElement[] = []
|
||||||
private onPickupCallback: ((item: GroundItemEntity) => void) | null = null
|
private onPickupCallback: ((item: GroundItemEntity) => void) | null = null
|
||||||
|
private onHoverCallback: ((item: GroundItemEntity | null) => void) | null = null
|
||||||
|
|
||||||
constructor(container: HTMLElement) {
|
constructor(container: HTMLElement) {
|
||||||
this.container = container
|
this.container = container
|
||||||
|
|
@ -259,6 +260,13 @@ export class GroundLabelOverlay {
|
||||||
this.onPickupCallback = callback
|
this.onPickupCallback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a callback when a ground item label is hovered or unhovered.
|
||||||
|
*/
|
||||||
|
setOnHoverCallback(callback: (item: GroundItemEntity | null) => void): void {
|
||||||
|
this.onHoverCallback = callback
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update and render ground item labels based on current visible items and Alt key state.
|
* Update and render ground item labels based on current visible items and Alt key state.
|
||||||
*/
|
*/
|
||||||
|
|
@ -295,10 +303,17 @@ export class GroundLabelOverlay {
|
||||||
el.addEventListener('mouseenter', () => {
|
el.addEventListener('mouseenter', () => {
|
||||||
el.style.backgroundColor = 'rgba(28, 28, 48, 0.96)'
|
el.style.backgroundColor = 'rgba(28, 28, 48, 0.96)'
|
||||||
el.style.borderColor = '#ffffff'
|
el.style.borderColor = '#ffffff'
|
||||||
|
const currentItem = (el as any).__groundItem as GroundItemEntity | undefined
|
||||||
|
if (currentItem && this.onHoverCallback) {
|
||||||
|
this.onHoverCallback(currentItem)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
el.addEventListener('mouseleave', () => {
|
el.addEventListener('mouseleave', () => {
|
||||||
el.style.backgroundColor = 'rgba(0, 0, 0, 0.88)'
|
el.style.backgroundColor = 'rgba(0, 0, 0, 0.88)'
|
||||||
el.style.borderColor = el.dataset.qualityColor || '#404040'
|
el.style.borderColor = el.dataset.qualityColor || '#404040'
|
||||||
|
if (this.onHoverCallback) {
|
||||||
|
this.onHoverCallback(null)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
el.addEventListener('pointerdown', (e) => {
|
el.addEventListener('pointerdown', (e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,8 @@ export const INV_CLOSE_BTN_BOUNDS = { x: 400 + 18, y: 60 + 385, w: 32, h: 32 } a
|
||||||
export interface ResolvableItem {
|
export interface ResolvableItem {
|
||||||
readonly code?: string | undefined
|
readonly code?: string | undefined
|
||||||
readonly name?: string | undefined
|
readonly name?: string | undefined
|
||||||
|
readonly nameZh?: string | undefined
|
||||||
|
readonly baseNameZh?: string | undefined
|
||||||
readonly invFile?: string | undefined
|
readonly invFile?: string | undefined
|
||||||
readonly flippyFile?: string | undefined
|
readonly flippyFile?: string | undefined
|
||||||
readonly isGold?: boolean | undefined
|
readonly isGold?: boolean | undefined
|
||||||
|
|
@ -397,6 +399,20 @@ export function resolveGroundItemSpriteRect(
|
||||||
return flippyRects[`flpgld_${pileIndex}`] ?? flippyRects['flpgld'] ?? null
|
return flippyRects[`flpgld_${pileIndex}`] ?? flippyRects['flpgld'] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1.5 Authentic Charm Ground Flippy Resolution (cm1 -> flpchm1, cm2 -> flpchm2, cm3 -> flpchm3)
|
||||||
|
const normCode = normalizeItemCode(item)
|
||||||
|
const isAnni = normCode === 'anni' || (typeof item.name === 'string' && item.name.includes('Annihilus'))
|
||||||
|
const isTorch = normCode === 'torch' || (typeof item.name === 'string' && item.name.includes('Torch'))
|
||||||
|
if (normCode === 'cm3' || normCode === 'gheeds' || (typeof item.name === 'string' && item.name.includes('Gheed'))) {
|
||||||
|
return flippyRects['flpchm3'] ?? null
|
||||||
|
}
|
||||||
|
if (normCode === 'cm2' || isTorch) {
|
||||||
|
return isTorch ? (flippyRects['flptrch'] ?? flippyRects['flpchm2'] ?? null) : (flippyRects['flpchm2'] ?? null)
|
||||||
|
}
|
||||||
|
if (normCode === 'cm1' || isAnni) {
|
||||||
|
return isAnni ? (flippyRects['flpmss'] ?? flippyRects['flpchm1'] ?? null) : (flippyRects['flpchm1'] ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Direct flippyFile property
|
// 2. Direct flippyFile property
|
||||||
if ((item as any).flippyFile) {
|
if ((item as any).flippyFile) {
|
||||||
const flpFile = String((item as any).flippyFile).toLowerCase()
|
const flpFile = String((item as any).flippyFile).toLowerCase()
|
||||||
|
|
@ -472,6 +488,10 @@ export function resolveGroundItemSpriteRect(
|
||||||
if (nameStr.includes('belt') || nameStr.includes('腰带') || nameStr.includes('带')) return flippyRects['flplbl'] ?? flippyRects['flphbl'] ?? null
|
if (nameStr.includes('belt') || nameStr.includes('腰带') || nameStr.includes('带')) return flippyRects['flplbl'] ?? flippyRects['flphbl'] ?? null
|
||||||
if (nameStr.includes('rune') || nameStr.includes('符文')) return flippyRects['flprun'] ?? null
|
if (nameStr.includes('rune') || nameStr.includes('符文')) return flippyRects['flprun'] ?? null
|
||||||
if (nameStr.includes('scroll') || nameStr.includes('卷轴')) return flippyRects['flptsc'] ?? flippyRects['flprsc'] ?? null
|
if (nameStr.includes('scroll') || nameStr.includes('卷轴')) return flippyRects['flptsc'] ?? flippyRects['flprsc'] ?? null
|
||||||
|
if (nameStr.includes('grand charm') || nameStr.includes('超大护身符') || nameStr.includes('超大型护身符')) return flippyRects['flpchm3'] ?? null
|
||||||
|
if (nameStr.includes('large charm') || nameStr.includes('大型护身符')) return flippyRects['flpchm2'] ?? null
|
||||||
|
if (nameStr.includes('small charm') || nameStr.includes('小型护身符') || nameStr.includes('小护身符')) return flippyRects['flpchm1'] ?? null
|
||||||
|
if (nameStr.includes('charm') || nameStr.includes('护身符')) return flippyRects['flpchm1'] ?? null
|
||||||
|
|
||||||
// 7. Fallback to 2D inventory item sprite rect if no flippy found
|
// 7. Fallback to 2D inventory item sprite rect if no flippy found
|
||||||
return resolveItemSpriteRect(item, BAKED_UI_MANIFEST.itemRects)
|
return resolveItemSpriteRect(item, BAKED_UI_MANIFEST.itemRects)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
/**
|
||||||
|
* tests/charm-ground-flippy-parity.test.ts
|
||||||
|
*
|
||||||
|
* Regression test for Issue #452:
|
||||||
|
* Grand Charm (cm3 / 超大护身符) ground sprite must resolve to flpchm3 (21x6 px),
|
||||||
|
* NOT flpmss (Annihilus / Soulstone, 9x4 px).
|
||||||
|
*
|
||||||
|
* Authentic 1.13c Charm Ground Flippy Parity:
|
||||||
|
* - cm1 (Small Charm): flpchm1 (13x5 px)
|
||||||
|
* - Annihilus (anni / Annihilus): flpmss (9x4 px)
|
||||||
|
* - cm2 (Large Charm): flpchm2 (13x6 px)
|
||||||
|
* - Hellfire Torch (torch / Hellfire Torch): flptrch
|
||||||
|
* - cm3 (Grand Charm): flpchm3 (21x6 px)
|
||||||
|
* - Gheed's Fortune (gheeds / Gheed's Fortune): flpchm3
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { resolveGroundItemSpriteRect } from '../src/ui/inventory.ts'
|
||||||
|
import { BAKED_UI_MANIFEST } from '../src/ui/baked-ui-meta.ts'
|
||||||
|
|
||||||
|
describe('Charm Ground Flippy Sprite Resolution Parity (Issue #452)', () => {
|
||||||
|
const flippyRects = BAKED_UI_MANIFEST.flippyRects
|
||||||
|
const flpchm1 = flippyRects['flpchm1']
|
||||||
|
const flpchm2 = flippyRects['flpchm2']
|
||||||
|
const flpchm3 = flippyRects['flpchm3']
|
||||||
|
const flpmss = flippyRects['flpmss']
|
||||||
|
const flptrch = flippyRects['flptrch']
|
||||||
|
|
||||||
|
it('verifies flippy sprite assets exist in manifest', () => {
|
||||||
|
expect(flpchm1).toBeDefined()
|
||||||
|
expect(flpchm2).toBeDefined()
|
||||||
|
expect(flpchm3).toBeDefined()
|
||||||
|
expect(flpmss).toBeDefined()
|
||||||
|
expect(flpchm3?.w).toBe(21)
|
||||||
|
expect(flpchm3?.h).toBe(6)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves normal Grand Charm (cm3) to flpchm3 and NOT flpmss', () => {
|
||||||
|
const item = { code: 'cm3', name: 'Grand Charm', quality: 'magic' }
|
||||||
|
const rect = resolveGroundItemSpriteRect(item)
|
||||||
|
expect(rect).toEqual(flpchm3)
|
||||||
|
expect(rect).not.toEqual(flpmss)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("resolves Gheed's Fortune unique Grand Charm to flpchm3 and NOT flpmss", () => {
|
||||||
|
const item = {
|
||||||
|
code: 'cm3',
|
||||||
|
name: "Gheed's Fortune",
|
||||||
|
nameZh: "基德的运气 (Gheed's Fortune)",
|
||||||
|
baseNameZh: '超大型护身符 (Grand Charm)',
|
||||||
|
quality: 'unique',
|
||||||
|
}
|
||||||
|
const rect = resolveGroundItemSpriteRect(item)
|
||||||
|
expect(rect).toEqual(flpchm3)
|
||||||
|
expect(rect).not.toEqual(flpmss)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves normal Small Charm (cm1) to flpchm1 and NOT flpmss', () => {
|
||||||
|
const item = { code: 'cm1', name: 'Small Charm', quality: 'magic' }
|
||||||
|
const rect = resolveGroundItemSpriteRect(item)
|
||||||
|
expect(rect).toEqual(flpchm1)
|
||||||
|
expect(rect).not.toEqual(flpmss)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves Annihilus unique Small Charm to flpmss', () => {
|
||||||
|
const item = {
|
||||||
|
code: 'cm1',
|
||||||
|
name: 'Annihilus',
|
||||||
|
nameZh: '毁灭 (Annihilus)',
|
||||||
|
quality: 'unique',
|
||||||
|
}
|
||||||
|
const rect = resolveGroundItemSpriteRect(item)
|
||||||
|
expect(rect).toEqual(flpmss)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves normal Large Charm (cm2) to flpchm2', () => {
|
||||||
|
const item = { code: 'cm2', name: 'Large Charm', quality: 'magic' }
|
||||||
|
const rect = resolveGroundItemSpriteRect(item)
|
||||||
|
expect(rect).toEqual(flpchm2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves Hellfire Torch unique Large Charm to flptrch', () => {
|
||||||
|
const item = {
|
||||||
|
code: 'cm2',
|
||||||
|
name: 'Hellfire Torch',
|
||||||
|
nameZh: '地狱火炬 (Hellfire Torch)',
|
||||||
|
quality: 'unique',
|
||||||
|
}
|
||||||
|
const rect = resolveGroundItemSpriteRect(item)
|
||||||
|
expect(rect).toEqual(flptrch)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves semantic Chinese fallback names for charms correctly', () => {
|
||||||
|
expect(resolveGroundItemSpriteRect({ name: '超大护身符' })).toEqual(flpchm3)
|
||||||
|
expect(resolveGroundItemSpriteRect({ nameZh: '超大型护身符' })).toEqual(flpchm3)
|
||||||
|
expect(resolveGroundItemSpriteRect({ name: '大型护身符' })).toEqual(flpchm2)
|
||||||
|
expect(resolveGroundItemSpriteRect({ name: '小型护身符' })).toEqual(flpchm1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -469,5 +469,45 @@ describe('Ground Items Alt Labels (Issue #390)', () => {
|
||||||
el.dispatchEvent({ type: 'pointerdown', stopPropagation: vi.fn(), preventDefault: vi.fn() })
|
el.dispatchEvent({ type: 'pointerdown', stopPropagation: vi.fn(), preventDefault: vi.fn() })
|
||||||
expect(onPickup).toHaveBeenCalledWith(uniqueItem)
|
expect(onPickup).toHaveBeenCalledWith(uniqueItem)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('triggers onHoverCallback on mouseenter and mouseleave for hovered labels (Issue #447)', () => {
|
||||||
|
const container = createMockDomElement('div')
|
||||||
|
const overlay = new GroundLabelOverlay(container)
|
||||||
|
const onHover = vi.fn()
|
||||||
|
overlay.setOnHoverCallback(onHover)
|
||||||
|
|
||||||
|
const magicItem = createMockItem({
|
||||||
|
id: 'ring-1',
|
||||||
|
name: 'Ring of the Apprentice',
|
||||||
|
nameZh: '学徒之戒',
|
||||||
|
quality: 'magic',
|
||||||
|
})
|
||||||
|
|
||||||
|
const layout: GroundLabelLayout = {
|
||||||
|
id: 'ring-1',
|
||||||
|
itemId: 'ring-1',
|
||||||
|
item: magicItem,
|
||||||
|
text: '学徒之戒',
|
||||||
|
color: GROUND_LABEL_QUALITY_COLORS.magic!,
|
||||||
|
borderColor: GROUND_LABEL_QUALITY_COLORS.magic!,
|
||||||
|
width: 70,
|
||||||
|
height: 18,
|
||||||
|
screenX: 200,
|
||||||
|
screenY: 100,
|
||||||
|
naturalY: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
overlay.update(true, [layout])
|
||||||
|
const el = container.children[0]
|
||||||
|
|
||||||
|
// Mouseenter
|
||||||
|
el.dispatchEvent({ type: 'mouseenter' })
|
||||||
|
expect(onHover).toHaveBeenCalledWith(magicItem)
|
||||||
|
expect(el.style.borderColor).toBe('#ffffff')
|
||||||
|
|
||||||
|
// Mouseleave
|
||||||
|
el.dispatchEvent({ type: 'mouseleave' })
|
||||||
|
expect(onHover).toHaveBeenCalledWith(null)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue