fix(ui): align Private Stash header and gold text inside TradeStash.dc6 recessed boxes (#493) #494
|
|
@ -47,6 +47,45 @@ export const STASH_CLOSE_BTN_BOUNDS = { x: 80 + 272, y: 60 + 388, w: 32, h: 32 }
|
||||||
export const STASH_DEPOSIT_BTN_BOUNDS = { x: 80 + 68, y: 60 + 386, w: 76, h: 26 } as const
|
export const STASH_DEPOSIT_BTN_BOUNDS = { x: 80 + 68, y: 60 + 386, w: 76, h: 26 } as const
|
||||||
export const STASH_WITHDRAW_BTN_BOUNDS = { x: 80 + 154, y: 60 + 386, w: 76, h: 26 } as const
|
export const STASH_WITHDRAW_BTN_BOUNDS = { x: 80 + 154, y: 60 + 386, w: 76, h: 26 } as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `Panel\TradeStash.dc6` (`public/ui/stash-bg.png`, 320x432 anchored at `ox = 80, oy = 60`)
|
||||||
|
* header recessed dark boxes (Issue #493):
|
||||||
|
* - Top box (`relY = 24..38`, height 15px) is divided by a vertical bar at `relX = 94..96` into:
|
||||||
|
* - Left icon socket: `relX = 74..93, relY = 24..38` (20x15px)
|
||||||
|
* - Right title text recess: `relX = 97..247, relY = 24..38` (151x15px, center `relX = 172, relY = 31`, alphabetic baseline `oy + 35`)
|
||||||
|
* - Bottom gold text recess: `relX = 75..246, relY = 49..65` (172x17px, center `relX = 160, relY = 57`, alphabetic baseline `oy + 61`)
|
||||||
|
*/
|
||||||
|
export const STASH_HEADER_ICON_SOCKET = {
|
||||||
|
x: 80 + 74,
|
||||||
|
y: 60 + 24,
|
||||||
|
relX: 74,
|
||||||
|
relY: 24,
|
||||||
|
w: 20,
|
||||||
|
h: 15,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const STASH_HEADER_TOP_RECESS = {
|
||||||
|
x: 80 + 97,
|
||||||
|
y: 60 + 24,
|
||||||
|
relX: 97,
|
||||||
|
relY: 24,
|
||||||
|
w: 151,
|
||||||
|
h: 15,
|
||||||
|
textX: 80 + 172,
|
||||||
|
textY: 60 + 35,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const STASH_HEADER_BOTTOM_RECESS = {
|
||||||
|
x: 80 + 75,
|
||||||
|
y: 60 + 49,
|
||||||
|
relX: 75,
|
||||||
|
relY: 49,
|
||||||
|
w: 172,
|
||||||
|
h: 17,
|
||||||
|
textX: 80 + 160,
|
||||||
|
textY: 60 + 61,
|
||||||
|
} as const
|
||||||
|
|
||||||
/** `Inventory.txt` `Transmogrify Box Page 1` / `Transmogrify Box2` (`supertransmogrifier.dc6` 3x4 grid) */
|
/** `Inventory.txt` `Transmogrify Box Page 1` / `Transmogrify Box2` (`supertransmogrifier.dc6` 3x4 grid) */
|
||||||
export const CUBE_PANEL_ORIGIN = { x: 80, y: 60, width: 320, height: 432, w: 320, h: 432 } as const
|
export const CUBE_PANEL_ORIGIN = { x: 80, y: 60, width: 320, height: 432, w: 320, h: 432 } as const
|
||||||
export const CUBE_GRID_ORIGIN = { x: 80 + 117, y: 60 + 139, cols: 3, rows: 4, cellPx: 29 } as const
|
export const CUBE_GRID_ORIGIN = { x: 80 + 117, y: 60 + 139, cols: 3, rows: 4, cellPx: 29 } as const
|
||||||
|
|
@ -1916,12 +1955,12 @@ export class WorldPanelsHud {
|
||||||
}
|
}
|
||||||
} else if (kind === 'stash') {
|
} else if (kind === 'stash') {
|
||||||
if (assets.stashBgImg) ctx.drawImage(assets.stashBgImg, ox, oy)
|
if (assets.stashBgImg) ctx.drawImage(assets.stashBgImg, ox, oy)
|
||||||
font.drawText(ctx, '私人储物箱 (Private Stash)', ox + 160, oy + 26, {
|
font.drawText(ctx, '私人储物箱 (Private Stash)', ox + 172, oy + 35, {
|
||||||
font: 'fontexocet10',
|
font: 'font8',
|
||||||
color: 'gold',
|
color: 'gold',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
})
|
})
|
||||||
font.drawText(ctx, `储物箱金币: ${this.stashGold.toLocaleString()}`, ox + 160, oy + 48, {
|
font.drawText(ctx, `储物箱金币: ${this.stashGold.toLocaleString()}`, ox + 160, oy + 61, {
|
||||||
font: 'font8',
|
font: 'font8',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,12 @@ import {
|
||||||
PLAYER_GOLD_CAP,
|
PLAYER_GOLD_CAP,
|
||||||
STASH_DEPOSIT_BTN_BOUNDS,
|
STASH_DEPOSIT_BTN_BOUNDS,
|
||||||
STASH_WITHDRAW_BTN_BOUNDS,
|
STASH_WITHDRAW_BTN_BOUNDS,
|
||||||
|
STASH_HEADER_ICON_SOCKET,
|
||||||
|
STASH_HEADER_TOP_RECESS,
|
||||||
|
STASH_HEADER_BOTTOM_RECESS,
|
||||||
WorldPanelsHud,
|
WorldPanelsHud,
|
||||||
} from '../src/ui/world-panels.ts'
|
} from '../src/ui/world-panels.ts'
|
||||||
|
import type { D2FontRenderer } from '../src/ui/font.ts'
|
||||||
import {
|
import {
|
||||||
InventoryPanel,
|
InventoryPanel,
|
||||||
type UiInventoryItem,
|
type UiInventoryItem,
|
||||||
|
|
@ -604,4 +608,123 @@ describe('Milestone M5 (Issue #150): Camp Town Stash Entities Across 5 Acts & 6x
|
||||||
expect(hud.inventory.visible).toBe(false)
|
expect(hud.inventory.visible).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('8. Stash Header Recessed Boxes & Text Alignment Parity (Issue #493)', () => {
|
||||||
|
it('defines exact TradeStash.dc6 recessed box bounds and centered baselines', () => {
|
||||||
|
// Left square icon socket in top row: relX = 74..93, relY = 24..38 (20x15)
|
||||||
|
expect(STASH_HEADER_ICON_SOCKET).toEqual({
|
||||||
|
x: 80 + 74,
|
||||||
|
y: 60 + 24,
|
||||||
|
relX: 74,
|
||||||
|
relY: 24,
|
||||||
|
w: 20,
|
||||||
|
h: 15,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Right title recess in top row (after relX = 94..96 divider): relX = 97..247, relY = 24..38 (151x15)
|
||||||
|
expect(STASH_HEADER_TOP_RECESS).toEqual({
|
||||||
|
x: 80 + 97,
|
||||||
|
y: 60 + 24,
|
||||||
|
relX: 97,
|
||||||
|
relY: 24,
|
||||||
|
w: 151,
|
||||||
|
h: 15,
|
||||||
|
textX: 80 + 172,
|
||||||
|
textY: 60 + 35,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Full-width gold readout recess in bottom row: relX = 75..246, relY = 49..65 (172x17)
|
||||||
|
expect(STASH_HEADER_BOTTOM_RECESS).toEqual({
|
||||||
|
x: 80 + 75,
|
||||||
|
y: 60 + 49,
|
||||||
|
relX: 75,
|
||||||
|
relY: 49,
|
||||||
|
w: 172,
|
||||||
|
h: 17,
|
||||||
|
textX: 80 + 160,
|
||||||
|
textY: 60 + 61,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders Private Stash title and gold readout centered inside their respective recessed boxes without crossing dividers', () => {
|
||||||
|
const panels = new WorldPanelsHud()
|
||||||
|
panels.stashGold = 2_500_000
|
||||||
|
|
||||||
|
const drawTextCalls: Array<{
|
||||||
|
text: string
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
opts?: { font?: string; color?: string; align?: string }
|
||||||
|
}> = []
|
||||||
|
|
||||||
|
const mockFont = {
|
||||||
|
drawText: (_ctx: CanvasRenderingContext2D, text: string, x: number, y: number, opts?: any) => {
|
||||||
|
drawTextCalls.push({ text, x, y, opts })
|
||||||
|
},
|
||||||
|
measureText: (text: string) => text.length * 6.5,
|
||||||
|
} as unknown as D2FontRenderer
|
||||||
|
|
||||||
|
const mockCtx = {
|
||||||
|
drawImage: () => {},
|
||||||
|
fillRect: () => {},
|
||||||
|
strokeRect: () => {},
|
||||||
|
fillStyle: '',
|
||||||
|
strokeStyle: '',
|
||||||
|
} as unknown as CanvasRenderingContext2D
|
||||||
|
|
||||||
|
panels.drawLeftDockPanel(
|
||||||
|
mockCtx,
|
||||||
|
'stash',
|
||||||
|
{
|
||||||
|
questBgImg: null,
|
||||||
|
waypointBgImg: null,
|
||||||
|
borderLeftImg: null,
|
||||||
|
stashBgImg: null,
|
||||||
|
cubeBgImg: null,
|
||||||
|
vendorBgImg: null,
|
||||||
|
buySellBtnImg: null,
|
||||||
|
},
|
||||||
|
mockFont,
|
||||||
|
)
|
||||||
|
|
||||||
|
const titleCall = drawTextCalls.find(c => c.text.includes('私人储物箱'))
|
||||||
|
const goldCall = drawTextCalls.find(c => c.text.includes('储物箱金币'))
|
||||||
|
|
||||||
|
expect(titleCall).toBeDefined()
|
||||||
|
expect(goldCall).toBeDefined()
|
||||||
|
|
||||||
|
// 1. Top title must be centered in the right compartment (relX = 97..247 -> center ox + 172 = 252)
|
||||||
|
// and vertically centered in relY = 24..38 (alphabetic baseline oy + 35 = 95)
|
||||||
|
expect(titleCall!.x).toBe(STASH_HEADER_TOP_RECESS.textX)
|
||||||
|
expect(titleCall!.y).toBe(STASH_HEADER_TOP_RECESS.textY)
|
||||||
|
expect(titleCall!.opts?.font).toBe('font8')
|
||||||
|
expect(titleCall!.opts?.color).toBe('gold')
|
||||||
|
expect(titleCall!.opts?.align).toBe('center')
|
||||||
|
|
||||||
|
// Verify title glyph vertical span [y - 9, y + 1] stays strictly inside top recess [oy + 24, oy + 38]
|
||||||
|
const topRecessTop = STASH_HEADER_TOP_RECESS.y
|
||||||
|
const topRecessBottom = STASH_HEADER_TOP_RECESS.y + STASH_HEADER_TOP_RECESS.h - 1
|
||||||
|
expect(titleCall!.y - 9).toBeGreaterThanOrEqual(topRecessTop)
|
||||||
|
expect(titleCall!.y + 1).toBeLessThanOrEqual(topRecessBottom)
|
||||||
|
|
||||||
|
// Verify title horizontal span (~130px in font8) stays strictly inside [ox + 97, ox + 247] (never crossing relX = 94..96 divider)
|
||||||
|
const approxTitleWidth = 130
|
||||||
|
expect(titleCall!.x - approxTitleWidth / 2).toBeGreaterThan(STASH_HEADER_TOP_RECESS.x)
|
||||||
|
expect(titleCall!.x + approxTitleWidth / 2).toBeLessThan(STASH_HEADER_TOP_RECESS.x + STASH_HEADER_TOP_RECESS.w)
|
||||||
|
|
||||||
|
// 2. Bottom gold text must be centered in the bottom recess (relX = 75..246 -> center ox + 160 = 240)
|
||||||
|
// and vertically centered in relY = 49..65 (alphabetic baseline oy + 61 = 121)
|
||||||
|
expect(goldCall!.x).toBe(STASH_HEADER_BOTTOM_RECESS.textX)
|
||||||
|
expect(goldCall!.y).toBe(STASH_HEADER_BOTTOM_RECESS.textY)
|
||||||
|
expect(goldCall!.opts?.font).toBe('font8')
|
||||||
|
expect(goldCall!.opts?.color).toBe('white')
|
||||||
|
expect(goldCall!.opts?.align).toBe('center')
|
||||||
|
|
||||||
|
// Verify gold glyph vertical span [y - 9, y + 1] stays strictly inside bottom recess [oy + 49, oy + 65]
|
||||||
|
const bottomRecessTop = STASH_HEADER_BOTTOM_RECESS.y
|
||||||
|
const bottomRecessBottom = STASH_HEADER_BOTTOM_RECESS.y + STASH_HEADER_BOTTOM_RECESS.h - 1
|
||||||
|
expect(goldCall!.y - 9).toBeGreaterThanOrEqual(bottomRecessTop)
|
||||||
|
expect(goldCall!.y + 1).toBeLessThanOrEqual(bottomRecessBottom)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue