feat: port retail magic lifecycle and retained spell UI
Complete the retail cast-intent, target, component, enchantment, and busy-state paths; mount the DAT-authored spell bar, spellbook, component book, effects panels, and shared panel lifecycle; and add scoped input plus conformance coverage. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
7b7ffcd278
commit
07be994d97
84 changed files with 17822 additions and 1051 deletions
|
|
@ -0,0 +1,173 @@
|
|||
using AcDream.App.Spells;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
public sealed class SpellcastingUiControllerTests
|
||||
{
|
||||
private static (uint, int, int) NoTex(uint _) => (0u, 0, 0);
|
||||
|
||||
[Fact]
|
||||
public void ImportedFixture_BindsAllTabs_AndTracksEquippedEndowment()
|
||||
{
|
||||
ImportedLayout layout = LayoutImporter.Build(
|
||||
FixtureLoader.LoadCombatInfos(), NoTex, datFont: null);
|
||||
var spellbook = new Spellbook();
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" });
|
||||
var used = new List<uint>();
|
||||
using SpellcastingUiController? controller = Bind(
|
||||
layout, spellbook, objects, used.Add);
|
||||
|
||||
Assert.True(controller is not null, DescribeBinding(layout));
|
||||
objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = 2u,
|
||||
Name = "Orb",
|
||||
Type = ItemType.Caster,
|
||||
WielderId = 1u,
|
||||
CurrentlyEquippedLocation = EquipMask.Held,
|
||||
SpellId = 2670u,
|
||||
IconId = 0x06001234u,
|
||||
});
|
||||
controller.Tick();
|
||||
|
||||
UiElement host = Assert.IsAssignableFrom<UiElement>(
|
||||
layout.FindElement(SpellcastingUiController.EndowmentId));
|
||||
Assert.True(host.Visible);
|
||||
UiCatalogSlot slot = Assert.IsType<UiCatalogSlot>(host.Children[^1]);
|
||||
Assert.Equal(2u, slot.EntryId);
|
||||
Assert.Equal(2670u, slot.CatalogIconTexture);
|
||||
Assert.Equal(2u, slot.CatalogOverlayTexture);
|
||||
|
||||
slot.OnEvent(new UiEvent(0, slot, UiEventType.Click));
|
||||
var cast = Assert.IsType<UiButton>(
|
||||
layout.FindElement(SpellcastingUiController.CastButtonId));
|
||||
cast.OnEvent(new UiEvent(0, cast, UiEventType.Click));
|
||||
Assert.Equal([2u], used);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FavoriteDrop_IgnoresForeignInventoryPayload()
|
||||
{
|
||||
ImportedLayout layout = LayoutImporter.Build(
|
||||
FixtureLoader.LoadCombatInfos(), NoTex, datFont: null);
|
||||
var spellbook = new Spellbook();
|
||||
spellbook.OnSpellLearned(42u, 1f);
|
||||
spellbook.SetFavorite(0, 0, 42u);
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" });
|
||||
using SpellcastingUiController? controller = Bind(
|
||||
layout, spellbook, objects, _ => { });
|
||||
|
||||
Assert.True(controller is not null, DescribeBinding(layout));
|
||||
controller.Tick();
|
||||
UiElement group = Assert.IsAssignableFrom<UiElement>(layout.FindElement(0x100000AAu));
|
||||
UiItemList list = Descendants(group).OfType<UiItemList>().First();
|
||||
UiCatalogSlot slot = Assert.IsType<UiCatalogSlot>(list.GetItem(0));
|
||||
var foreign = new ItemDragPayload(9u, ItemDragSource.Inventory, 0, new UiItemSlot(), null);
|
||||
|
||||
bool handled = slot.OnEvent(new UiEvent(
|
||||
0, slot, UiEventType.DropReleased, Payload: foreign));
|
||||
|
||||
Assert.True(handled);
|
||||
Assert.Equal([42u], spellbook.GetFavorites(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnrelatedObjectUpdate_DoesNotRecreateFavoriteSlots()
|
||||
{
|
||||
ImportedLayout layout = LayoutImporter.Build(
|
||||
FixtureLoader.LoadCombatInfos(), NoTex, datFont: null);
|
||||
var spellbook = new Spellbook();
|
||||
spellbook.OnSpellLearned(42u, 1f);
|
||||
spellbook.SetFavorite(0, 0, 42u);
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" });
|
||||
using SpellcastingUiController controller = Bind(
|
||||
layout, spellbook, objects, _ => { })!;
|
||||
UiElement group = layout.FindElement(0x100000AAu)!;
|
||||
UiItemList list = Descendants(group).OfType<UiItemList>().First();
|
||||
UiItemSlot before = list.GetItem(0)!;
|
||||
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = 99u, Name = "Rock" });
|
||||
controller.Tick();
|
||||
|
||||
Assert.Same(before, list.GetItem(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ObjectTableClear_RemovesEquippedEndowmentAndCannotUseStaleGuid()
|
||||
{
|
||||
ImportedLayout layout = LayoutImporter.Build(
|
||||
FixtureLoader.LoadCombatInfos(), NoTex, datFont: null);
|
||||
var spellbook = new Spellbook();
|
||||
var objects = new ClientObjectTable();
|
||||
objects.AddOrUpdate(new ClientObject { ObjectId = 1u, Name = "Player" });
|
||||
objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = 2u,
|
||||
Name = "Orb",
|
||||
Type = ItemType.Caster,
|
||||
WielderId = 1u,
|
||||
CurrentlyEquippedLocation = EquipMask.Held,
|
||||
SpellId = 2670u,
|
||||
});
|
||||
var used = new List<uint>();
|
||||
using SpellcastingUiController controller = Bind(
|
||||
layout, spellbook, objects, used.Add)!;
|
||||
UiElement host = layout.FindElement(SpellcastingUiController.EndowmentId)!;
|
||||
UiCatalogSlot slot = Assert.IsType<UiCatalogSlot>(host.Children[^1]);
|
||||
slot.OnEvent(new UiEvent(0, slot, UiEventType.Click));
|
||||
|
||||
objects.Clear();
|
||||
controller.Tick();
|
||||
|
||||
Assert.False(host.Visible);
|
||||
Assert.Equal(0u, slot.EntryId);
|
||||
slot.OnEvent(new UiEvent(0, slot, UiEventType.DoubleClick));
|
||||
Assert.Empty(used);
|
||||
}
|
||||
|
||||
private static SpellcastingUiController? Bind(
|
||||
ImportedLayout layout,
|
||||
Spellbook spellbook,
|
||||
ClientObjectTable objects,
|
||||
Action<uint> useItem)
|
||||
{
|
||||
var casting = new SpellCastingController(
|
||||
spellbook, () => null, () => 1u, () => { }, _ => { }, (_, _) => { }, _ => { });
|
||||
return SpellcastingUiController.Bind(
|
||||
layout, spellbook, casting, objects, () => 1u,
|
||||
spellId => spellId,
|
||||
item => item.ObjectId,
|
||||
useItem,
|
||||
new SelectionState(),
|
||||
(_, _, _) => { },
|
||||
(_, _) => { });
|
||||
}
|
||||
|
||||
private static IEnumerable<UiElement> Descendants(UiElement root)
|
||||
{
|
||||
foreach (UiElement child in root.Children)
|
||||
{
|
||||
yield return child;
|
||||
foreach (UiElement nested in Descendants(child)) yield return nested;
|
||||
}
|
||||
}
|
||||
|
||||
private static string DescribeBinding(ImportedLayout layout)
|
||||
{
|
||||
uint[] ids =
|
||||
[
|
||||
SpellcastingUiController.CastButtonId, SpellcastingUiController.EndowmentId,
|
||||
0x100000A3u, 0x100000AAu, 0x100005C2u, 0x100005C3u,
|
||||
];
|
||||
return string.Join(", ", ids.Select(id =>
|
||||
$"{id:X8}={layout.FindElement(id)?.GetType().Name ?? "missing"}"));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue