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`).
|
* Diablo II v1.13c 800x600 Bottom Control Panel (`Panel\800CtrlPnl7.dc6`).
|
||||||
*
|
*
|
||||||
* Houses:
|
* 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
|
* - 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`)
|
* - 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) &
|
* - 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
|
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 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 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
|
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
|
this.hoveredElement = MINIPANEL_ITEMS[idx]!.action
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.inRect(logicalX, logicalY, EXP_BAR_BOUNDS)) {
|
if (this.inRect(logicalX, logicalY, EXP_BAR_HOVER_BOUNDS)) {
|
||||||
this.hoveredElement = 'exp'
|
this.hoveredElement = 'exp'
|
||||||
} else if (this.inRect(logicalX, logicalY, STAMINA_BAR_BOUNDS)) {
|
} else if (this.inRect(logicalX, logicalY, STAMINA_BAR_BOUNDS)) {
|
||||||
this.hoveredElement = 'stamina'
|
this.hoveredElement = 'stamina'
|
||||||
|
|
@ -136,13 +137,19 @@ export class ControlBarHud {
|
||||||
ctx.fillRect(0, 545, 800, 55)
|
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 xpSpan = Math.max(1, state.nextLevelXp - state.currentLevelXp)
|
||||||
const xpRatio = Math.max(0, Math.min(1, (state.xp - state.currentLevelXp) / xpSpan))
|
const xpRatio = Math.max(0, Math.min(1, (state.xp - state.currentLevelXp) / xpSpan))
|
||||||
ctx.fillStyle = '#070605'
|
ctx.fillStyle = '#070605'
|
||||||
ctx.fillRect(EXP_BAR_BOUNDS.x, EXP_BAR_BOUNDS.y, EXP_BAR_BOUNDS.width, EXP_BAR_BOUNDS.height)
|
ctx.fillRect(EXP_BAR_BOUNDS.x, EXP_BAR_BOUNDS.y, EXP_BAR_BOUNDS.width, EXP_BAR_BOUNDS.height)
|
||||||
ctx.fillStyle = '#d8d0a8'
|
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`)
|
// 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)))
|
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 { 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 { 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 { 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 {
|
import {
|
||||||
InventoryPanel,
|
InventoryPanel,
|
||||||
INV_PANEL_ORIGIN,
|
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(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(MANA_GLOBE_BOUNDS).toEqual({ x: 692, y: 515, width: 80, height: 80 })
|
||||||
expect(BELT_BASE_X).toBe(423)
|
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', () => {
|
it('computes vertical bottom-up liquid clipping for Life & Mana globes', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue