fix: align ControlBar XP bar coordinates and 10-segment ticks (Fixes #146)
This commit is contained in:
commit
6e4d0382ec
|
|
@ -2,7 +2,7 @@
|
|||
* Diablo II v1.13c 800x600 Bottom Control Panel (`Panel\800CtrlPnl7.dc6`).
|
||||
*
|
||||
* Houses:
|
||||
* - Experience Bar (x=256, y=552, w=120, h=4) with tick notches & hover readout
|
||||
* - Experience Bar (x=256, y=560, w=120, h=4) with tick notches & hover readout
|
||||
* - Stamina Bar (x=273, y=573, w=102, h=18) with run drain & walk/stand recovery
|
||||
* - Walk / Run Toggle Button (`Panel\runbutton.dc6` at x=255, y=572, w=16, h=20, hotkey `R`)
|
||||
* - Central Mini-Panel Toggle (`Panel\menubutton.dc6` at x=393, y=562, w=15, h=24) &
|
||||
|
|
@ -27,7 +27,8 @@ export interface ControlBarState {
|
|||
readonly unspentSkillPoints: number
|
||||
}
|
||||
|
||||
export const EXP_BAR_BOUNDS = { x: 256, y: 552, width: 120, height: 5 } as const
|
||||
export const EXP_BAR_BOUNDS = { x: 256, y: 560, width: 120, height: 4 } as const
|
||||
export const EXP_BAR_HOVER_BOUNDS = { x: 256, y: 558, width: 120, height: 8 } as const
|
||||
export const STAMINA_BAR_BOUNDS = { x: 273, y: 573, width: 102, height: 18 } as const
|
||||
export const RUN_BTN_BOUNDS = { x: 255, y: 572, width: 16, height: 20 } as const
|
||||
export const MENU_TOGGLE_BOUNDS = { x: 393, y: 562, width: 15, height: 24 } as const
|
||||
|
|
@ -59,7 +60,7 @@ export class ControlBarHud {
|
|||
this.hoveredElement = MINIPANEL_ITEMS[idx]!.action
|
||||
return
|
||||
}
|
||||
if (this.inRect(logicalX, logicalY, EXP_BAR_BOUNDS)) {
|
||||
if (this.inRect(logicalX, logicalY, EXP_BAR_HOVER_BOUNDS)) {
|
||||
this.hoveredElement = 'exp'
|
||||
} else if (this.inRect(logicalX, logicalY, STAMINA_BAR_BOUNDS)) {
|
||||
this.hoveredElement = 'stamina'
|
||||
|
|
@ -136,13 +137,19 @@ export class ControlBarHud {
|
|||
ctx.fillRect(0, 545, 800, 55)
|
||||
}
|
||||
|
||||
// 2. Experience Bar (`x=256, y=552, w=120, h=4`)
|
||||
// 2. Experience Bar (`x=256, y=560, w=120, h=4`)
|
||||
const xpSpan = Math.max(1, state.nextLevelXp - state.currentLevelXp)
|
||||
const xpRatio = Math.max(0, Math.min(1, (state.xp - state.currentLevelXp) / xpSpan))
|
||||
ctx.fillStyle = '#070605'
|
||||
ctx.fillRect(EXP_BAR_BOUNDS.x, EXP_BAR_BOUNDS.y, EXP_BAR_BOUNDS.width, EXP_BAR_BOUNDS.height)
|
||||
ctx.fillStyle = '#d8d0a8'
|
||||
ctx.fillRect(EXP_BAR_BOUNDS.x, EXP_BAR_BOUNDS.y, Math.round(EXP_BAR_BOUNDS.width * xpRatio), EXP_BAR_BOUNDS.height - 1)
|
||||
ctx.fillRect(EXP_BAR_BOUNDS.x, EXP_BAR_BOUNDS.y, Math.round(EXP_BAR_BOUNDS.width * xpRatio), EXP_BAR_BOUNDS.height)
|
||||
|
||||
// Tick notches (10 segments, 12px each per D2 800x600 specification)
|
||||
ctx.fillStyle = '#1e1812'
|
||||
for (let i = 1; i < 10; i++) {
|
||||
ctx.fillRect(EXP_BAR_BOUNDS.x + i * 12, EXP_BAR_BOUNDS.y, 1, EXP_BAR_BOUNDS.height)
|
||||
}
|
||||
|
||||
// 3. Stamina Bar (`x=273, y=573, w=102, h=18`)
|
||||
const stamRatio = Math.max(0, Math.min(1, state.stamina / Math.max(1, state.maxStamina)))
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ import { parseColoredText, parseFontTblMetrics, D2_COLOR_HEX } from '../src/ui/f
|
|||
import { GlobesHud, LIFE_GLOBE_BOUNDS, MANA_GLOBE_BOUNDS } from '../src/ui/globes.ts'
|
||||
import { BeltHud, BELT_COLS, BELT_ROWS, BELT_BASE_X } from '../src/ui/belt.ts'
|
||||
import { SkillHotkeysHud, LEFT_SKILL_BOUNDS, RIGHT_SKILL_BOUNDS } from '../src/ui/hotkeys.ts'
|
||||
import {
|
||||
ControlBarHud,
|
||||
EXP_BAR_BOUNDS,
|
||||
EXP_BAR_HOVER_BOUNDS,
|
||||
STAMINA_BAR_BOUNDS,
|
||||
} from '../src/ui/control-bar.ts'
|
||||
import {
|
||||
InventoryPanel,
|
||||
INV_PANEL_ORIGIN,
|
||||
|
|
@ -76,6 +82,55 @@ describe('Diablo II v1.13c UI / HUD (Issue #27)', () => {
|
|||
expect(LIFE_GLOBE_BOUNDS).toEqual({ x: 28, y: 515, width: 80, height: 80 })
|
||||
expect(MANA_GLOBE_BOUNDS).toEqual({ x: 692, y: 515, width: 80, height: 80 })
|
||||
expect(BELT_BASE_X).toBe(423)
|
||||
|
||||
// Experience Bar (Issue #146: groove at x=256, y=560, w=120, h=4 in 800CtrlPnl7)
|
||||
expect(EXP_BAR_BOUNDS).toEqual({ x: 256, y: 560, width: 120, height: 4 })
|
||||
expect(EXP_BAR_HOVER_BOUNDS).toEqual({ x: 256, y: 558, width: 120, height: 8 })
|
||||
expect(STAMINA_BAR_BOUNDS).toEqual({ x: 273, y: 573, width: 102, height: 18 })
|
||||
})
|
||||
|
||||
it('aligns Experience Bar to authentic 1.13c groove and handles hover hit-testing (Issue #146)', () => {
|
||||
expect(EXP_BAR_BOUNDS).toEqual({ x: 256, y: 560, width: 120, height: 4 })
|
||||
expect(EXP_BAR_HOVER_BOUNDS).toEqual({ x: 256, y: 558, width: 120, height: 8 })
|
||||
|
||||
// 10 segments of 12px width = 120px total width
|
||||
expect(EXP_BAR_BOUNDS.width).toBe(120)
|
||||
const segmentWidth = EXP_BAR_BOUNDS.width / 10
|
||||
expect(segmentWidth).toBe(12)
|
||||
|
||||
// 9 divider ticks at x = 256 + i * 12 (i = 1..9)
|
||||
const tickXs = Array.from({ length: 9 }, (_, i) => EXP_BAR_BOUNDS.x + (i + 1) * 12)
|
||||
expect(tickXs).toEqual([268, 280, 292, 304, 316, 328, 340, 352, 364])
|
||||
|
||||
const bar = new ControlBarHud()
|
||||
|
||||
// Test start of groove (256, 560)
|
||||
bar.handleMouseMove(256, 560)
|
||||
expect(bar.hoveredElement).toBe('exp')
|
||||
|
||||
// Test throughout the groove
|
||||
bar.handleMouseMove(256 + 60, 562)
|
||||
expect(bar.hoveredElement).toBe('exp')
|
||||
|
||||
// Test end of groove (376, 563)
|
||||
bar.handleMouseMove(256 + 120, 563)
|
||||
expect(bar.hoveredElement).toBe('exp')
|
||||
|
||||
// Test padded hover bounds: y = 558 and y = 566
|
||||
bar.handleMouseMove(256, 558)
|
||||
expect(bar.hoveredElement).toBe('exp')
|
||||
bar.handleMouseMove(376, 566)
|
||||
expect(bar.hoveredElement).toBe('exp')
|
||||
|
||||
// Outside bounds
|
||||
bar.handleMouseMove(255, 560)
|
||||
expect(bar.hoveredElement).toBeNull()
|
||||
bar.handleMouseMove(377, 560)
|
||||
expect(bar.hoveredElement).toBeNull()
|
||||
bar.handleMouseMove(300, 557)
|
||||
expect(bar.hoveredElement).toBeNull()
|
||||
bar.handleMouseMove(300, 567)
|
||||
expect(bar.hoveredElement).toBeNull()
|
||||
})
|
||||
|
||||
it('computes vertical bottom-up liquid clipping for Life & Mana globes', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue