fix(ui): render live component counts

Port retail text-surface clipping so the 15px component count field remains visible under the 16px DAT font. Cover SetStackSize decrements and final component removal while preserving desired restock values.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-15 18:11:02 +02:00
parent a1175e6aac
commit 0527325b25
7 changed files with 217 additions and 36 deletions

View file

@ -323,9 +323,12 @@ public sealed class SpellbookWindowControllerTests
Assert.Equal("Copper Scarab", Assert.Single(
Assert.IsType<UiText>(copper.Content.FindElement(
ComponentBookTemplateFactory.NameId)).LinesProvider()).Text);
Assert.Equal("2", Assert.Single(
Assert.IsType<UiText>(copper.Content.FindElement(
ComponentBookTemplateFactory.OwnedCountId)).LinesProvider()).Text);
UiText ownedCount = Assert.IsType<UiText>(copper.Content.FindElement(
ComponentBookTemplateFactory.OwnedCountId));
Assert.Equal((224f, 0f, 60f, 15f),
(ownedCount.Left, ownedCount.Top, ownedCount.Width, ownedCount.Height));
Assert.False(ownedCount.OneLine);
Assert.Equal("2", Assert.Single(ownedCount.LinesProvider()).Text);
UiField desiredField = Assert.IsType<UiField>(copper.Content.FindElement(
ComponentBookTemplateFactory.DesiredCountId));
Assert.Equal((224f, 15f, 60f, 15f),
@ -361,6 +364,27 @@ public sealed class SpellbookWindowControllerTests
Assert.False(copper.Selected);
selection.Select(0xDEADBEEFu, SelectionChangeSource.World);
Assert.False(amaranth.Selected);
// ACE consumes components through SetStackSize (0x0197). The object table
// publishes ObjectUpdated; the controller rebuilds the tracker total on Tick.
Assert.True(objects.UpdateStackSize(10u, stackSize: 1, value: 0));
controller.Tick();
UiTemplateListSlot updatedCopper = Assert.IsType<UiTemplateListSlot>(list.GetItem(1));
Assert.Equal("1", Assert.Single(
Assert.IsType<UiText>(updatedCopper.Content.FindElement(
ComponentBookTemplateFactory.OwnedCountId)).LinesProvider()).Text);
Assert.Equal("21", Assert.IsType<UiField>(updatedCopper.Content.FindElement(
ComponentBookTemplateFactory.DesiredCountId)).Text);
// Consuming the final item removes its object. The row disappears while the
// category remains because Diamond Scarab is still owned.
Assert.True(objects.Remove(10u));
controller.Tick();
Assert.DoesNotContain(
Enumerable.Range(0, list.GetNumUIItems())
.Select(list.GetItem)
.OfType<UiTemplateListSlot>(),
slot => slot.EntryId == 100u);
}
private static SpellbookWindowController? Bind(

View file

@ -277,4 +277,36 @@ public class UiTextTests
Assert.Equal(9f, y);
}
[Fact]
public void LineIntersectsViewport_PartialLineRemainsDrawable()
{
// Component owned-count boxes are 15px tall while Font 0x40000000
// advances 16px. Retail clips the final pixel instead of dropping the line.
Assert.True(UiText.LineIntersectsViewport(
lineTop: 0f,
lineHeight: 16f,
viewportTop: 0f,
viewportBottom: 15f));
Assert.True(UiText.LineIntersectsViewport(
lineTop: -1f,
lineHeight: 16f,
viewportTop: 0f,
viewportBottom: 15f));
}
[Theory]
[InlineData(-16f, 16f)]
[InlineData(15f, 16f)]
public void LineIntersectsViewport_FullyClippedLineIsSkipped(
float lineTop,
float lineHeight)
{
Assert.False(UiText.LineIntersectsViewport(
lineTop,
lineHeight,
viewportTop: 0f,
viewportBottom: 15f));
}
}