feat(ui): port retail appraisal panel
Preserve retail's one-pending-appraisal busy lifetime, parse the complete gated response, and mount the authored examination layout in the shared main-panel host. Keep known 3D preview and inscription-write gaps explicit in AP-110.
This commit is contained in:
parent
6b1ae4fb76
commit
643cdfe66e
24 changed files with 17132 additions and 40 deletions
|
|
@ -301,15 +301,54 @@ public sealed class ItemInteractionControllerTests
|
|||
|
||||
Assert.True(h.Controller.ExamineSelectedOrEnterMode(Player));
|
||||
Assert.Equal(new[] { Player }, h.Examines);
|
||||
Assert.Equal(1, h.Controller.BusyCount);
|
||||
|
||||
Assert.True(h.Controller.ExamineSelectedOrEnterMode(0u));
|
||||
Assert.Equal(InteractionModeKind.Examine, h.Controller.InteractionState.Current.Kind);
|
||||
Assert.Equal(ItemPrimaryClickResult.ConsumedSuccess,
|
||||
h.Controller.OfferPrimaryClick(Pack));
|
||||
Assert.Equal(new[] { Player, Pack }, h.Examines);
|
||||
Assert.Equal(1, h.Controller.BusyCount);
|
||||
Assert.False(h.Controller.IsAnyTargetModeActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppraisalResponse_releasesOneBusyReferenceAndAcceptsCurrentRefresh()
|
||||
{
|
||||
var h = new Harness();
|
||||
|
||||
Assert.True(h.Controller.ExamineSelectedOrEnterMode(Player));
|
||||
Assert.True(h.Controller.ExamineSelectedOrEnterMode(Pack));
|
||||
Assert.Equal(1, h.Controller.BusyCount);
|
||||
|
||||
Assert.False(h.Controller.AcceptAppraisalResponse(Player).Accepted);
|
||||
Assert.Equal(1, h.Controller.BusyCount);
|
||||
|
||||
var first = h.Controller.AcceptAppraisalResponse(Pack);
|
||||
Assert.True(first.Accepted);
|
||||
Assert.True(first.FirstResponse);
|
||||
Assert.Equal(0, h.Controller.BusyCount);
|
||||
Assert.Equal(Pack, h.Controller.CurrentAppraisalId);
|
||||
|
||||
var refresh = h.Controller.AcceptAppraisalResponse(Pack);
|
||||
Assert.True(refresh.Accepted);
|
||||
Assert.False(refresh.FirstResponse);
|
||||
Assert.Equal(0, h.Controller.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RefreshCurrentAppraisal_sendsWithoutAcquiringBusyReference()
|
||||
{
|
||||
var h = new Harness();
|
||||
Assert.True(h.Controller.ExamineSelectedOrEnterMode(Pack));
|
||||
Assert.True(h.Controller.AcceptAppraisalResponse(Pack).Accepted);
|
||||
|
||||
Assert.True(h.Controller.RefreshCurrentAppraisal());
|
||||
|
||||
Assert.Equal(new[] { Pack, Pack }, h.Examines);
|
||||
Assert.Equal(0, h.Controller.BusyCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelfTarget_sendsUseWithTargetAndClearsTargetMode()
|
||||
{
|
||||
|
|
|
|||
192
tests/AcDream.App.Tests/UI/Layout/AppraisalUiControllerTests.cs
Normal file
192
tests/AcDream.App.Tests/UI/Layout/AppraisalUiControllerTests.cs
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
public sealed class AppraisalUiControllerTests
|
||||
{
|
||||
private const uint ObjectId = 0x50000001u;
|
||||
|
||||
[Fact]
|
||||
public void ItemResponse_UsesAuthoredItemSubviewTitleAndScrollbars()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadExamination();
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = ObjectId,
|
||||
Name = "Atlan Weapon",
|
||||
Type = ItemType.MeleeWeapon,
|
||||
Value = 1250,
|
||||
Burden = 350,
|
||||
});
|
||||
var sent = new List<uint>();
|
||||
using var interaction = NewInteraction(objects, sent);
|
||||
var combat = new CombatState();
|
||||
int shown = 0;
|
||||
int closed = 0;
|
||||
using AppraisalUiController controller = AppraisalUiController.Bind(
|
||||
layout,
|
||||
objects,
|
||||
interaction,
|
||||
combat,
|
||||
new Spellbook(),
|
||||
() => shown++,
|
||||
() => closed++)!;
|
||||
|
||||
Assert.True(interaction.ExamineSelectedOrEnterMode(ObjectId));
|
||||
var properties = new PropertyBundle();
|
||||
properties.Strings[16u] = "A finely balanced weapon.";
|
||||
properties.Strings[7u] = "Remember the fallen.";
|
||||
properties.Strings[8u] = "Tester";
|
||||
|
||||
Assert.True(controller.Apply(Parsed(properties)));
|
||||
|
||||
Assert.Equal(1, shown);
|
||||
Assert.Equal(AppraisalView.Item, controller.ActiveView);
|
||||
Assert.Equal(0, interaction.BusyCount);
|
||||
UiText title = Assert.IsType<UiText>(
|
||||
layout.FindElement(AppraisalUiController.TitleId));
|
||||
Assert.Equal("Atlan Weapon", Assert.Single(title.LinesProvider()).Text);
|
||||
UiText itemText = Assert.IsType<UiText>(
|
||||
layout.FindElement(AppraisalUiController.ItemTextId));
|
||||
string report = string.Join('\n', itemText.LinesProvider().Select(line => line.Text));
|
||||
Assert.Contains("Value: 1,250", report);
|
||||
Assert.Contains("Burden: 350", report);
|
||||
Assert.Contains("A finely balanced weapon.", report);
|
||||
UiScrollbar scrollbar = Assert.IsType<UiScrollbar>(
|
||||
layout.FindElement(AppraisalUiController.ItemScrollbarId));
|
||||
Assert.Same(itemText.Scroll, scrollbar.Model);
|
||||
UiField inscription = Assert.IsType<UiField>(
|
||||
layout.FindElement(AppraisalUiController.InscriptionTextId));
|
||||
Assert.Contains("Remember the fallen", inscription.Text);
|
||||
Assert.False(inscription.Enabled);
|
||||
UiText signature = Assert.IsType<UiText>(
|
||||
layout.FindElement(AppraisalUiController.SignatureTextId));
|
||||
Assert.Equal("Inscribed by Tester", Assert.Single(signature.LinesProvider()).Text);
|
||||
|
||||
((UiButton)layout.FindElement(AppraisalUiController.CloseId)!).OnClick!.Invoke();
|
||||
Assert.Equal(1, closed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreatureResponse_SelectsCreatureSubviewAndRefreshesInCombatWithoutBusy()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadExamination();
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = ObjectId,
|
||||
Name = "Drudge Slinker",
|
||||
Type = ItemType.Creature,
|
||||
});
|
||||
var sent = new List<uint>();
|
||||
using var interaction = NewInteraction(objects, sent);
|
||||
var combat = new CombatState();
|
||||
int shown = 0;
|
||||
using AppraisalUiController controller = AppraisalUiController.Bind(
|
||||
layout,
|
||||
objects,
|
||||
interaction,
|
||||
combat,
|
||||
new Spellbook(),
|
||||
() => shown++,
|
||||
() => { })!;
|
||||
|
||||
interaction.ExamineSelectedOrEnterMode(ObjectId);
|
||||
var properties = new PropertyBundle();
|
||||
properties.Ints[25u] = 12;
|
||||
var creature = new AppraiseInfoParser.CreatureProfile(
|
||||
Flags: 0,
|
||||
Health: 80,
|
||||
HealthMax: 100,
|
||||
Strength: null,
|
||||
Endurance: null,
|
||||
Quickness: null,
|
||||
Coordination: null,
|
||||
Focus: null,
|
||||
Self: null,
|
||||
Stamina: null,
|
||||
Mana: null,
|
||||
StaminaMax: null,
|
||||
ManaMax: null,
|
||||
AttributeHighlights: null,
|
||||
AttributeColors: null);
|
||||
|
||||
Assert.True(controller.Apply(Parsed(properties, creature)));
|
||||
Assert.Equal(AppraisalView.Creature, controller.ActiveView);
|
||||
Assert.Equal(1, shown);
|
||||
Assert.False(layout.FindElement(AppraisalUiController.ItemPanelId)!.Visible);
|
||||
Assert.True(layout.FindElement(AppraisalUiController.CreaturePanelId)!.Visible);
|
||||
|
||||
combat.SetCombatMode(CombatMode.Melee);
|
||||
controller.Tick(0.74);
|
||||
Assert.Single(sent);
|
||||
controller.Tick(0.01);
|
||||
Assert.Equal(new[] { ObjectId, ObjectId }, sent);
|
||||
Assert.Equal(0, interaction.BusyCount);
|
||||
|
||||
layout.Root.Visible = false;
|
||||
controller.Tick(0.75);
|
||||
Assert.Equal(new[] { ObjectId, ObjectId }, sent);
|
||||
|
||||
Assert.True(controller.Apply(Parsed(properties, creature)));
|
||||
Assert.Equal(1, shown);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResponseForNeitherPendingNorCurrent_IsIgnored()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadExamination();
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = ObjectId, Name = "Item" });
|
||||
using var interaction = NewInteraction(objects, []);
|
||||
using AppraisalUiController controller = AppraisalUiController.Bind(
|
||||
layout,
|
||||
objects,
|
||||
interaction,
|
||||
new CombatState(),
|
||||
new Spellbook(),
|
||||
() => throw new InvalidOperationException("must not show"),
|
||||
() => { })!;
|
||||
|
||||
Assert.False(controller.Apply(Parsed(new PropertyBundle())));
|
||||
Assert.Equal(0u, controller.CurrentObjectId);
|
||||
}
|
||||
|
||||
private static ItemInteractionController NewInteraction(
|
||||
ClientObjectTable objects,
|
||||
List<uint> sent)
|
||||
=> new(
|
||||
objects,
|
||||
playerGuid: () => 0x50000002u,
|
||||
sendUse: null,
|
||||
sendUseWithTarget: null,
|
||||
sendWield: null,
|
||||
sendDrop: null,
|
||||
sendExamine: sent.Add);
|
||||
|
||||
private static AppraiseInfoParser.Parsed Parsed(
|
||||
PropertyBundle properties,
|
||||
AppraiseInfoParser.CreatureProfile? creature = null)
|
||||
=> new(
|
||||
Guid: ObjectId,
|
||||
Flags: creature is null
|
||||
? AppraiseInfoParser.IdentifyResponseFlags.IntStatsTable
|
||||
: AppraiseInfoParser.IdentifyResponseFlags.CreatureProfile,
|
||||
Success: true,
|
||||
Properties: properties,
|
||||
SpellBook: [],
|
||||
ArmorProfile: null,
|
||||
CreatureProfile: creature,
|
||||
WeaponProfile: null,
|
||||
HookProfile: null,
|
||||
ArmorLevels: null,
|
||||
ArmorEnchantments: null,
|
||||
WeaponEnchantments: null,
|
||||
ResistEnchantments: null);
|
||||
}
|
||||
|
|
@ -107,6 +107,12 @@ public static class FixtureLoader
|
|||
public static ElementInfo LoadComponentRowTemplateInfos()
|
||||
=> LoadInfos("component_row_21000033_10000467.json");
|
||||
|
||||
public static ImportedLayout LoadExamination()
|
||||
=> LayoutImporter.Build(LoadExaminationInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadExaminationInfos()
|
||||
=> LoadInfos("examine_2100006B_100005F2.json");
|
||||
|
||||
public static ImportedLayout LoadPowerbar()
|
||||
=> LayoutImporter.Build(LoadPowerbarInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,29 @@ public sealed class RetailFixtureConformanceTests
|
|||
Assert.NotNull(Find(root, CharacterStatController.TitlesPageId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExaminationFixture_ContainsRetailChromeSubviewsAndScrollbars()
|
||||
{
|
||||
ElementInfo root = FixtureLoader.LoadExaminationInfos();
|
||||
|
||||
Assert.Equal(AppraisalUiController.RootId, root.Id);
|
||||
Assert.Equal(310f, root.Width);
|
||||
Assert.Equal(400f, root.Height);
|
||||
Assert.NotNull(Find(root, AppraisalUiController.CloseId));
|
||||
Assert.NotNull(Find(root, AppraisalUiController.TitleId));
|
||||
Assert.NotNull(Find(root, AppraisalUiController.ItemPanelId));
|
||||
Assert.NotNull(Find(root, AppraisalUiController.ItemTextId));
|
||||
Assert.NotNull(Find(root, AppraisalUiController.ItemScrollbarId));
|
||||
Assert.NotNull(Find(root, AppraisalUiController.CreaturePanelId));
|
||||
Assert.NotNull(Find(root, AppraisalUiController.SpellPanelId));
|
||||
|
||||
ImportedLayout layout = FixtureLoader.LoadExamination();
|
||||
Assert.IsType<UiButton>(layout.FindElement(AppraisalUiController.CloseId));
|
||||
Assert.IsType<UiText>(layout.FindElement(AppraisalUiController.TitleId));
|
||||
Assert.IsType<UiScrollbar>(
|
||||
layout.FindElement(AppraisalUiController.ItemScrollbarId));
|
||||
}
|
||||
|
||||
public static TheoryData<Func<ElementInfo>, uint, uint, float, float, int> LayoutCases
|
||||
=> new()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ public sealed class RetailLayoutFixtureGenerator
|
|||
"effects_row_2100001B_10000128.json"),
|
||||
(CharacterController.LayoutId, CharacterController.RootId,
|
||||
"character_info_2100006E_10000183.json"),
|
||||
(0x2100006Bu, 0x100005F2u, "examine_2100006B_100005F2.json"),
|
||||
})
|
||||
{
|
||||
ElementInfo? panelPart = LayoutImporter.ImportInfos(dats, layoutId, rootId);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue