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

@ -4,6 +4,7 @@ using System.Text;
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Net.Messages;
using AcDream.Core.Selection;
using AcDream.Core.Spells;
namespace AcDream.App.UI.Layout;
@ -29,6 +30,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
public const uint CreaturePanelId = 0x10000140u;
public const uint CreatureViewportId = 0x10000148u;
public const uint CreatureStatsListId = 0x10000149u;
public const uint CreatureExtraListId = 0x10000335u;
public const uint CreatureDisplayNameId = 0x1000014Eu;
public const uint CreatureLevelValueId = 0x1000014Cu;
public const uint SpellPanelId = 0x10000153u;
@ -41,12 +43,14 @@ public sealed class AppraisalUiController : IRetainedPanelController
private readonly ImportedLayout _layout;
private readonly ClientObjectTable _objects;
private readonly ItemInteractionController _interaction;
private readonly SelectionState _selection;
private readonly CombatState _combat;
private readonly Spellbook _spellbook;
private readonly Func<string> _playerName;
private readonly Action<uint, string> _sendSetInscription;
private readonly Action<string> _systemMessage;
private readonly Action _show;
private readonly Action _closeWindow;
private readonly UiElement _itemPanel;
private readonly UiElement _creaturePanel;
private readonly UiElement? _spellPanel;
@ -57,7 +61,8 @@ public sealed class AppraisalUiController : IRetainedPanelController
private readonly UiText? _signature;
private readonly UiDatElement? _inscriptionBackground;
private readonly UiButton? _close;
private readonly UiItemList? _creatureStats;
private readonly CreatureAppraisalLayeredList? _creatureStats;
private readonly CreatureAppraisalLayeredList? _creatureExtra;
private readonly CreatureAppraisalRowTemplateFactory? _creatureRowTemplates;
private readonly CreatureDisplayNameResolver _creatureNames;
private AppraisalView _activeView;
@ -71,6 +76,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
private string _scribeName = string.Empty;
private string _oldInscription = string.Empty;
private bool _presentationInscribable;
private bool _windowVisible;
private double _refreshElapsed;
private bool _disposed;
@ -78,6 +84,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
ImportedLayout layout,
ClientObjectTable objects,
ItemInteractionController interaction,
SelectionState selection,
CombatState combat,
Spellbook spellbook,
Func<string> playerName,
@ -95,12 +102,14 @@ public sealed class AppraisalUiController : IRetainedPanelController
_layout = layout;
_objects = objects;
_interaction = interaction;
_selection = selection;
_combat = combat;
_spellbook = spellbook;
_playerName = playerName;
_sendSetInscription = sendSetInscription;
_systemMessage = systemMessage;
_show = show;
_closeWindow = close;
_itemPanel = itemPanel;
_creaturePanel = creaturePanel;
_spellPanel = layout.FindElement(SpellPanelId);
@ -150,24 +159,25 @@ public sealed class AppraisalUiController : IRetainedPanelController
}
if (creatureRowTemplates is not null
&& layout.FindElement(CreatureStatsListId) is { } statsHost)
&& layout.FindElement(CreatureStatsListId) is { } statsHost
&& layout.FindElement(CreatureExtraListId) is { } extraHost
&& layout.FindElement(CreatureViewportId) is UiViewport viewport)
{
_creatureStats = statsHost as UiItemList
?? new UiItemList
{
Width = statsHost.Width,
Height = statsHost.Height,
Anchors = AnchorEdges.Left | AnchorEdges.Top
| AnchorEdges.Right | AnchorEdges.Bottom,
};
if (!ReferenceEquals(_creatureStats, statsHost))
statsHost.AddChild(_creatureStats);
_creatureStats.Flush();
_creatureStats.Columns = 1;
_creatureStats.CellWidth = creatureRowTemplates.Width;
_creatureStats.CellHeight = creatureRowTemplates.Height;
_creatureStats = CreatureAppraisalLayeredList.Create(
_creaturePanel,
statsHost,
viewport,
creatureRowTemplates,
backgroundZOrder: viewport.ZOrder - 2);
_creatureExtra = CreatureAppraisalLayeredList.Create(
_creaturePanel,
extraHost,
viewport,
creatureRowTemplates,
backgroundZOrder: viewport.ZOrder - 1);
}
_selection.Changed += HandleSelectionChanged;
SetActiveView(AppraisalView.Item);
}
@ -178,6 +188,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
ImportedLayout layout,
ClientObjectTable objects,
ItemInteractionController interaction,
SelectionState selection,
CombatState combat,
Spellbook spellbook,
Func<string> playerName,
@ -191,6 +202,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
ArgumentNullException.ThrowIfNull(layout);
ArgumentNullException.ThrowIfNull(objects);
ArgumentNullException.ThrowIfNull(interaction);
ArgumentNullException.ThrowIfNull(selection);
ArgumentNullException.ThrowIfNull(combat);
ArgumentNullException.ThrowIfNull(spellbook);
ArgumentNullException.ThrowIfNull(playerName);
@ -209,6 +221,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
layout,
objects,
interaction,
selection,
combat,
spellbook,
playerName,
@ -274,7 +287,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
public void Tick(double deltaSeconds)
{
if (!_layout.Root.Visible
if (!_windowVisible
|| _activeView is not (AppraisalView.Creature or AppraisalView.Character)
|| _combat.CurrentMode == CombatMode.NonCombat
|| CurrentObjectId == 0)
@ -522,18 +535,12 @@ public sealed class AppraisalUiController : IRetainedPanelController
if (_creatureStats is null || _creatureRowTemplates is null)
return;
using (_creatureStats.DeferLayout())
{
_creatureStats.Flush();
if (appraisal.CreatureProfile is not { } profile)
return;
foreach (CreatureAppraisalRow row in CreatureAppraisalRows.Build(
profile,
appraisal.Success))
{
_creatureStats.AddItem(_creatureRowTemplates.Create(row));
}
}
_creatureStats.Rebuild(
appraisal.CreatureProfile is { } profile
? CreatureAppraisalRows.Build(profile, appraisal.Success)
: Array.Empty<CreatureAppraisalRow>());
_creatureExtra?.Rebuild(
CreatureAppraisalRows.BuildExtra(appraisal.Properties));
}
private void ConfigureScrollableText(
@ -577,11 +584,13 @@ public sealed class AppraisalUiController : IRetainedPanelController
if (_layout.FindElement(id) is UiText text)
text.LinesProvider = static () => Array.Empty<UiText.Line>();
_creatureStats?.Flush();
_creatureExtra?.Flush();
}
private void ResetCreatureScroll()
{
_creatureStats?.Scroll.SetScrollY(0);
_creatureStats?.ResetScroll();
_creatureExtra?.ResetScroll();
foreach (UiText text in Descendants(_creaturePanel).OfType<UiText>())
text.Scroll.SetScrollY(0);
}
@ -647,6 +656,29 @@ public sealed class AppraisalUiController : IRetainedPanelController
public void OnShown()
{
_windowVisible = true;
}
public void OnHidden() => _windowVisible = false;
/// <summary>
/// Port of <c>gmExaminationUI::RecvNotice_SelectionChanged @ 0x004AB3D0</c>.
/// Retail initializes <c>m_examineNewlySelectedItem</c> to one: while the
/// floaty window is visible, a new selection is appraised immediately and
/// clearing selection hides the window.
/// </summary>
private void HandleSelectionChanged(SelectionTransition transition)
{
if (!_windowVisible || _disposed)
return;
if (transition.SelectedObjectId is uint objectId && objectId != 0u)
{
_interaction.ExamineSelectedOrEnterMode(objectId);
return;
}
_closeWindow();
}
public void Dispose()
@ -654,6 +686,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
if (_disposed)
return;
_disposed = true;
_selection.Changed -= HandleSelectionChanged;
if (_close is not null)
_close.OnClick = null;
if (_inscriptionField is not null)