feat(ui): complete retail creature appraisal details

Port the separate creature rating list, layer the animated preview between authored row chrome and text, and follow selection while the examination floaty is visible. Preserve the remaining item-preview and font-state gaps in AP-110.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-23 14:04:19 +02:00
parent 7eaa68a5f4
commit d96ea2de98
11 changed files with 578 additions and 61 deletions

View file

@ -1,4 +1,6 @@
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Core.Items;
using AcDream.Core.Net.Messages;
namespace AcDream.App.Tests.UI.Layout;
@ -45,6 +47,53 @@ public sealed class CreatureAppraisalRowsTests
Assert.Equal(CreatureAppraisalValueStyle.Normal, rows[1].Style);
}
[Fact]
public void ExtraRatingsFollowRetailGroupingFormattingAndSeparators()
{
var properties = new PropertyBundle();
properties.Ints[0x133u] = 35;
properties.Ints[0x139u] = 7;
properties.Ints[0x13Au] = 4;
properties.Ints[0x134u] = 12;
properties.Ints[0x13Bu] = 3;
properties.Ints[0x13Cu] = 2;
properties.Ints[0x15Eu] = 9;
properties.Ints[0x15Fu] = 6;
IReadOnlyList<CreatureAppraisalRow> rows =
CreatureAppraisalRows.BuildExtra(properties);
Assert.Equal(5, rows.Count);
Assert.Equal(("", ""), (rows[0].Label, rows[0].Value));
Assert.Equal(
("Dmg/CritDmg", "Rating: 35/4"),
(rows[1].Label, rows[1].Value));
Assert.Equal(
("Dmg/CritDmg", "Resist: 12/2"),
(rows[2].Label, rows[2].Value));
Assert.Equal(
("DoT/Life:", "Resist: 9/6"),
(rows[3].Label, rows[3].Value));
Assert.Equal(("", ""), (rows[4].Label, rows[4].Value));
}
[Fact]
public void CritOnlyTriggersRatingRowWhileHealingBoostAloneDoesNot()
{
var critOnly = new PropertyBundle();
critOnly.Ints[0x139u] = 8;
IReadOnlyList<CreatureAppraisalRow> rows =
CreatureAppraisalRows.BuildExtra(critOnly);
Assert.Equal(3, rows.Count);
Assert.Equal("Rating: 0/0", rows[1].Value);
var healingOnly = new PropertyBundle();
healingOnly.Ints[0x143u] = 20;
Assert.Empty(CreatureAppraisalRows.BuildExtra(healingOnly));
}
[Fact]
public void AuthoredRowTemplateCarriesRetailOverlappingLabelValueGeometry()
{
@ -76,6 +125,43 @@ public sealed class CreatureAppraisalRowsTests
Assert.Equal(string.Empty, resolver.Resolve(999));
}
[Fact]
public void LayeredTemplateSeparatesChromeFromForegroundText()
{
var templates = new CreatureAppraisalRowTemplateFactory(
FixtureLoader.LoadExaminationRowTemplateInfos(),
_ => (0u, 0, 0),
defaultFont: null);
var row = new CreatureAppraisalRow(
"Strength",
"120",
CreatureAppraisalValueStyle.Normal);
UiTemplateListSlot background = templates.Create(
row,
CreatureAppraisalRowLayer.Background);
UiTemplateListSlot foreground = templates.Create(
row,
CreatureAppraisalRowLayer.Foreground);
Assert.Empty(((UiText)background.Content.FindElement(
CreatureAppraisalRowTemplateFactory.LabelId)!).LinesProvider());
Assert.Empty(((UiText)background.Content.FindElement(
CreatureAppraisalRowTemplateFactory.ValueId)!).LinesProvider());
Assert.False(
Assert.IsType<UiDatElement>(foreground.Content.Root).MediaVisible);
Assert.Equal(
"Strength",
Assert.Single(((UiText)foreground.Content.FindElement(
CreatureAppraisalRowTemplateFactory.LabelId)!)
.LinesProvider()).Text);
Assert.Equal(
"120",
Assert.Single(((UiText)foreground.Content.FindElement(
CreatureAppraisalRowTemplateFactory.ValueId)!)
.LinesProvider()).Text);
}
private static AppraiseInfoParser.CreatureProfile Profile(
uint health,
uint healthMax,