fix(combat): restore retail combat bar controls
Resolve authored StringInfo labels from local.dat, port gmCombatUI's runtime option captions and checkbox widgets, and bind horizontal scrollbar media by retail structural roles so the green power jewel is a thumb instead of a tiled track. Persist the three combat options and make Auto Target govern target acquisition. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
2215c76c7e
commit
927fa7881a
19 changed files with 24591 additions and 48 deletions
|
|
@ -1,5 +1,6 @@
|
|||
using AcDream.App.Combat;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
|
|
@ -21,12 +22,17 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
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;
|
||||
|
||||
private const uint MeleeState = 0x10000003u;
|
||||
private const uint MissileState = 0x10000004u;
|
||||
public const uint MeleeState = 0x10000003u;
|
||||
public const uint MissileState = 0x10000004u;
|
||||
|
||||
private readonly UiElement _root;
|
||||
private readonly UiElement _basicPanel;
|
||||
|
|
@ -35,8 +41,13 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
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<GameplaySettings> _gameplay;
|
||||
private readonly Action<GameplaySettings> _setGameplay;
|
||||
private readonly Action<bool> _setWindowVisible;
|
||||
private bool _disposed;
|
||||
|
||||
|
|
@ -48,8 +59,14 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
UiButton high,
|
||||
UiButton medium,
|
||||
UiButton low,
|
||||
UiButton repeatAttacks,
|
||||
UiButton autoTarget,
|
||||
UiButton keepInView,
|
||||
CombatState combat,
|
||||
CombatAttackController attacks,
|
||||
Func<GameplaySettings> gameplay,
|
||||
Action<GameplaySettings> setGameplay,
|
||||
CombatUiLabels labels,
|
||||
Action<bool> setWindowVisible)
|
||||
{
|
||||
_root = layout.Root;
|
||||
|
|
@ -59,8 +76,13 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
_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
|
||||
|
|
@ -77,6 +99,22 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
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;
|
||||
SetStaticText(layout.FindElement(SpeedLabelId) as UiText, labels.Speed);
|
||||
SetStaticText(layout.FindElement(PowerLabelId) as UiText, labels.Power);
|
||||
|
||||
_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();
|
||||
|
|
@ -86,11 +124,17 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
ImportedLayout layout,
|
||||
CombatState combat,
|
||||
CombatAttackController attacks,
|
||||
Func<GameplaySettings> gameplay,
|
||||
Action<GameplaySettings> setGameplay,
|
||||
CombatUiLabels labels,
|
||||
Action<bool> 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
|
||||
|
|
@ -98,12 +142,16 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
|| 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(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,
|
||||
combat, attacks, setWindowVisible);
|
||||
repeatAttacks, autoTarget, keepInView,
|
||||
combat, attacks, gameplay, setGameplay, labels, setWindowVisible);
|
||||
}
|
||||
|
||||
public void SyncVisibility() => OnCombatModeChanged(_combat.CurrentMode);
|
||||
|
|
@ -136,6 +184,22 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
_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 static void SetStaticText(UiText? text, string value)
|
||||
{
|
||||
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;
|
||||
UiText.Line[] line = [new UiText.Line(value, text.DefaultColor)];
|
||||
text.LinesProvider = () => line;
|
||||
}
|
||||
|
||||
public void OnShown() => SyncControls();
|
||||
|
|
@ -154,5 +218,60 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
_medium.OnReleased = null;
|
||||
_low.OnPressed = null;
|
||||
_low.OnReleased = null;
|
||||
_repeatAttacks.OnClick = null;
|
||||
_autoTarget.OnClick = null;
|
||||
_keepInView.OnClick = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Localized labels assigned by retail <c>gmCombatUI::PostInit</c>.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue