Files
sacred_k/docs/hud.md
T
khallaandClaude Opus 5.5 1fd89524d3 Skills, auto-attack, aura and chain lightning rework, Esc, cache fix
- Skill bar: no art selection; slot 0 is the right-click art, keys 0-6 cast
  slots directly; cooldown labels in exact seconds; slot 0 set apart.
- Arts regenerate on their own (casting no longer refills the group).
- Auto-attack after targeting or casting at a hostile creature, at the speed
  of a held button.
- Auras can be recast while active (renew, no second visual).
- Blicke wie Klingen hits every enemy in range, physical, with life leech.
- Blitzschlag steers the game's own chain: 3 + level/5 hits, 1 + level/10
  chains, re-hits a lone enemy.
- Weapon sets: click selects, drag only from the selected set; the leftmost
  set is clickable (signed hit tests).
- Esc closes open windows before opening the game menu.
- HUD: standalone buttons and texts are centred.
- Record cache sized for widened views (town lag while walking).
- Shared game code: orders, fx events, text drawing, cooldowns, effects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 00:11:38 +02:00

3.8 KiB

HUD layout in widened views (src/mods/hud.cpp)

The UI is laid out for 1024x768. In a widened render target (e.g. 1835x768) the windows are moved as a whole, without touching their own coordinates: the bottom bar and all dialogs are centred, the party panel (portrait, health) sits at the right edge. Main menu and loading screen keep the classic 1024x768 frame (see widescreen.md).

How a window is moved

A window's render and event methods are hooked (HUD_RENDER, HUD_RENDER3, HUD_EVENT). While one of them runs on a thread, everything the window sees or draws is translated by its offset dx (outermost window wins, children and shared base methods are inside the scope):

What Where Translation
screen-space vertices (XYZRHW) IDirect3DDevice7::DrawPrimitive / DrawIndexedPrimitive (vtable 25 / 26) x + dx
3D items (belt, equipment) ortho projection centred on the screen none needed
mouse events cEvent_mouseMove x at +4 (previous) and +0xc; button down / up x at +8 x - dx, restored afterwards
mouse cursor object (*0xcdbadc, accessor 0x6550f0) x at +4, previous x at +0xc (polled for hover, item pick-up 0x6559a0) the accessor returns a per-thread copy with x - dx; dwords the window changed are written back afterwards
GetMouseClientPos (0x66e500) polled position x - dx

Calls outside a window's scope

  • Tooltips (cUI_Tooltip::vf14 0x6e7300) are separate windows that the hovered window positions in its own layout: they draw with the offset of the moved window under the cursor (noted while windows render, expires after 150 ms).
  • Hit tests (vf1c: 0x731d40, taskbar 0x6dfb10) are also called by the UI manager with plain render coordinates, e.g. "is UI under the cursor" for world picking (0x75a370). They use the offset the top-level window (parent chain at +0x50) was last drawn with.
  • The item carried by the cursor (0x6555e0, drawn by the render loop) and the manager's status line / message box (0x75bbf0, 0x75c200, laid out around x = 512) draw with the centre offset.

UI events

vf04 of an event is its category: 1 mouse move, 2 button down, 3 button up, 4 key (code at +8), 5 UI command (cEventUI2: id at +4, vf08 subtype). Only mouse events carry positions; category 5 events with subtype 0x10 test a point at +0x48 in cUI_Window2::vf10, but commands such as "open ESC menu" (0x11) keep unrelated data there.

Why the cursor is copied, not shifted

UI windows render on the render thread while world input (picking, hold-to-move, 0x611f10) reads the same cursor object on another thread. Shifting the shared object for the duration of a window's scope let the world sample a position ~400 px off now and then (jerky hold-to-move). Every user of the cursor goes through the accessor 0x6550f0, so inside a moved window it returns a thread-local copy instead (with its own CRITICAL_SECTION at +0x24; copying and writing back hold the real object's lock).

Full-screen layers

A primitive drawn inside a moved window that covers the whole 1024x768 layout (e.g. the dimming behind the exit confirmation) is stretched across the render width instead of shifted.

Standalone controls (quest dialog)

Some dialogs (e.g. accepting a quest) are not one of the hooked window classes: their buttons and texts sit directly in the UI manager as top-level controls (measured: the clicked button's root window was the cUI_Button2 itself). They were drawn unshifted or with whichever window's scope happened to be active, so the dialog jumped between the 1024 layout and the centre and clicks only hit at the unshifted position. cUI_Button2, cUI_Static2, cUI_StaticText64, cUI_StaticText64FX (render and event) and cUI_TextLines (render) are hooked as centred windows too; inside other windows they are in that window's scope already (outermost wins).