Commit graph

1739 commits

Author SHA1 Message Date
Erik
30b28c248a docs(D.5.1): register toolbar phase-1 in the roadmap 2026-06-16 21:40:46 +02:00
Erik
44fabd350e docs(D.5.1): toolbar phase-1 implementation plan (+ spec wiring-delta note)
12-task TDD plan: register D.5.1 -> CreateObject IconId capture -> ItemRepository.EnrichItem -> spawn-event icon wiring -> persist shortcuts -> IconComposer (CPU composite) -> UiItemSlot -> UiItemList + factory branch -> ToolbarController -> GameWindow mount -> visual gate -> bookkeeping. Concrete call sites pinned (WorldSession.cs:701 EntitySpawned, GameEventWiring.WireAll, GameWindow Items@598, BuildUse 0x0036). Synced the spec's CreateObject section with the wider-than-expected wiring found during planning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:27:49 +02:00
Erik
0b5e849325 docs(D.5.1): toolbar phase-1 design spec
First D.5 sub-phase: ship gmToolbarUI as the first data-driven game panel (18 slots from LayoutDesc 0x21000016, populated from the persisted PlayerDescription shortcuts, real composited icons, click-to-use). Minimal scope; faithful CPU icon pre-composite (IconData::RenderIcons port). Five bounded units: UiItemSlot, UiItemList, IconComposer, CreateObject IconId extension, ToolbarController. Roadmap registration of D.5.1 is plan step 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:05:04 +02:00
Erik
a5c5126e8d docs(D.5): action bar / inventory / paperdoll research drop
Five report-only deep-dives + synthesis for the next D.2b UI panels, built on the shipped widget toolkit. Confirms LayoutDesc ids (toolbar 0x21000016, inventory 0x21000023, backpack 0x21000022, paperdoll 0x21000024, 3ditems 0x21000021), the shared item-slot/item-list spine (UIElement_UIItem 0x10000032 / UIElement_ItemList 0x10000031), the 5-layer icon composite (IconData::RenderIcons @407524), the cross-panel wire catalog with acdream parse-status, and the dependency-ordered build plan.

Produced via a multi-agent research workflow; the spine agent died on a transient API error and was re-run as a focused follow-up with its decomp anchors verified against source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:04:57 +02:00
Erik
78c91875b8 docs: file #139 — D.2b retail UI polish (chat text colors + buttons)
Deferred cosmetic polish after the widget-generalization landing: tune the
per-ChatKind transcript text colors against retail, and add pressed/hover state
feedback to the chat buttons (UiButton draws only its default state today; the
dat carries Normal/Pressed/Highlight). Not a regression — the generalized chat
matches the prior hand-made build (user-confirmed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 19:01:50 +02:00
Erik
9e4faae9d2 docs(D.2b): roadmap — widget generalization (Plan 2) shipped
Record the D.2b widget-generalization landing: generic Type-registered widgets
built by DatWidgetFactory, thin find-by-id controllers, the ConsumesDatChildren
leaf rule, Type-3-not-registered decision, and the centered-UiText vitals numbers.
Both visual gates user-confirmed; 404 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:55:06 +02:00
Erik
89626cd400 feat(D.2b): vitals numbers as UiText (widget-generalization Task 8)
The vitals cur/max numbers now render through the generic UiText widget — retail
gmVitalsUI uses UIElement_Text for them, not a meter-internal label. VitalsController
attaches a centered, non-interactive UiText child to each meter and stops the meter
drawing its own label (UiMeter.Label -> null). New UiText.Centered draws the first line
centered H+V with the SAME formula UiMeter's overlay used, so the numbers are
pixel-identical — user-confirmed in the live client.

This completes the D.2b widget-generalization pass: every chat + vitals widget is now
built generically and registered to its retail Type (Button/Field*/Menu/Meter/Scrollbar/
Text), with thin find-by-id controllers. (*Field is controller-placed; Type 3 stays
UiDatElement for chrome.)

Divergence register: AP-37 vitals-numbers-via-UiMeter.Label clause retired. Full suite:
404 passed, 2 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:52:42 +02:00
Erik
d7002552bc fix(D.2b): behavioral widgets are leaf — ConsumesDatChildren (chat menu open)
The generalized channel menu wouldn't open: the factory recursed the Type-6
menu element's dat children, building its invisible Type-12 label child as a
UiText. Hit-testing is children-first and UiText consumes MouseDown (selection),
so the label child swallowed the menu button click and the dropdown never opened.
The transcript similarly gained an invisible Ghosted-button child (a 16x16
selection dead-zone). The old hand-made build never had these — it skipped Type 12
and hand-placed the widgets with no children.

Fix: behavioral widgets (Meter/Menu/Button/Scrollbar/Text/Field) draw their full
appearance and reproduce their dat sub-elements procedurally, so they are LEAF —
the importer must not build their dat children as separate (click-stealing)
widgets. Add UiElement.ConsumesDatChildren (default false; the 6 behavioral
widgets override true) and gate LayoutImporter recursion on it (replacing the
UiMeter-only special case). Only generic containers (UiDatElement, panels) recurse.

Visually confirmed in the live client (channel menu opens; General/Trade selected
and sent). Vitals unchanged (UiMeter was already leaf). Full suite: 404 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:36:40 +02:00
Erik
83076cdbb6 docs(D.2b): spec correction — input is Variant B, Type 3 not registered
Record the two execution-time corrections to the design's registration
assumptions: the editable input resolves to Type 12 (Variant B, controller-placed
UiField), and Type 3 is NOT factory-registered (acdream's Type-3 elements are
chrome/containers, kept on the UiDatElement fallback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:54:52 +02:00
Erik
ee2e0fafa0 fix(D.2b): do NOT register Type 3 -> UiField (review fix for Task 6)
Task 6 registered Type 3 -> UiField globally, which broke acdream's Type-3 dat
elements: in these layouts Type 3 is sprite-bearing CHROME (the 8-piece bevel
corners, e.g. vitals 0x10000633 -> sprite 0x060074C3) and the transcript/input
CONTAINER panels — NOT editable fields. UiField draws no dat sprite, so the
vitals bevel corners would render empty; the regression was masked by weakening
VitalsTree_ChromeCornerHasExpectedSprite (UiDatElement+sprite -> UiField+exists).

Retail Type 3 IS UIElement_Field, but retail draws those chrome elements as inert
media-bearing Fields, which our UiDatElement reproduces pixel-for-pixel without a
spurious focus/edit affordance. The one true editable field — the chat input
0x10000016 — resolves to Type 12 and is controller-placed as a UiField (Variant B,
kept). So Type 3 stays on the generic fallback; register it as UiField only when a
window carries a factory-built editable Type-3 field (and UiField grows a
background-media draw + an opt-in editable flag then).

Restored the chrome-corner conformance test (asserts UiDatElement + sprite, an
early warning if Type 3 is ever wrongly routed to UiField). Kept the good Task-6
work: UiField rename + the Variant-B input wiring (stray Type-12 placeholder
removed). Full suite: 404 passed, 2 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:53:56 +02:00
Erik
e059a3f6ef feat(D.2b): UiField (Type 3) — editable input as a generic field; remove the stray Type-12 input placeholder (widget-generalization Task 6)
- Rename UiChatInput → UiField (UIElement_Field, RegisterElementClass(3) @ :126190);
  update doc to cite retail's CatchDroppedItem/MouseOverTop drag-drop hooks for
  future item windows. BackgroundColor default → transparent (controller sets
  the translucent 0.35α value explicitly, matching UiText pattern).
- Register Type 3 in DatWidgetFactory.Create: `3 => new UiField()`.
- ChatWindowController.Bind (Variant B): factory now builds 0x10000016 as an
  invisible UiText placeholder (Type 12); Bind removes that placeholder via
  FindElement(InputId).Parent.RemoveChild and places a UiField at the same rect.
  Result: exactly ONE input widget in the input bar, no stray UiText duplicate.
- Input property type changed from UiChatInput to UiField; GameWindow.cs:1861
  UiField.Keyboard assignment compiles unchanged (field exists).
- Tests: UiChatInputTests → UiFieldTests (class + all ctor refs renamed);
  DatWidgetFactoryTests: new Type3_Field_MakesUiField test; ChatWindowControllerTests:
  updated stale "skipped by factory" comments; LayoutConformanceTests: updated
  VitalsTree_ChromeCornerHasExpectedSprite — Type-3 chrome-corner elements are
  now UiField (sprite rendering for Type-3 dat image elements is a known
  limitation, tracked for post-Task-8 UiField.BackgroundSprite follow-up).
- Full suite: 404 passed, 2 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:48:51 +02:00
Erik
cb082b59e4 feat(D.2b): UiText (Type 12) -- generic text + Type-12 flip; transcript factory-built (widget-generalization Task 5)
Rename UiChatView -> UiText (the retail UIElement_Text class,
RegisterElementClass(0xc) @ acclient_2013_pseudo_c.txt:115655).

Factory changes (DatWidgetFactory.cs):
- Remove the Type-12 skip (was: no-media -> null, with-media -> UiDatElement).
- Add Type 12 -> BuildText() -> UiText in the switch.
- BuildText extracts the element's Direct/Normal sprite as BackgroundSprite
  so any dat-media the element carried keeps rendering under the text.

UiText changes (renamed from UiChatView.cs):
- BackgroundColor default: (0,0,0,0.35) -> (0,0,0,0) (transparent).
  An unbound UiText draws nothing; the controller opts in to the translucent bg.
- New BackgroundSprite + SpriteResolve: optional dat state-sprite background
  drawn UNDER DrawFill+text (faithful UIElement_Text media support).

ChatWindowController.cs (Task 5 Step 8):
- Transcript property: UiChatView -> UiText.
- Bind() now uses layout.FindElement(TranscriptId) as UiText (factory-built)
  instead of manually constructing + AddChild-ing a new UiChatView.
- Sets BackgroundColor = (0,0,0,0.35) on the found widget (retail translucent bg).
- Removes the tInfo null-check from the early guard (transcript is factory-built;
  iInfo lookup kept for the input widget which is still manually constructed).
- BuildLines: UiChatView.Line -> UiText.Line throughout.

Vitals frozen: the Type-12 vitals number elements are meter children and are
never recursed by BuildWidget (the `if (w is not UiMeter)` gate), so they are
not built as widgets and keep rendering via UiMeter.Label. Vitals fixture
vitals_2100006C.json unchanged; LayoutConformanceTests + VitalsBindingTests green.

Tests:
- UiChatViewTests.cs -> UiTextTests.cs (class: UiTextTests, all UiChatView.* -> UiText.*)
- UiChatViewDatFontTests.cs -> UiTextDatFontTests.cs (same)
- DatWidgetFactoryTests: delete Type12_StylePrototype_ReturnsNull +
  DatWidgetFactory_Type12WithMedia_Renders; add Type12_Text_MakesUiText +
  DatWidgetFactory_Type12_AlwaysMakesUiText.
- LayoutImporterTests: BuildFromInfos_Type12Child_IsSkipped_Type3Present updated
  to assert IsType<UiText> (element is now in tree, transparent, not skipped).

Divergence register: AP-37 amended -- removed the "standalone Type-0 text
elements skipped / dat-text widget is Plan 2" clause (now shipped as UiText);
kept the meter-collapse clause and the vitals-numbers-via-UiMeter.Label clause.
AP-38/AP-39/AD-28 file references updated UiChatView.cs -> UiText.cs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:39:02 +02:00
Erik
67e5b8cff2 fix(D.2b): UiMenu — controller owns Selected (review fix for Task 4)
Review caught a behavior divergence: the generic UiMenu auto-set its own
Selected on any enabled pick, while the controller's EnabledProvider keeps the
null-payload specials (Squelch / Tell-to-Selected) enabled/white like retail.
So a special-item click set Selected=null and shifted the highlight onto the
deferred placeholders — and the menu tests masked it by using a different
(specials-disabled) gate than the controller ships.

Fix: clean MVC contract mirroring retail UIElement_Menu::NewSelection — the
widget REPORTS the pick via OnSelect; the controller OWNS Selected (it sets it
only for talk-channel payloads). A special-item click now fires OnSelect(null),
the controller ignores it, and the active channel + highlight stay put —
observably identical to the pre-generalization widget, and extensible for when
Squelch lands. Tests realigned to the controller's gate (specials white) and to
the controller-owns-Selected contract.

Full suite: 403 passed, 2 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:27:30 +02:00
Erik
955f7a69a8 feat(D.2b): UiMenu (Type 6) — generic dropdown; channel knowledge moves to controller (widget-generalization Task 4)
UiChannelMenu → UiMenu: removed ChatChannelKind, the 14-item array, the
button-text map, and the availability default. Generic surface: MenuItem
(label + object? Payload), Selected (object?), OnSelect, EnabledProvider,
ButtonLabelProvider, RowsPerColumn/RowHeight/ColumnWidth (all settable).
All draw/event mechanics unchanged — same popup geometry, same click
coordinates, same 8-piece bevel, same 3-slice button face.

ChatWindowController gains ChannelItems[], ChannelButtonLabel(), and
ChannelAvailable() (verbatim from old widget), and populates the
factory-built Type-6 UiMenu via find-by-id rather than constructing a
replacement widget. The Menu property type is now UiMenu. OnChannelChanged
wrap replaced with the generic OnSelect wrap for the ReflowInputRow hook.

DatWidgetFactory registers Type 6 → new UiMenu().

Tests: UiChannelMenuTests → UiMenuTests (10 tests, all green); factory
Type6 test added; ChatWindowControllerTests updated to use OnSelect.
Divergence register: AP-42 added (flat item model vs retail nested-submenu
MakePopup @0x46d310 — latent, unreachable through the chat menu).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:18:27 +02:00
Erik
805ab5f40b feat(D.2b): UiButton (Type 1) — Send + Max/Min as generic buttons (widget-generalization Task 3)
Introduces UiButton: a dedicated dat-widget button that ports UIElement_Button
(RegisterElementClass(1,...) @ acclient_2013_pseudo_c.txt:125828). State selection,
tiled DrawSprite, and label rendering mirror UiDatElement exactly so the chat Send
and Max/Min buttons have zero behavioral change.

DatWidgetFactory now maps Type 1 → UiButton (beside Type 7 → UiMeter, Type 11 →
UiScrollbar). ChatWindowController's Send and Max/Min bind blocks updated from
UiDatElement casts to UiButton casts; ClickThrough=false lines dropped (UiButton
is interactive by construction).

The old UiPanel.cs UiButton (a plain dev-scaffold rect+text button with no dat
sprites) is renamed UiSimpleButton to free the name — no production code
instantiated it.

Full suite: 402 passed, 2 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:07:58 +02:00
Erik
3593d6623d feat(D.2b): UiScrollbar (Type 11) — promote the generic chat scrollbar (widget-generalization Task 2)
- git mv UiChatScrollbar.cs → UiScrollbar.cs; rename class + update doc summary to
  "Generic scrollbar. Ports retail UIElement_Scrollbar (RegisterElementClass(0xb) @
  acclient_2013_pseudo_c.txt:124137); thumb size = trackLen * ThumbRatio (min 8px); step ±1 line."
- git mv UiChatScrollbarTests.cs → UiScrollbarTests.cs; rename test class + replace
  every UiChatScrollbar reference with UiScrollbar (bodies unchanged).
- DatWidgetFactory: register Type 11 → new UiScrollbar() before the _ fallback case.
- ChatWindowController: change Scrollbar property type to UiScrollbar; replace the old
  "construct-remove-add" block with a "find factory-built UiScrollbar and bind in place"
  block (no RemoveChild/AddChild); keep `var track` assignment in scope so the Max/Min
  block's track.Left/track.Width reads still compile against UiElement?.
- AP-41 divergence register: update file:line to UiScrollbar.cs:35; narrow wording to
  "fallback only — single-tile drawn only when cap ids are unset; the chat controller
  passes all three cap ids so the 3-slice path is the active code path."
- Update inline UiChatScrollbar doc-comment references in UiScrollable.cs + UiChatView.cs.
- Full suite: 399 passed, 2 skipped (dat/tower fixture skips), 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:02:49 +02:00
Erik
d1b13a7dbf test(D.2b): chat golden fixture + resolved-Type conformance (widget-generalization Task 1)
- Add ChatLayoutFixtureGenerator.cs (Skip-by-default) to regenerate
  chat_21000006.json from the live portal.dat via LayoutImporter.ImportInfos
- Commit generated fixture chat_21000006.json (13 KB, 400 lines) — dat-free,
  auto-copied to test output via existing *.json csproj glob
- Refactor FixtureLoader: extract shared LoadInfos(fileName) helper; add
  LoadChat() + LoadChatInfos() mirroring the vitals pattern; LoadVitalsInfos()
  now delegates to the shared loader (behavior unchanged, vitals tests green)
- Add ChatLayoutConformanceTests: ResolvesKnownElements + ResolvedTypes_MatchRetailRegistry

Confirmed resolved Types from live dat:
  0x10000011 (transcript) → Type 12 (style-prototype, skipped by factory)
  0x10000016 (input)      → Type 12 (style-prototype, skipped by factory)
  0x10000014 (menu)       → Type 6
  0x10000012 (scrollbar)  → Type 11
  0x10000019 (send)       → Type 1
  0x1000046F (max/min)    → Type 1

Also fix pre-existing build break: UiChatInput.MoveCaret(int delta) was made
private in ce848c1 but UiChatInputTests.Backspace_DeletesBeforeCaret called it
as public. Expose a public MoveCaret(int) overload (no-shift) alongside the
private MoveCaret(int,bool) — restores the intended test surface.

Full suite: 398 passed, 2 skipped (generator + pre-existing), 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:55:51 +02:00
Erik
34e79096f3 docs(D.2b): widget-generalization implementation plan
8-task TDD plan: chat golden fixture + resolved-Type conformance (Task 1,
empirically resolves the input's Type), then one-widget-per-commit migration —
UiScrollbar(11), UiButton(1), UiMenu(6), UiText(12)+the Type-12 flip,
UiField(3) — then thin the controller (Task 7, visual gate) and the gated
vitals UiText rewire (Task 8). Each task: failing test, register in the
factory switch, controller find-by-id binding, build+test green, commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:47:32 +02:00
Erik
56f5bc7aa1 docs(D.2b): add strategic-purpose section to widget-generalization design
Capture the 'why beyond chat' the user articulated: chat is the proving ground;
the real payoff is inventory/spell-bar/vendor/character-sheet/trade becoming
data-driven assembly + thin controller. Notes what carries forward (the generic
widget toolkit + the find-by-id controller pattern) vs what those windows still
need (ListBox/Panel + Field drag-drop, the window-manager half of Plan 2, and
per-domain item/container data).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:33:14 +02:00
Erik
b7f7e2b4ef docs(D.2b): widget-generalization design (Plan 2 widget piece)
Design for refactoring the hand-named chat widgets + Send/MaxMin click-wiring
into generic, Type-registered widgets built by DatWidgetFactory, collapsing
ChatWindowController (and, gated-last, VitalsController) to a thin retail
gm*UI::PostInit-style find-by-id binder.

Key finding that reframes the pass: the importer's base-chain Type resolution
is already retail-faithful, and Type 12 is UIElement_Text (a real behavioral
class), not a style prototype to skip — verified against
acclient_2013_pseudo_c.txt:115655. The generalization is therefore a
registration task (register Types 1/3/6/11/12 -> generic widgets, delete the
Type-12 skip), not a new mechanism.

Approved scope: full registry (bounded to the Types chat+vitals use; rest stays
UiDatElement fallback), chat-first, vitals rewire as the final separately-gated
step. 7-step one-widget-per-commit migration; new chat_21000006.json golden
fixture; vitals fixture stays frozen through steps 1-6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:26:32 +02:00
Erik
fed838847b chore(cli): dump-font-atlas tool for headless font inspection
A `dump-font-atlas` subcommand renders a dat Font's fg/bg atlases (alpha as
luminance) plus a sample string composited exactly the way DrawStringDat does
it (outline + fill, integer-snapped). Used to reproduce the glyph-baseline
jitter offline (fractional-origin bug vs the fix) without launching the client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:24:37 +02:00
Erik
ce848c154d feat(D.2b): chat wiring — menu/input sprites, button reflow, char-wrap, panel wash fix
- ChatWindowController: wires the menu chrome (popup bevel, row/checkbox
  sprites), the input focused-field sprite + keyboard, and autosizes the channel
  button + reflows the input field to start after it (anchor re-capture so the
  per-frame layout doesn't fight it). DefaultTextInput / write-mode focus hooked
  up.
- WrapText now breaks an over-long UNBROKEN token at character boundaries (no
  hyphen), packed onto the current line first — so a spaceless token wraps
  instead of overflowing, and a "You say," prefix stays on the same row as the
  start of the message.
- UiChatView: transcript background + selection highlight use DrawFill (sprite
  bucket) so the transcript text draws ON TOP instead of being dimmed by its own
  translucent rect background.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:24:30 +02:00
Erik
2284a376ae feat(D.2b): write-mode movement gate that preserves autorun
In chat write mode the keyboard belongs to the input — typing "swd" must not
walk the character — but AUTORUN must keep going (the user can chat while
running).

- InputDispatcher.IsActionHeld now returns false while WantCaptureKeyboard is
  set (a focused chat input), the polling-path twin of the existing gate on
  Fired actions. This SUPERSEDES the old per-frame OnUpdate early-return, which
  also killed autorun. Gating here instead lets the movement block keep running,
  so autorun — a separate latched bool ORed into Forward at the call site, not a
  polled key — survives. Test updated to encode the new contract.
- GameWindow: the movement suppress-guard reverts to ImGui-devtools-only (the
  retail write mode no longer early-returns); wires DefaultTextInput = the chat
  input (Tab/Enter activation) and Input.Keyboard for clipboard. Drops the
  one-shot UI-scale diagnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:24:19 +02:00
Erik
367a752078 feat(D.2b): chat input — write mode, selection, clipboard, key-repeat, scroll-clip
Port the retail UIElement_Text editable single-line field:

- Focused = "write mode": draws the gold lit field sprite (0x060011AB, the
  Normal_focussed state) instead of the flat translucent rect; Enter submits
  AND blurs (exits write mode).
- Single-line SELECTION: click-drag, Shift+Arrows, Shift+Home/End, Ctrl+A;
  translucent-blue highlight behind the span; typing/Backspace/Delete/Paste
  replace the selection first.
- CLIPBOARD: Ctrl+C copy, Ctrl+X cut, Ctrl+V paste at the caret (control chars
  stripped — single-line). Wired to the keyboard device for clipboard + Ctrl/
  Shift state.
- Held-key AUTO-REPEAT (Silk delivers one KeyDown per press): Backspace /
  Delete / Left / Right repeat via a 0.4s-delay, ~25/s OnTick timer.
- Horizontal SCROLL + clip: keeps the caret in the field and draws only the
  glyph window that fits inside it, so long input scrolls within the box
  instead of spilling past Send into the 3D world.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:24:09 +02:00
Erik
260507e33c feat(D.2b): channel menu — retail colors, 8-piece border, checkbox align, autosize button
Match the talk-focus menu + button to retail (decomp-verified):

- Menu item text is FILL-ONLY (retail UIElement_Text outlines only when
  SetOutline(true); the talk-focus items don't) — kills the grey halo. Available
  items render white; UNAVAILABLE items render grey (not the salmon colorPink,
  which is a chat-MESSAGE color we'd misapplied). Special items (Squelch /
  Tell-to-Selected) render white. Labels indent past the baked checkbox in the
  row sprite (0600124E empty box / 0600124D white checkmark) instead of
  overlapping it.
- The popup is wrapped in the universal 8-piece window bevel (the menu sprite
  family has no border) and draws in OnDrawOverlay so the translucent chat
  panel no longer greys its right column.
- The button face (0600124D/66, a fixed 46px LED+arrow sprite) is now 3-sliced
  (LED cap / stretch / arrow cap) and autosizes to its label via
  NaturalButtonWidth, so "Chat" fits in the body instead of running into the
  arrow. The status LED (red Normal / green Pressed) is no longer overdrawn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:23:59 +02:00
Erik
ebfeaff840 feat(D.2b): UI render infra — overlay layer, DrawFill, crisp text, write-mode focus
The retail-look render + focus primitives this chat pass builds on:

- TextRenderer: an OVERLAY layer (sprite/rect/text buckets flushed AFTER the
  normal layer) so an open popup composites on top of everything incl. rect
  panel backgrounds; a DrawFill primitive (solid quad via a 1x1 white texture)
  routed through the SPRITE bucket so a panel background draws UNDER its text
  instead of being washed by the later rect bucket; and the text pass now
  disables SampleAlphaToCoverage + Multisample so glyph alpha edges aren't
  dithered into MSAA coverage (the "fuzzy text") — self-contained GL state
  per feedback_render_self_contained_gl_state.
- UiRenderContext.DrawStringDat: snap the line baseline to a whole pixel ONCE
  then add the integer per-glyph offset (retail DrawCharacter takes an int
  pen-Y + schar m_VerticalOffsetBefore) — fixes the "letters dip down" jitter
  at a fractional line origin. Outline pass is now opt-in (retail gates it per
  element via SetOutline; default off = crisp fill-only). Adds DrawFill +
  Begin/EndOverlayLayer.
- UiElement: OnDrawOverlay + DrawOverlays (second traversal), FindRoot (blur
  self), ResetAnchorCapture (re-baseline an anchored element after reflow).
- UiRoot: runs the overlay pass after the main tree; Tab/Enter focuses the
  DefaultTextInput (write-mode activation); a left click on a non-edit target
  blurs the focused input (exit write mode without submitting).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:23:48 +02:00
Erik
828bec5fb5 @
fix(D.2b): point-sample the dat-font atlas so UI text is pixel-crisp

The font glyph atlas was uploaded with bilinear (Linear) min/mag filtering, which
softens the small dat-font glyphs (the menu/button text "blur"). Add a nearest-filter
path to UploadRgba8/GetOrUploadRenderSurface and use it for the font atlases only
(world + other UI surfaces keep bilinear). Combined with the existing per-glyph
pixel-snap, glyph texels now map 1:1 to screen pixels. Sharpens all dat-font text
(transcript, menu, Send/Chat buttons, vitals numbers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-16 12:02:07 +02:00
Erik
621a4ab468 @
fix(D.2b): arrow swap, centered menu text, scrollbar-to-top, Send caption

- scroll arrows: native sprites are opposite (0x06004C6C up / 0x06004C69 down) per live
  visual — swap the assignment, drop the V-flip.
- menu labels centered vertically in each 17px row (was top-aligned, looked corrupt).
- scrollbar pulled up to the panel top so the top arrow meets the window border and the
  max/min button lines up with it (the 6px dat offset left a gap after the resize-bar reclaim).
- Send button: the dat sprite 0x06001915 is a blank gold frame (export-confirmed), so add a
  generic optional Label/LabelFont to UiDatElement and draw "Send" centered on it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-16 11:56:07 +02:00
Erik
bb983ae850 @
feat(D.2b): data-driven channel menu chrome + greying + scroll-arrow fix

Investigation found the menu popup is fully dat-driven (UIElement_Menu::MakePopup
@0x46d310 reads LayoutDesc 0x21000006 elements 0x1000001C/1D/1E — the "stray" top-level
elements). Render the popup from the real sprites instead of a flat rect:
- panel 0x0600124C, item row 0x0600124E, selected row 0x0600124D; 191x17 rows, 2 cols.
- drawing rows as SPRITES also fixes the z-order (a DrawRect bg composited OVER the
  labels; sprites share the labels submission bucket so text lands on top).
- item greying: available channels white, unavailable salmon (colorPink) — static
  approximation (Say/General/Trade/LFG) with an AvailabilityProvider hook for live
  TurbineChat state; unavailable items are inert on click. Ports ResetAllTalkFocusMenuButtons.
- scroll arrows: both dat sprites point down (export-confirmed); V-flip the top button
  so it points up.
Tabs confirmed to have NO digits in retail (blank gold frames) — acdream already matches.

Build + 392 App tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-16 11:33:38 +02:00
Erik
7094a1c847 @
fix(D.2b): channel menu popup opaque + button label tracks selected target

- the popup inherited the chat window 0.75 opacity so the transcript bled through;
  add UiRenderContext.PushAlphaAbsolute and draw the popup at absolute opacity.
- the "Chat" button was hardcoded; it now shows the active talk target (retail
  updates it on selection). Exact textured menu-panel sprite is a follow-up (the
  popup is a keystone UIElement_Menu construct, not in the chat LayoutDesc).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-16 10:38:56 +02:00
Erik
ccaf188e41 @
feat(D.2b): exact retail chat colors from a live cdb dump

Attached cdb to a live retail acclient (PDB-matched) and read the named RGBAColor
constants at acclient 0x81c4a8+ (colorWhite/colorBrightPurple/colorLightBlue/
colorGreen/colorLightRed/colorGrey), used by ChatInterface::BuildChatColorLookupTable
@0x4f31c0. Replaced the approximated RetailChatColor palette with the ground-truth
values: speech=white, tell=colorBrightPurple(1,.498,1), channel=colorLightBlue
(.247,.749,1), system/popup=colorGreen(.5,1,.498), combat=colorLightRed, emote=colorGrey.
Capture scripts saved under tools/cdb/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-16 10:08:42 +02:00
Erik
1da697ec2a @
feat(D.2b): chat polish — typing fix, opacity, scrollbar 3-slice, retail channel menu

Visual-iteration batch (decomp-grounded), each fix verified against the retail screenshots:
- typing: UiElement.HitTest aborted on ClickThrough BEFORE walking children, so the
  ClickThrough UiDatElement panels blocked hit-testing to the input/transcript inside
  them. Check ClickThrough AFTER the child walk (it only gates whether THIS element
  claims the hit). Restores input focus + typing.
- opacity: UiElement.Opacity + a UiRenderContext alpha stack applied to sprite/rect
  draws (text bypasses it, stays sharp); chat frame Opacity=0.75 → translucent chat.
- brown sliver: grow the transcript panel up 9px to cover the dropped resize-bar strip.
- scrollbar: real 3-slice thumb (caps 0x06004C60/66 + tiled mid) + tiled track.
- max/min: shifted one button-width left of the scrollbar (dat right-anchors collide).
- system text now green (retail ChatMessageType 5; was yellow).
- word-wrap: transcript lines wrap to the panel width (greedy, ports GlyphList::Recalculate).
- channel menu reworked to retail gmMainChatUI::InitTalkFocusMenu: "Chat" button + a
  TWO-COLUMN popup of the 14 talk-focus items (Squelch, Tell to Selected, Chat to All,
  Tell to Fellows, ...) on a tan panel; channel items set the active outbound channel.

Build + 392 App tests green. Visual confirmation in progress.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-16 09:37:40 +02:00
Erik
0ec36f6197 fix(D.2b): chat input resolves the live command bus lazily (was bound to null) + register thumb-3-slice row
The live session + its LiveCommandBus are created after the retail-UI block in
OnLoad, so binding the bus by value captured NullCommandBus and silently dropped
outbound chat. Pass a Func<ICommandBus> resolved at submit time (mirrors how the
ImGui ChatPanel re-reads the bus each frame).

AP-41: scrollbar thumb drawn as single stretched tile (0x06004C63) instead of
retail's 3-slice top-cap/middle/bottom-cap — acknowledged in UiChatScrollbar.cs:37,
registered per the divergence-register rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:24:44 +02:00
Erik
12ab9663d2 feat(D.2b): cut GameWindow over to the data-driven chat window
Replace the hand-authored chat block (UiNineSlicePanel + inline UiChatView
+ local BuildRetailChatLines/RetailChatColor statics) with
ChatWindowController.Bind(LayoutDesc 0x21000006) — the same LayoutImporter
path as the vitals window.  The controller places UiChatView (transcript) +
UiChatInput (text entry, on-submit) + UiChatScrollbar + UiChannelMenu inside
the dat-authored chrome.  The dead local statics are deleted.

Wired to _commandBus (same LiveCommandBus as the ImGui ChatPanel) so
type+Enter dispatches SendChatCmd server-ward.  Transcript keyboard set from
_uiHost.Keyboard (set by WireKeyboard above the chat block) for Ctrl+C/Ctrl+A.

Divergence register: added AD-28 (two-widget split vs UIElement_Text),
AP-38 (no in-element word-wrap), AP-39 (per-line colour vs per-glyph runs),
AP-40 (no opacity fade / shared vitalsDatFont), TS-30 (tab buttons no-op),
TS-31 (no squelch); updated IA-15 to cover both vitals + chat importer paths.

Build: 0 errors/warnings.  Tests: 392 passed, 1 skipped (expected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:15:04 +02:00
Erik
9d9e036e4c feat(D.2b): ChatWindowController — bind chat LayoutDesc, place widgets, route chat
Implements Task G2: binds the imported chat LayoutDesc (0x21000006) to live
behavior, the acdream analogue of retail ChatInterface + gmMainChatUI::PostInit.

- UiDatElement: add OnClick hook + OnEvent override so Send/max-min buttons
  can be wired by a controller without needing a dedicated widget type.
- ChatWindowController.Bind: reads transcript (0x10000011) and input
  (0x10000016) rects from the raw ElementInfo tree (factory skips them as
  Type-12/no-media), places UiChatView under the transcript panel and
  UiChatInput under the input bar; replaces the imported scrollbar track
  (0x10000012) with UiChatScrollbar driving UiChatView.Scroll; replaces
  the channel menu placeholder (0x10000014) with UiChannelMenu; wires
  Send button and max/min toggle via the new OnClick hook.
  ChatCommandRouter.Submit routes all input through the existing pipeline.
- 6 smoke tests: Bind returns non-null, Transcript is child of panel,
  Input is child of bar, Input.OnSubmit publishes SendChatCmd, channel
  change updates submit channel, returns null when panels missing.

Build: 0 errors. Test suite: 392 passed / 1 skipped / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:04:57 +02:00
Erik
6e6339b026 feat(D.2b): importer renders Type-12-with-sprites + carries DefaultState
Task G1: two gaps blocked chat window static sprite elements from rendering.

Change 1 — DatWidgetFactory: only skip Type-12 elements that have no own
state media (pure style prototypes). A Type-12 element that carries sprites
(e.g. a chat Send button whose derived Type-0 element inherited Type 12 from
its base prototype) now renders as a UiDatElement.

Change 2 — ElementInfo: add DefaultStateName field (string, default "").

Change 3 — LayoutImporter.ToInfo: read ElementDesc.DefaultState.ToString()
into DefaultStateName; normalize Undef/Undefined/0 sentinels to "".

Change 4 — ElementReader.Merge: inherit DefaultStateName (derived wins if
non-empty, else base).

Change 5 — UiDatElement ctor: initialize ActiveState to DefaultStateName
when set; else "Normal" when a Normal-state sprite is present (retail's
implicit default for buttons/tabs); else "" (DirectState). This makes the
Send button, max/min button, and numbered tabs render their default sprite
without requiring explicit state assignment at runtime.

Vitals neutrality: all vitals chrome/grip elements carry DirectState-only
sprites with no "Normal" named state and DefaultStateName="" (Undef in dat),
so their ActiveState stays "" and their existing conformance tests are
unaffected. Vitals text labels (Type 0→12 via Merge, no StateMedia) are
still skipped by the refined Type-12 guard (StateMedia.Count==0).

Tests: 4 new tests (2 in DatWidgetFactoryTests, 3 in UiDatElementTests).
All 386 pass; 387 total (1 pre-existing skip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:54:37 +02:00
Erik
c2170ab18f feat(D.2b): UiChannelMenu — channel selector popup (UIElement_Menu port)
Port of retail gmMainChatUI::InitTalkFocusMenu @0x4cdc50 + HandleSelection
@0x4cd540 → SetTalkFocus. Button shows active channel label; click opens a
12-item popup that extends UPWARD (chat sits at screen bottom); selecting an
entry calls OnChannelChanged and updates Selected. BitmapFont? Font uses the
fully-qualified type name to match UiChatInput convention. Includes 6 xunit
tests covering channel table shape, default selection, and popup-pick routing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:42:22 +02:00
Erik
bcc45d668e feat(D.2b): UiChatInput — editable field, caret, 100-entry history (UIElement_Text port)
Ports retail UIElement_Text editable one-line mode (caret = glyph index;
caret pixel-X = sum of glyph advances via UiDatFont) and ChatInterface's
100-entry command history (up/down arrow; sentinel -1 = live line).
Submit (Enter/KeypadEnter) fires OnSubmit callback, clears, pushes history.
Draws via DrawStringDat (dat font) or DrawString (BitmapFont) fallback.
AcceptsFocus=true + IsEditControl=true so UiRoot routes Char/KeyDown to it
and suppresses global hotkeys while typing. 6 new tests, all green.

Decomp refs: UIElement_Text::MoveCursor @0x468d00,
             UIElement_Text::FindPixelsFromPos @0x472b40,
             ChatInterface::ProcessCommand @0x4f5100

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:36:44 +02:00
Erik
2940b4e3b2 feat(D.2b): UiChatScrollbar — track/thumb/buttons driving UiScrollable
Implements the right-side chat scrollbar widget. Ports retail
UIElement_Scrollbar::UpdateLayout @0x4710d0 (thumb sizing + placement)
and HandleButtonClick @0x470e90 (step ±1 line, page on track click).

Dat element ids sourced from chat LayoutDesc 0x21000006 (base layout
0x2100003E): up-button sprite 0x06004C69, down-button 0x06004C6C, track
0x06004C5F, thumb middle 0x06004C63. Up/down buttons occupy the top and
bottom ButtonH (16px) regions of the widget height, matching element
positions Y=0 and Y=32 in the base scrollbar template.

Adds 6 pure ThumbRect tests (no GL): sizing, clamping to MinThumb,
position at start/mid/end, no-overflow full-fill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:31:12 +02:00
Erik
0eaef67b9d feat(D.2b): UiChatView drives the shared UiScrollable model
Replace the ad-hoc _scroll float with a public UiScrollable instance.
OnDraw feeds ContentHeight/ViewHeight/LineHeight into the model each
frame and reads baseY = bottom - contentH + (MaxScroll - ScrollY) —
the (MaxScroll-ScrollY) inversion reconciles UiScrollable's top-origin
convention (0=oldest, MaxScroll=newest) with the visual layout (newest
at bottom). The wheel handler routes through ScrollByLines with a sign
flip so wheel-up still reveals older lines. _pinBottom tracks whether
the view is at the end and calls ScrollToEnd() each draw to auto-scroll
new messages. ClampScroll static method kept — referenced by existing
tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:23:17 +02:00
Erik
9f273c9343 feat(D.2b): UiScrollable — pixel scroll model (UIElement_Scrollable port)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:19:29 +02:00
Erik
7552dcba39 feat(D.2b): UiChatView dat-font transcript + 1-line wheel quantum
- Add `DatFont` property (UiDatFont?): when set, OnDraw uses
  ctx.DrawStringDat + datFont.MeasureWidth for all transcript lines;
  BitmapFont path unchanged as fallback when DatFont is null.
- Cache `_lastDatFont` alongside `_lastFont` so HitChar hit-tests the
  same advance source that drew the last frame.
- HitChar prefers `_lastDatFont` (via UiDatFont.GlyphAdvance) over
  `_lastFont` (via bf.Advance) for column resolution, keeping
  drag-select and Ctrl+C accurate with the dat font.
- Scroll event handler uses DatFont?.LineHeight first, so the wheel
  quantum stays correct when the dat font has a different line height.
- WheelLines 3f → 1f: retail UIElement_Text::HandleMouseWheel
  (@0x471450) advances one line per notch; our 3-line quantum was
  wrong.
- Add UiChatViewDatFontTests: pins GlyphAdvance formula
  (Before+Width+After = 10) and CharIndexAt dat-advance integration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:14:56 +02:00
Erik
50883e445b feat(D.2b): extract ChatCommandRouter — shared chat submit pipeline
Both the ImGui devtools ChatPanel and the upcoming retail chat window
now route through ChatCommandRouter.Submit so command handling lives in
one place. The ChatPanel inline block (TryHandleClientCommand / EqAny /
BuildHelpText) is deleted; ChatCommandRouter carries the same logic
verbatim. ChatPanel.Render becomes a 2-line delegate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:09:27 +02:00
Erik
3d25e8760f @
docs(D.2b): chat-window re-drive implementation plan (8 tasks A-H)

TDD task breakdown for the data-driven chat window: ChatCommandRouter extraction
(A), UiChatView dat-font (B), UiScrollable + wire-in (C/C2), UiChatScrollbar (D),
UiChatInput (E), UiChannelMenu (F), ChatWindowController bind/route (G), GameWindow
cutover + divergence rows (H). Each ported widget cites its retail class::method.

Plan: docs/superpowers/plans/2026-06-15-chat-window-redrive.md
Spec: docs/superpowers/specs/2026-06-15-chat-window-redrive-design.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-15 22:04:35 +02:00
Erik
26cb34f126 @
docs(D.2b): chat-window re-drive design spec + list-ui-layouts research tool

Plan-2 chat piece of the LayoutDesc importer. Identifies the chat window as
LayoutDesc 0x21000006 (gmMainChatUI, element class 0x10000041) and grounds a
faithful, data-driven re-drive in the named retail decomp (ChatInterface +
gmMainChatUI + UIElement_Text/_Scrollable/_Scrollbar/_Menu) plus a user-provided
retail screenshot.

Design (full-faithful scope, user-approved):
- transcript = UIElement_Text 0x10000011 (dat font, bottom-pinned, 10k behead cap,
  pixel scroll, 1 line/wheel-notch)
- scrollbar = right-side track 0x10000012 + thumb 0x1000048c + up/down
- input = editable UIElement_Text 0x10000016 (caret, 100-entry history, Enter/Send)
- channel menu = UIElement_Menu 0x10000014 ("Chat" selector -> active channel)
- shared ChatCommandRouter extracted from ChatPanel
- screenshot correction: the four 0x10000522-525 left-edge elements are the
  numbered CHAT TABS (1-4), not scroll buttons (a research-agent inference the
  retail screenshot refutes)
- deferred (need non-UI plumbing, each gets a divergence row): tab switching/
  filtering, squelch, clickable name-tags, in-element word-wrap, styled runs,
  font config, opacity transition

Tooling: AcDream.Cli `list-ui-layouts <datdir> [0xRootType]` — read-only index of
every UI LayoutDesc by root element class + size + element-Type histogram; how the
chat layout was located (root type 0x10000041). Reusable for future panel re-drives.

Spec: docs/superpowers/specs/2026-06-15-chat-window-redrive-design.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-15 19:38:27 +02:00
Erik
50758d4795 docs(D.2b): chat-window re-drive session handoff (Plan 2 chat piece)
Captures: current hand-authored chat window (UiNineSlicePanel + UiChatView,
read-only, debug font), the importer toolkit to reuse, the retail gmMainChatUI
oracles, the open design questions (scope / behavioral widgets / dat font), and
the first research step (find the chat LayoutDesc id). Resume via brainstorming.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:07:05 +02:00
Erik
0474feb6ca docs(D.2b): correct roadmap/plan — vitals window IS resizable (resize shipped 8aa643f)
The earlier 'not resizable / fixed-size' note was wrong (inverted edge-flag
reading). Resize shipped: dat edge-anchors reflow per UIElement::UpdateForParentSizeChange.
Noted the two number-render fixes (submission-order + glyph pixel-snap).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:35:29 +02:00
Erik
34243f2c26 fix(D.2b): pixel-snap dat-font glyphs so vitals numbers stay sharp on resize
DrawStringDat placed each glyph quad at the raw (often fractional) pen/origin.
When a bar resizes to a fractional width, the centered cur/max number lands on a
sub-pixel x and the glyph atlas (linear-filtered) smears — the 'unsharp at certain
sizes' artifact. Round each glyph's destination to whole pixels (the pen keeps its
true fractional advance, so spacing is unaffected) — matches retail blitting glyphs
to integer dest. User-confirmed sharp across resize widths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:31:58 +02:00
Erik
43064bab09 fix(D.2b): draw UI sprites in submission order so stamina/mana numbers render
TextRenderer batched sprites per-texture and drew each texture's whole buffer at
its FIRST-insertion point. The dat-font glyph atlas is one shared texture used by
all three vital numbers; it first appeared at the health bar, so all three numbers
were emitted right after the health bars — then the stamina + mana bar sprites
painted over their own numbers (only health survived). Replaced the per-texture
dictionary with submission-ordered segments (consecutive same-texture quads still
batch); each meter's number now draws after its own bars. The renderer's own
comment had predicted this break once bars became sprites (importer did that).
Removed the temporary UiMeter label diagnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:27:13 +02:00
Erik
8aa643f3e0 fix(D.2b): correct edge-anchor mapping (RightEdge==1=stretch) + enable vitals horizontal resize
ToAnchors was inverted vs retail UIElement::UpdateForParentSizeChange @0x00462640:
stretch is RightEdge==1 (not ==2/==4), LeftEdge==2 = track-right. Verified against
all 19 vitals fixture pieces. Enables Resizable/ResizeX on the importer vitals root
(the prior 'dat is fixed-size' conclusion was wrong). At-rest render unchanged
(anchors only fire on resize). Added a 160->200 resize conformance test.
Also fixed DatWidgetFactoryTests.RectAndAnchors_SetFromElementInfo which encoded
the old inverted model (Right=2 expecting Right anchor; corrected to Right=1).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:05:04 +02:00