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
|
|
@ -3,12 +3,14 @@ using AcDream.App.Plugins;
|
|||
using AcDream.App.Combat;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Spells;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.App.UI.Testing;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.UI.Abstractions;
|
||||
using AcDream.UI.Abstractions.Panels.Chat;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
|
@ -43,6 +45,26 @@ public sealed record CombatRuntimeBindings(
|
|||
Func<GameplaySettings> Gameplay,
|
||||
Action<GameplaySettings> SetGameplay);
|
||||
|
||||
public sealed record MagicRuntimeBindings(
|
||||
Spellbook Spellbook,
|
||||
SpellCastingController Casting,
|
||||
ClientObjectTable Objects,
|
||||
Func<uint> PlayerGuid,
|
||||
IReadOnlyDictionary<uint, SpellComponentDescriptor> Components,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
||||
Func<uint, uint> ResolveSpellIcon,
|
||||
Func<uint, uint> ResolveComponentIcon,
|
||||
SelectionState Selection,
|
||||
Func<uint, int> SpellLevel,
|
||||
Action<uint> SelectObject,
|
||||
Action<uint> UseItem,
|
||||
Action<int, int, uint> AddFavorite,
|
||||
Action<int, uint> RemoveFavorite,
|
||||
Action<uint> SendSpellbookFilter,
|
||||
Action<uint, uint> SetDesiredComponent,
|
||||
Func<double> ServerTime);
|
||||
|
||||
public sealed record JumpPowerbarRuntimeBindings(Func<JumpChargeSnapshot> Snapshot);
|
||||
|
||||
public sealed record FpsRuntimeBindings(
|
||||
|
|
@ -116,6 +138,7 @@ public sealed record RetailUiRuntimeBindings(
|
|||
ChatRuntimeBindings Chat,
|
||||
RadarRuntimeBindings Radar,
|
||||
CombatRuntimeBindings Combat,
|
||||
MagicRuntimeBindings Magic,
|
||||
JumpPowerbarRuntimeBindings JumpPowerbar,
|
||||
FpsRuntimeBindings Fps,
|
||||
ToolbarRuntimeBindings Toolbar,
|
||||
|
|
@ -139,6 +162,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
private StackSplitQuantityState StackSplitQuantity => _bindings.StackSplitQuantity;
|
||||
private readonly RetailWindowLayoutPersistence? _persistence;
|
||||
private readonly RetailUiAutomationScriptRunner? _automation;
|
||||
private readonly RetailPanelUiController _panelUi;
|
||||
private GameplayConfirmationController? _gameplayConfirmationController;
|
||||
private RetailItemConfirmationController? _itemConfirmationController;
|
||||
private RetailSkillTrainingConfirmationController? _skillTrainingConfirmationController;
|
||||
|
|
@ -147,12 +171,19 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
|
||||
{
|
||||
_bindings = bindings;
|
||||
_panelUi = new RetailPanelUiController(
|
||||
bindings.Host.IsWindowVisible,
|
||||
bindings.Host.ShowWindow,
|
||||
bindings.Host.HideWindow);
|
||||
MountFpsDisplay();
|
||||
MountVitals();
|
||||
MountRadar();
|
||||
MountChat();
|
||||
MountToolbar();
|
||||
MountCombat();
|
||||
MountSpellbook();
|
||||
MountEffects();
|
||||
MountIndicators();
|
||||
MountJumpPowerbar();
|
||||
MountDialogFactory();
|
||||
MountCharacter();
|
||||
|
|
@ -192,6 +223,11 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
public ToolbarController? ToolbarController { get; private set; }
|
||||
public ToolbarInputController? ToolbarInputController { get; private set; }
|
||||
public CombatUiController? CombatUiController { get; private set; }
|
||||
public SpellcastingUiController? SpellcastingUiController { get; private set; }
|
||||
public SpellbookWindowController? SpellbookWindowController { get; private set; }
|
||||
public EffectsUiController? PositiveEffectsController { get; private set; }
|
||||
public EffectsUiController? NegativeEffectsController { get; private set; }
|
||||
public EffectsIndicatorController? EffectsIndicatorController { get; private set; }
|
||||
public JumpPowerbarController? JumpPowerbarController { get; private set; }
|
||||
public RetailFpsController? FpsController { get; private set; }
|
||||
public SelectedObjectController? SelectedObjectController { get; private set; }
|
||||
|
|
@ -216,6 +252,10 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
public void Tick(double deltaSeconds)
|
||||
{
|
||||
FpsController?.Tick();
|
||||
SpellbookWindowController?.Tick();
|
||||
SpellcastingUiController?.Tick();
|
||||
PositiveEffectsController?.Tick();
|
||||
NegativeEffectsController?.Tick();
|
||||
JumpPowerbarController?.Tick();
|
||||
SelectedObjectController?.Tick(deltaSeconds);
|
||||
DialogFactory?.Tick();
|
||||
|
|
@ -226,7 +266,33 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
public void Draw(System.Numerics.Vector2 screenSize) => Host.Draw(screenSize);
|
||||
|
||||
public bool HandleInputAction(AcDream.UI.Abstractions.Input.InputAction action)
|
||||
=> ToolbarInputController?.Handle(action) == true;
|
||||
{
|
||||
if (SpellcastingUiController?.Handle(action) == true)
|
||||
return true;
|
||||
if (action == AcDream.UI.Abstractions.Input.InputAction.ToggleSpellbookPanel)
|
||||
{
|
||||
OpenSpellbook(SpellbookWindowPage.Spells);
|
||||
return true;
|
||||
}
|
||||
if (action == AcDream.UI.Abstractions.Input.InputAction.ToggleSpellComponentsPanel)
|
||||
{
|
||||
OpenSpellbook(SpellbookWindowPage.Components);
|
||||
return true;
|
||||
}
|
||||
return ToolbarInputController?.Handle(action) == true;
|
||||
}
|
||||
|
||||
private void OpenSpellbook(SpellbookWindowPage page)
|
||||
{
|
||||
bool visible = Host.IsWindowVisible(WindowNames.Spellbook);
|
||||
if (visible && SpellbookWindowController?.CurrentPage == page)
|
||||
CloseWindow(WindowNames.Spellbook);
|
||||
else
|
||||
{
|
||||
SpellbookWindowController?.ShowPage(page);
|
||||
_panelUi.SetPanelVisibility(RetailPanelCatalog.Magic, visible: true);
|
||||
}
|
||||
}
|
||||
|
||||
public bool HandleConfirmationRequest(GameEvents.CharacterConfirmationRequest request)
|
||||
=> _gameplayConfirmationController?.HandleRequest(request) == true;
|
||||
|
|
@ -258,15 +324,22 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
public void RestoreNamedLayout(string profileName) => _persistence?.RestoreNamed(profileName);
|
||||
|
||||
public bool ToggleWindow(string name)
|
||||
=> Host.ToggleWindow(name);
|
||||
=> RetailPanelCatalog.TryGetPanelId(name, out uint panelId)
|
||||
? _panelUi.TogglePanel(panelId)
|
||||
: Host.ToggleWindow(name);
|
||||
|
||||
public void CloseWindow(string name)
|
||||
=> Host.HideWindow(name);
|
||||
{
|
||||
if (RetailPanelCatalog.TryGetPanelId(name, out uint panelId))
|
||||
_panelUi.SetPanelVisibility(panelId, visible: false);
|
||||
else
|
||||
Host.HideWindow(name);
|
||||
}
|
||||
|
||||
public void SyncToolbarWindowButtons()
|
||||
{
|
||||
if (ToolbarController is null) return;
|
||||
foreach (var (panelId, windowName) in RetailPanelCatalog.MountedPanels)
|
||||
foreach (var (panelId, windowName) in RetailPanelCatalog.ToolbarPanels)
|
||||
ToolbarController.SetPanelOpen(panelId, Host.IsWindowVisible(windowName));
|
||||
}
|
||||
|
||||
|
|
@ -284,6 +357,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
|
||||
private void OnWindowVisibilityChanged(string windowName, bool visible)
|
||||
{
|
||||
_panelUi.ObserveWindowVisibility(windowName, visible);
|
||||
if (RetailPanelCatalog.TryGetPanelId(windowName, out uint panelId))
|
||||
ToolbarController?.SetPanelOpen(panelId, visible);
|
||||
}
|
||||
|
|
@ -583,7 +657,28 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
SpellcastingUiController? spellcasting = Layout.SpellcastingUiController.Bind(
|
||||
layout,
|
||||
_bindings.Magic.Spellbook,
|
||||
_bindings.Magic.Casting,
|
||||
_bindings.Magic.Objects,
|
||||
_bindings.Magic.PlayerGuid,
|
||||
_bindings.Magic.ResolveSpellIcon,
|
||||
item => _bindings.Magic.ResolveDragIcon(
|
||||
item.Type, item.IconId, item.IconUnderlayId,
|
||||
item.IconOverlayId, item.Effects),
|
||||
_bindings.Magic.UseItem,
|
||||
_bindings.Magic.Selection,
|
||||
_bindings.Magic.AddFavorite,
|
||||
_bindings.Magic.RemoveFavorite);
|
||||
if (spellcasting is null)
|
||||
Console.WriteLine("[M3] spellcasting: required controls missing in LayoutDesc 0x21000073.");
|
||||
|
||||
CombatUiController = controller;
|
||||
SpellcastingUiController = spellcasting;
|
||||
IRetainedPanelController controllerOwner = spellcasting is null
|
||||
? controller
|
||||
: new RetainedPanelControllerGroup(controller, spellcasting);
|
||||
UiElement root = layout.Root;
|
||||
RetailWindowFrame.Mount(
|
||||
Host.Root,
|
||||
|
|
@ -601,10 +696,190 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
ResizeY = false,
|
||||
ConstrainDragToParent = true,
|
||||
ContentClickThrough = false,
|
||||
Controller = controller,
|
||||
Controller = controllerOwner,
|
||||
});
|
||||
controller.SyncVisibility();
|
||||
Console.WriteLine("[M2] retail combat bar from gmCombatUI LayoutDesc 0x21000073.");
|
||||
Console.WriteLine(spellcasting is null
|
||||
? "[M2] retail combat from LayoutDesc 0x21000073; magic binding unavailable."
|
||||
: "[M3] retail combat + spell bar from LayoutDesc 0x21000073.");
|
||||
}
|
||||
|
||||
private void MountSpellbook()
|
||||
{
|
||||
ImportedLayout? layout;
|
||||
lock (_bindings.Assets.DatLock)
|
||||
{
|
||||
layout = LayoutImporter.Import(
|
||||
_bindings.Assets.Dats,
|
||||
SpellbookWindowController.LayoutId,
|
||||
SpellbookWindowController.RootId,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
_bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.ResolveFont);
|
||||
}
|
||||
if (layout is null)
|
||||
{
|
||||
Console.WriteLine("[M3] spellbook: LayoutDesc 0x21000034 not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
SpellbookWindowController? controller = Layout.SpellbookWindowController.Bind(
|
||||
layout,
|
||||
_bindings.Magic.Spellbook,
|
||||
_bindings.Magic.Objects,
|
||||
_bindings.Magic.PlayerGuid,
|
||||
_bindings.Magic.Components,
|
||||
_bindings.Magic.ResolveSpellIcon,
|
||||
_bindings.Magic.ResolveComponentIcon,
|
||||
_bindings.Magic.SpellLevel,
|
||||
_bindings.Magic.SelectObject,
|
||||
spellId => SpellcastingUiController?.AddFavorite(spellId),
|
||||
_bindings.Magic.SendSpellbookFilter,
|
||||
_bindings.Magic.SetDesiredComponent,
|
||||
() => CloseWindow(WindowNames.Spellbook));
|
||||
if (controller is null)
|
||||
{
|
||||
Console.WriteLine("[M3] spellbook: required controls missing in LayoutDesc 0x21000034.");
|
||||
return;
|
||||
}
|
||||
|
||||
SpellbookWindowController = controller;
|
||||
UiElement root = layout.Root;
|
||||
RetailWindowFrame.Mount(
|
||||
Host.Root,
|
||||
root,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
new RetailWindowFrame.Options
|
||||
{
|
||||
WindowName = WindowNames.Spellbook,
|
||||
Chrome = RetailWindowChrome.Imported,
|
||||
Left = 18f,
|
||||
Top = 18f,
|
||||
Visible = false,
|
||||
Resizable = false,
|
||||
ResizeX = false,
|
||||
ResizeY = false,
|
||||
ConstrainDragToParent = true,
|
||||
ContentClickThrough = false,
|
||||
Controller = controller,
|
||||
});
|
||||
_panelUi.Register(RetailPanelCatalog.Magic, WindowNames.Spellbook);
|
||||
Console.WriteLine("[M3] retail spellbook/component book from LayoutDesc 0x21000034.");
|
||||
}
|
||||
|
||||
private void MountEffects()
|
||||
{
|
||||
MountEffectsInstance(positive: true);
|
||||
MountEffectsInstance(positive: false);
|
||||
}
|
||||
|
||||
private void MountEffectsInstance(bool positive)
|
||||
{
|
||||
uint rootId = positive ? EffectsUiController.PositiveRootId : EffectsUiController.NegativeRootId;
|
||||
ElementInfo? rootInfo;
|
||||
ImportedLayout? layout;
|
||||
lock (_bindings.Assets.DatLock)
|
||||
{
|
||||
rootInfo = LayoutImporter.ImportInfos(
|
||||
_bindings.Assets.Dats,
|
||||
EffectsUiController.LayoutId,
|
||||
rootId);
|
||||
layout = rootInfo is null
|
||||
? null
|
||||
: LayoutImporter.Build(
|
||||
rootInfo,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
_bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.ResolveFont,
|
||||
new DatStringResolver(_bindings.Assets.Dats).Resolve);
|
||||
}
|
||||
if (rootInfo is null || layout is null)
|
||||
{
|
||||
Console.WriteLine($"[M3] effects: root 0x{rootId:X8} not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
EffectsUiController? controller = Layout.EffectsUiController.Bind(
|
||||
layout,
|
||||
_bindings.Magic.Spellbook,
|
||||
positive,
|
||||
_bindings.Magic.ServerTime,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
_bindings.Magic.ResolveSpellIcon,
|
||||
close: () => CloseWindow(
|
||||
positive ? WindowNames.PositiveEffects : WindowNames.NegativeEffects));
|
||||
if (controller is null)
|
||||
{
|
||||
Console.WriteLine($"[M3] effects: list missing under root 0x{rootId:X8}.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (positive) PositiveEffectsController = controller;
|
||||
else NegativeEffectsController = controller;
|
||||
UiElement root = layout.Root;
|
||||
RetailWindowFrame.Mount(
|
||||
Host.Root,
|
||||
root,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
new RetailWindowFrame.Options
|
||||
{
|
||||
WindowName = positive ? WindowNames.PositiveEffects : WindowNames.NegativeEffects,
|
||||
Chrome = RetailWindowChrome.Imported,
|
||||
Left = Math.Max(0f, Host.Root.Width - root.Width - 12f),
|
||||
Top = 18f,
|
||||
Visible = false,
|
||||
Resizable = false,
|
||||
ResizeX = false,
|
||||
ResizeY = false,
|
||||
ConstrainDragToParent = true,
|
||||
ContentClickThrough = false,
|
||||
Controller = controller,
|
||||
});
|
||||
_panelUi.Register(
|
||||
positive ? RetailPanelCatalog.PositiveEffects : RetailPanelCatalog.NegativeEffects,
|
||||
positive ? WindowNames.PositiveEffects : WindowNames.NegativeEffects,
|
||||
rootInfo);
|
||||
}
|
||||
|
||||
private void MountIndicators()
|
||||
{
|
||||
ImportedLayout? layout = Import(EffectsIndicatorController.LayoutId);
|
||||
if (layout is null)
|
||||
{
|
||||
Console.WriteLine("[M3] effects indicators: LayoutDesc 0x21000071 not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
EffectsIndicatorController? controller = Layout.EffectsIndicatorController.Bind(
|
||||
layout,
|
||||
_bindings.Magic.Spellbook,
|
||||
panelId => _panelUi.TogglePanel(panelId));
|
||||
if (controller is null)
|
||||
{
|
||||
Console.WriteLine("[M3] effects indicators: authored Helpful/Harmful buttons missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
EffectsIndicatorController = controller;
|
||||
RetailWindowFrame.Mount(
|
||||
Host.Root,
|
||||
layout.Root,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
new RetailWindowFrame.Options
|
||||
{
|
||||
WindowName = WindowNames.Indicators,
|
||||
Chrome = RetailWindowChrome.Imported,
|
||||
Left = 10f,
|
||||
Top = 96f,
|
||||
Visible = true,
|
||||
Resizable = false,
|
||||
ResizeX = false,
|
||||
ResizeY = false,
|
||||
ConstrainDragToParent = true,
|
||||
ContentClickThrough = false,
|
||||
Controller = controller,
|
||||
});
|
||||
Console.WriteLine("[M3] retail Helpful/Harmful effect indicators from LayoutDesc 0x21000071.");
|
||||
}
|
||||
|
||||
private void MountJumpPowerbar()
|
||||
|
|
@ -825,6 +1100,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
||||
ContentClickThrough = false,
|
||||
});
|
||||
_panelUi.Register(RetailPanelCatalog.Character, WindowNames.Character);
|
||||
Console.WriteLine("[D.2b-C] retail character window from LayoutDesc importer (0x2100002E).");
|
||||
}
|
||||
|
||||
|
|
@ -906,6 +1182,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
Host.Root.Height - root.Top),
|
||||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Bottom,
|
||||
});
|
||||
_panelUi.Register(RetailPanelCatalog.Inventory, WindowNames.Inventory);
|
||||
|
||||
uint contents, sideBag, mainPack;
|
||||
IReadOnlyDictionary<uint, uint> paperdollEmptySprites;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue