fix(ui): port exact paperdoll body selection
Resolve and sample retail's authored nine-color paperdoll click map, preserve local click coordinates, and select the stable highest-priority worn item with the player fallback. Keep targeted-use body clicks routed to self and pin both synthetic and live-DAT conformance. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
2644d1d527
commit
0b74d19475
16 changed files with 490 additions and 32 deletions
83
tests/AcDream.App.Tests/UI/Layout/PaperdollClickMapTests.cs
Normal file
83
tests/AcDream.App.Tests/UI/Layout/PaperdollClickMapTests.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Items;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.Options;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
public sealed class PaperdollClickMapTests
|
||||
{
|
||||
public static TheoryData<byte, byte, byte, EquipMask> RetailColors => new()
|
||||
{
|
||||
{ 0x00, 0x00, 0xFF, EquipMask.HeadWear },
|
||||
{ 0x00, 0xFF, 0x00, EquipMask.ChestWear | EquipMask.ChestArmor },
|
||||
{ 0xFF, 0x00, 0x00, EquipMask.AbdomenWear | EquipMask.AbdomenArmor },
|
||||
{ 0x00, 0xFF, 0xFF, EquipMask.UpperArmWear | EquipMask.UpperArmArmor },
|
||||
{ 0xFF, 0x00, 0xFF, EquipMask.LowerArmWear | EquipMask.LowerArmArmor },
|
||||
{ 0xFF, 0xFF, 0x00, EquipMask.UpperLegWear | EquipMask.UpperLegArmor },
|
||||
{ 0x00, 0x00, 0x80, EquipMask.LowerLegWear | EquipMask.LowerLegArmor },
|
||||
{ 0x00, 0x80, 0x00, EquipMask.HandWear },
|
||||
{ 0x80, 0x00, 0x00, EquipMask.FootWear },
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RetailColors))]
|
||||
public void GetBodyLocation_mapsRetailHitColors(
|
||||
byte r,
|
||||
byte g,
|
||||
byte b,
|
||||
EquipMask expected)
|
||||
{
|
||||
var map = new PaperdollClickMap([r, g, b, 0xFF], 1, 1);
|
||||
|
||||
Assert.Equal(expected, map.GetBodyLocation(0, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBodyLocation_rejectsBackgroundAndOutOfBounds()
|
||||
{
|
||||
var map = new PaperdollClickMap([0xFF, 0xFF, 0xFF, 0xFF], 1, 1);
|
||||
|
||||
Assert.Equal(EquipMask.None, map.GetBodyLocation(0, 0));
|
||||
Assert.Equal(EquipMask.None, map.GetBodyLocation(-1, 0));
|
||||
Assert.Equal(EquipMask.None, map.GetBodyLocation(1, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LiveDat_resolvesRetailClickMapAndAllNineBodyRegions()
|
||||
{
|
||||
string datDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||
?? Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
"Documents",
|
||||
"Asheron's Call");
|
||||
if (!Directory.Exists(datDir)) return;
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
PaperdollClickMap map = Assert.IsType<PaperdollClickMap>(PaperdollClickMap.Load(dats));
|
||||
ImportedLayout layout = Assert.IsType<ImportedLayout>(LayoutImporter.Import(
|
||||
dats,
|
||||
0x21000023u,
|
||||
static _ => (0u, 0, 0),
|
||||
null));
|
||||
UiElement dragMask = Assert.IsAssignableFrom<UiElement>(
|
||||
layout.FindElement(PaperdollController.DollDragMaskId));
|
||||
Assert.Equal((int)dragMask.Width, map.Width);
|
||||
Assert.Equal((int)dragMask.Height, map.Height);
|
||||
|
||||
var regions = new HashSet<EquipMask>();
|
||||
for (int y = 0; y < map.Height; y++)
|
||||
for (int x = 0; x < map.Width; x++)
|
||||
{
|
||||
EquipMask region = map.GetBodyLocation(x, y);
|
||||
if (region != EquipMask.None)
|
||||
regions.Add(region);
|
||||
}
|
||||
|
||||
Assert.Equal(9, regions.Count);
|
||||
Assert.Contains(EquipMask.HeadWear, regions);
|
||||
Assert.Contains(EquipMask.ChestWear | EquipMask.ChestArmor, regions);
|
||||
Assert.Contains(EquipMask.FootWear, regions);
|
||||
}
|
||||
}
|
||||
|
|
@ -186,7 +186,9 @@ public sealed class RetailUiInteractionFlowTests
|
|||
return interaction;
|
||||
}
|
||||
|
||||
public void BindPaperdoll(ItemInteractionController? itemInteraction = null)
|
||||
public void BindPaperdoll(
|
||||
ItemInteractionController? itemInteraction = null,
|
||||
PaperdollClickMap? clickMap = null)
|
||||
=> PaperdollController.Bind(
|
||||
Layout,
|
||||
Objects,
|
||||
|
|
@ -195,7 +197,23 @@ public sealed class RetailUiInteractionFlowTests
|
|||
selection: Selection,
|
||||
sendWield: (item, mask) => Wields.Add((item, mask)),
|
||||
emptySlotSprite: 0x06004D20u,
|
||||
itemInteraction: itemInteraction);
|
||||
itemInteraction: itemInteraction,
|
||||
clickMap: clickMap ?? SolidClickMap(0x00, 0x00, 0xFF));
|
||||
|
||||
public static PaperdollClickMap SolidClickMap(byte r, byte g, byte b)
|
||||
{
|
||||
const int width = 70;
|
||||
const int height = 90;
|
||||
var pixels = new byte[width * height * 4];
|
||||
for (int i = 0; i < pixels.Length; i += 4)
|
||||
{
|
||||
pixels[i] = r;
|
||||
pixels[i + 1] = g;
|
||||
pixels[i + 2] = b;
|
||||
pixels[i + 3] = 0xFF;
|
||||
}
|
||||
return new PaperdollClickMap(pixels, width, height);
|
||||
}
|
||||
|
||||
public RetailUiAutomationProbe Probe()
|
||||
=> new(Root, Objects);
|
||||
|
|
@ -300,6 +318,33 @@ public sealed class RetailUiInteractionFlowTests
|
|||
Assert.False(interaction.IsTargetModeActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PaperdollBodyClick_uncoveredPart_selectsPlayerThroughUiRoot()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.BindPaperdoll();
|
||||
var probe = h.Probe();
|
||||
|
||||
Assert.True(probe.ClickElement(PaperdollController.DollDragMaskId));
|
||||
|
||||
Assert.Equal(Player, h.Selection.SelectedObjectId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PaperdollBodyClick_coveredPart_selectsUpperEquippedItemThroughUiRoot()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SeedHauberk();
|
||||
h.Objects.MoveItem(Hauberk, Player, -1, HauberkMask);
|
||||
h.BindPaperdoll(
|
||||
clickMap: Harness.SolidClickMap(0x00, 0xFF, 0x00));
|
||||
var probe = h.Probe();
|
||||
|
||||
Assert.True(probe.ClickElement(PaperdollController.DollDragMaskId));
|
||||
|
||||
Assert.Equal(Hauberk, h.Selection.SelectedObjectId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InventoryTargetUse_invalidInventoryItem_isConsumedWithoutSelectionFallback()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,6 +16,20 @@ public class UiButtonTests
|
|||
Assert.True(_clicked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Click_ProvidesLocalCoordinatesToPositionAwareHandler()
|
||||
{
|
||||
(int X, int Y) clicked = default;
|
||||
var b = new UiButton(new ElementInfo { Type = 1, Width = 46, Height = 18 }, NoTex)
|
||||
{
|
||||
OnClickAt = (x, y) => clicked = (x, y),
|
||||
};
|
||||
|
||||
b.OnEvent(new UiEvent(0, b, UiEventType.Click, Data1: 17, Data2: 9));
|
||||
|
||||
Assert.Equal((17, 9), clicked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NotClickThrough_SoItReceivesClicks()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue