Merge pull request 'fix(ui): align Private Stash header and gold text inside TradeStash.dc6 recessed boxes (#493)' (#494) from fix/stash-text-align into main

Merge pull request 'fix(ui): align Private Stash header and gold text inside TradeStash.dc6 recessed boxes (#493)' (#494) from fix/stash-text-align into main
This commit is contained in:
troytt 2026-09-27 05:55:32 +00:00
commit d2358ede37
2 changed files with 165 additions and 3 deletions

View File

@ -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_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) */
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
@ -1916,12 +1955,12 @@ export class WorldPanelsHud {
}
} else if (kind === 'stash') {
if (assets.stashBgImg) ctx.drawImage(assets.stashBgImg, ox, oy)
font.drawText(ctx, '私人储物箱 (Private Stash)', ox + 160, oy + 26, {
font: 'fontexocet10',
font.drawText(ctx, '私人储物箱 (Private Stash)', ox + 172, oy + 35, {
font: 'font8',
color: 'gold',
align: 'center',
})
font.drawText(ctx, `储物箱金币: ${this.stashGold.toLocaleString()}`, ox + 160, oy + 48, {
font.drawText(ctx, `储物箱金币: ${this.stashGold.toLocaleString()}`, ox + 160, oy + 61, {
font: 'font8',
color: 'white',
align: 'center',

View File

@ -15,8 +15,12 @@ import {
PLAYER_GOLD_CAP,
STASH_DEPOSIT_BTN_BOUNDS,
STASH_WITHDRAW_BTN_BOUNDS,
STASH_HEADER_ICON_SOCKET,
STASH_HEADER_TOP_RECESS,
STASH_HEADER_BOTTOM_RECESS,
WorldPanelsHud,
} from '../src/ui/world-panels.ts'
import type { D2FontRenderer } from '../src/ui/font.ts'
import {
InventoryPanel,
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)
})
})
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)
})
})
})