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:
Erik 2026-07-15 10:55:22 +02:00
parent 7b7ffcd278
commit 07be994d97
84 changed files with 17822 additions and 1051 deletions

View file

@ -20,7 +20,8 @@ public sealed class CombatUiController : IRetainedPanelController
{
public const uint LayoutId = 0x21000073u;
public const uint BasicPanelId = 0x1000005Cu;
public const uint AdvancedPanelId = 0x10000061u;
public const uint SpellcastingPanelId = 0x10000061u;
public const uint AdvancedPanelId = SpellcastingPanelId;
public const uint PowerControlId = 0x1000004Fu;
public const uint SpeedLabelId = 0x10000051u;
public const uint PowerLabelId = 0x10000052u;
@ -36,7 +37,7 @@ public sealed class CombatUiController : IRetainedPanelController
private readonly UiElement _root;
private readonly UiElement _basicPanel;
private readonly UiElement _advancedPanel;
private readonly UiElement _spellcastingPanel;
private readonly UiScrollbar _powerControl;
private readonly UiButton _high;
private readonly UiButton _medium;
@ -54,7 +55,7 @@ public sealed class CombatUiController : IRetainedPanelController
private CombatUiController(
ImportedLayout layout,
UiElement basicPanel,
UiElement advancedPanel,
UiElement spellcastingPanel,
UiScrollbar powerControl,
UiButton high,
UiButton medium,
@ -71,7 +72,7 @@ public sealed class CombatUiController : IRetainedPanelController
{
_root = layout.Root;
_basicPanel = basicPanel;
_advancedPanel = advancedPanel;
_spellcastingPanel = spellcastingPanel;
_powerControl = powerControl;
_high = high;
_medium = medium;
@ -85,11 +86,10 @@ public sealed class CombatUiController : IRetainedPanelController
_setGameplay = setGameplay;
_setWindowVisible = setWindowVisible;
// PlayerModule::AdvancedCombatUI false selects gmCombatUI's authored
// basic page. The advanced page belongs to the separate advanced-
// combat flow and stays hidden in the basic panel.
// Retail layout 0x21000073 contains two sibling pages: gmCombatUI's
// physical-attack controls and gmSpellcastingUI's favorite-spell bar.
_basicPanel.Visible = true;
_advancedPanel.Visible = false;
_spellcastingPanel.Visible = false;
_powerControl.SetScalarPosition(_attacks.DesiredPower);
_powerControl.ScalarChanged = _attacks.SetDesiredPower;
@ -140,7 +140,7 @@ public sealed class CombatUiController : IRetainedPanelController
ArgumentNullException.ThrowIfNull(setWindowVisible);
if (layout.FindElement(BasicPanelId) is not { } basic
|| layout.FindElement(AdvancedPanelId) is not { } advanced
|| layout.FindElement(SpellcastingPanelId) is not { } spellcasting
|| layout.FindElement(PowerControlId) is not UiScrollbar power
|| layout.FindElement(HighButtonId) is not UiButton high
|| layout.FindElement(MediumButtonId) is not UiButton medium
@ -151,7 +151,7 @@ public sealed class CombatUiController : IRetainedPanelController
return null;
return new CombatUiController(
layout, basic, advanced, power, high, medium, low,
layout, basic, spellcasting, power, high, medium, low,
repeatAttacks, autoTarget, keepInView,
combat, attacks, gameplay, setGameplay, labels, setWindowVisible);
}
@ -166,7 +166,9 @@ public sealed class CombatUiController : IRetainedPanelController
private void OnCombatModeChanged(CombatMode mode)
{
bool visible = CombatInputPlanner.SupportsTargetedAttack(mode);
bool visible = mode is CombatMode.Melee or CombatMode.Missile or CombatMode.Magic;
_basicPanel.Visible = mode is CombatMode.Melee or CombatMode.Missile;
_spellcastingPanel.Visible = mode == CombatMode.Magic;
if (_root is IUiDatStateful stateful)
{
if (mode == CombatMode.Melee)