using AcDream.App.Combat;
using AcDream.Core.Combat;
using AcDream.UI.Abstractions.Panels.Settings;
namespace AcDream.App.UI.Layout;
///
/// Binds the imported retail gmCombatUI layout to the client combat
/// state machine. No panel geometry is synthesized here: every control is the
/// authored element from LayoutDesc 0x21000073.
///
///
/// Retail references: gmCombatUI::RecvNotice_AttackHeightChanged
/// (0x004CC080), RecvNotice_SetPowerbarLevel (0x004CC0E0),
/// RecvNotice_DesiredAttackPowerChanged (0x004CC110),
/// ListenToElementMessage (0x004CC430), and
/// RecvNotice_SetCombatMode (0x004CC620).
///
public sealed class CombatUiController : IRetainedPanelController
{
public const uint LayoutId = 0x21000073u;
public const uint BasicPanelId = 0x1000005Cu;
public const uint AdvancedPanelId = 0x10000061u;
public const uint PowerControlId = 0x1000004Fu;
public const uint SpeedLabelId = 0x10000051u;
public const uint PowerLabelId = 0x10000052u;
public const uint RepeatAttacksId = 0x10000053u;
public const uint AutoTargetId = 0x10000054u;
public const uint KeepInViewId = 0x10000055u;
public const uint HighButtonId = 0x10000057u;
public const uint MediumButtonId = 0x10000058u;
public const uint LowButtonId = 0x10000059u;
public const uint MeleeState = 0x10000003u;
public const uint MissileState = 0x10000004u;
private readonly UiElement _root;
private readonly UiElement _basicPanel;
private readonly UiElement _advancedPanel;
private readonly UiScrollbar _powerControl;
private readonly UiButton _high;
private readonly UiButton _medium;
private readonly UiButton _low;
private readonly UiButton _repeatAttacks;
private readonly UiButton _autoTarget;
private readonly UiButton _keepInView;
private readonly CombatState _combat;
private readonly CombatAttackController _attacks;
private readonly Func _gameplay;
private readonly Action _setGameplay;
private readonly Action _setWindowVisible;
private bool _disposed;
private CombatUiController(
ImportedLayout layout,
UiElement basicPanel,
UiElement advancedPanel,
UiScrollbar powerControl,
UiButton high,
UiButton medium,
UiButton low,
UiButton repeatAttacks,
UiButton autoTarget,
UiButton keepInView,
CombatState combat,
CombatAttackController attacks,
Func gameplay,
Action setGameplay,
CombatUiLabels labels,
Action setWindowVisible)
{
_root = layout.Root;
_basicPanel = basicPanel;
_advancedPanel = advancedPanel;
_powerControl = powerControl;
_high = high;
_medium = medium;
_low = low;
_repeatAttacks = repeatAttacks;
_autoTarget = autoTarget;
_keepInView = keepInView;
_combat = combat;
_attacks = attacks;
_gameplay = gameplay;
_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.
_basicPanel.Visible = true;
_advancedPanel.Visible = false;
_powerControl.SetScalarPosition(_attacks.DesiredPower);
_powerControl.ScalarChanged = _attacks.SetDesiredPower;
_powerControl.ScalarFill = () => null;
_powerControl.ScalarFillFollowsThumb = true;
BindAttackButton(_high, AttackHeight.High);
BindAttackButton(_medium, AttackHeight.Medium);
BindAttackButton(_low, AttackHeight.Low);
_high.Label = labels.High;
_medium.Label = labels.Medium;
_low.Label = labels.Low;
_repeatAttacks.Label = labels.RepeatAttacks;
_autoTarget.Label = labels.AutoTarget;
_keepInView.Label = labels.KeepInView;
UiText? speedLabel = layout.FindElement(SpeedLabelId) as UiText;
UiText? powerLabel = layout.FindElement(PowerLabelId) as UiText;
SetStaticText(speedLabel, labels.Speed, rightAligned: false);
SetStaticText(powerLabel, labels.Power, rightAligned: true);
ConfigurePowerRange(speedLabel, powerLabel);
_repeatAttacks.OnClick = () =>
_setGameplay(_gameplay() with { AutoRepeatAttack = _repeatAttacks.Selected });
_autoTarget.OnClick = () =>
_setGameplay(_gameplay() with { AutoTarget = _autoTarget.Selected });
_keepInView.OnClick = () =>
_setGameplay(_gameplay() with { ViewCombatTarget = _keepInView.Selected });
_combat.CombatModeChanged += OnCombatModeChanged;
_attacks.StateChanged += OnAttackStateChanged;
SyncControls();
}
public static CombatUiController? Bind(
ImportedLayout layout,
CombatState combat,
CombatAttackController attacks,
Func gameplay,
Action setGameplay,
CombatUiLabels labels,
Action setWindowVisible)
{
ArgumentNullException.ThrowIfNull(layout);
ArgumentNullException.ThrowIfNull(combat);
ArgumentNullException.ThrowIfNull(attacks);
ArgumentNullException.ThrowIfNull(gameplay);
ArgumentNullException.ThrowIfNull(setGameplay);
ArgumentNullException.ThrowIfNull(labels);
ArgumentNullException.ThrowIfNull(setWindowVisible);
if (layout.FindElement(BasicPanelId) is not { } basic
|| layout.FindElement(AdvancedPanelId) is not { } advanced
|| layout.FindElement(PowerControlId) is not UiScrollbar power
|| layout.FindElement(HighButtonId) is not UiButton high
|| layout.FindElement(MediumButtonId) is not UiButton medium
|| layout.FindElement(LowButtonId) is not UiButton low
|| layout.FindElement(RepeatAttacksId) is not UiButton repeatAttacks
|| layout.FindElement(AutoTargetId) is not UiButton autoTarget
|| layout.FindElement(KeepInViewId) is not UiButton keepInView)
return null;
return new CombatUiController(
layout, basic, advanced, power, high, medium, low,
repeatAttacks, autoTarget, keepInView,
combat, attacks, gameplay, setGameplay, labels, setWindowVisible);
}
public void SyncVisibility() => OnCombatModeChanged(_combat.CurrentMode);
private void BindAttackButton(UiButton button, AttackHeight height)
{
button.OnPressed = () => _attacks.PressAttack(height);
button.OnReleased = _attacks.ReleaseAttack;
}
private void OnCombatModeChanged(CombatMode mode)
{
bool visible = CombatInputPlanner.SupportsTargetedAttack(mode);
if (_root is IUiDatStateful stateful)
{
if (mode == CombatMode.Melee)
stateful.TrySetRetailState(MeleeState);
else if (mode == CombatMode.Missile)
stateful.TrySetRetailState(MissileState);
}
_setWindowVisible(visible);
SyncControls();
}
private void OnAttackStateChanged() => SyncControls();
private void SyncControls()
{
_powerControl.SetScalarPosition(_attacks.DesiredPower);
_high.Selected = _attacks.RequestedHeight == AttackHeight.High;
_medium.Selected = _attacks.RequestedHeight == AttackHeight.Medium;
_low.Selected = _attacks.RequestedHeight == AttackHeight.Low;
GameplaySettings gameplay = _gameplay();
_repeatAttacks.Selected = gameplay.AutoRepeatAttack;
_autoTarget.Selected = gameplay.AutoTarget;
_keepInView.Selected = gameplay.ViewCombatTarget;
}
private void ConfigurePowerRange(UiText? speedLabel, UiText? powerLabel)
{
if (speedLabel is null || powerLabel is null) return;
// All three elements are siblings in gmCombatUI's basic page. Keeping
// the meter between the authored text rectangles leaves the base gray
// track behind Speed/Power while the dark/live red textures occupy the
// semantic power range between them.
_powerControl.ScalarRangeProvider = () =>
{
float left = speedLabel.Left + speedLabel.Width - _powerControl.Left;
float right = powerLabel.Left - _powerControl.Left;
float clampedLeft = Math.Clamp(left, 0f, _powerControl.Width);
return (clampedLeft, Math.Max(
0f, Math.Clamp(right, 0f, _powerControl.Width) - clampedLeft));
};
}
private static void SetStaticText(UiText? text, string value, bool rightAligned)
{
if (text is null) return;
// Retail UIElement_Text starts with zero margins; these 15px-high
// gmCombatUI captions are single authored lines. The scrollback path's
// generic 4px inset would leave less than one glyph row and clip them all.
text.OneLine = true;
text.Padding = 0f;
text.Centered = false;
text.RightAligned = rightAligned;
UiText.Line[] line = [new UiText.Line(value, text.DefaultColor)];
text.LinesProvider = () => line;
}
public void OnShown() => SyncControls();
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_combat.CombatModeChanged -= OnCombatModeChanged;
_attacks.StateChanged -= OnAttackStateChanged;
_powerControl.ScalarChanged = null;
_powerControl.ScalarFill = () => null;
_powerControl.ScalarFillFollowsThumb = false;
_powerControl.ScalarRangeProvider = null;
_high.OnPressed = null;
_high.OnReleased = null;
_medium.OnPressed = null;
_medium.OnReleased = null;
_low.OnPressed = null;
_low.OnReleased = null;
_repeatAttacks.OnClick = null;
_autoTarget.OnClick = null;
_keepInView.OnClick = null;
}
}
/// Localized labels assigned by retail gmCombatUI::PostInit.
public sealed record CombatUiLabels(
string Speed,
string Power,
string RepeatAttacks,
string AutoTarget,
string KeepInView,
string High,
string Medium,
string Low)
{
private const uint UiStringTable = 0x23000001u;
public static CombatUiLabels Resolve(ElementInfo root, DatStringResolver strings)
{
ArgumentNullException.ThrowIfNull(root);
ArgumentNullException.ThrowIfNull(strings);
return new CombatUiLabels(
ElementString(CombatUiController.SpeedLabelId, UiStateInfo.DirectStateId, "Speed"),
ElementString(CombatUiController.PowerLabelId, CombatUiController.MeleeState, "Power"),
RuntimeString("ID_CombatPanelOption_AutoRepeatAttack", "Repeat Attacks"),
RuntimeString("ID_CombatPanelOption_AutoTarget", "Auto Target"),
RuntimeString("ID_CombatPanelOption_ViewCombatTarget", "Keep in View"),
ElementString(CombatUiController.HighButtonId, UiStateInfo.DirectStateId, "High"),
ElementString(CombatUiController.MediumButtonId, UiStateInfo.DirectStateId, "Medium"),
ElementString(CombatUiController.LowButtonId, UiStateInfo.DirectStateId, "Low"));
string RuntimeString(string id, string fallback)
=> strings.Resolve(UiStringTable, DatStringResolver.ComputeHash(id)) ?? fallback;
string ElementString(uint elementId, uint stateId, string fallback)
{
ElementInfo? element = Find(root, elementId);
if (element is null
|| !element.TryGetEffectiveProperty(0x17u, out var property, stateId)
|| property.Kind != UiPropertyKind.StringInfo)
return fallback;
return strings.Resolve(property.StringInfoValue) ?? fallback;
}
}
private static ElementInfo? Find(ElementInfo element, uint id)
{
if (element.Id == id) return element;
foreach (ElementInfo child in element.Children)
if (Find(child, id) is { } found)
return found;
return null;
}
}