feat(combat): port retail basic combat bar
Mount authored gmCombatUI, share one press/hold/release request state machine across DAT buttons and keybindings, and recover the exact 1.0s/0.8s power timing from matching retail x86. The same timer fixes jump charge, while ready-stance, response queueing, auto-repeat, layout binding, migration, and conformance coverage keep behavior architectural rather than panel-local. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
9958458318
commit
2215c76c7e
22 changed files with 1265 additions and 46 deletions
158
src/AcDream.App/UI/Layout/CombatUiController.cs
Normal file
158
src/AcDream.App/UI/Layout/CombatUiController.cs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
using AcDream.App.Combat;
|
||||
using AcDream.Core.Combat;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Binds the imported retail <c>gmCombatUI</c> layout to the client combat
|
||||
/// state machine. No panel geometry is synthesized here: every control is the
|
||||
/// authored element from LayoutDesc <c>0x21000073</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Retail references: <c>gmCombatUI::RecvNotice_AttackHeightChanged</c>
|
||||
/// (0x004CC080), <c>RecvNotice_SetPowerbarLevel</c> (0x004CC0E0),
|
||||
/// <c>RecvNotice_DesiredAttackPowerChanged</c> (0x004CC110),
|
||||
/// <c>ListenToElementMessage</c> (0x004CC430), and
|
||||
/// <c>RecvNotice_SetCombatMode</c> (0x004CC620).
|
||||
/// </remarks>
|
||||
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 HighButtonId = 0x10000057u;
|
||||
public const uint MediumButtonId = 0x10000058u;
|
||||
public const uint LowButtonId = 0x10000059u;
|
||||
|
||||
private const uint MeleeState = 0x10000003u;
|
||||
private 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 CombatState _combat;
|
||||
private readonly CombatAttackController _attacks;
|
||||
private readonly Action<bool> _setWindowVisible;
|
||||
private bool _disposed;
|
||||
|
||||
private CombatUiController(
|
||||
ImportedLayout layout,
|
||||
UiElement basicPanel,
|
||||
UiElement advancedPanel,
|
||||
UiScrollbar powerControl,
|
||||
UiButton high,
|
||||
UiButton medium,
|
||||
UiButton low,
|
||||
CombatState combat,
|
||||
CombatAttackController attacks,
|
||||
Action<bool> setWindowVisible)
|
||||
{
|
||||
_root = layout.Root;
|
||||
_basicPanel = basicPanel;
|
||||
_advancedPanel = advancedPanel;
|
||||
_powerControl = powerControl;
|
||||
_high = high;
|
||||
_medium = medium;
|
||||
_low = low;
|
||||
_combat = combat;
|
||||
_attacks = attacks;
|
||||
_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 = () => _attacks.PowerBarLevel;
|
||||
|
||||
BindAttackButton(_high, AttackHeight.High);
|
||||
BindAttackButton(_medium, AttackHeight.Medium);
|
||||
BindAttackButton(_low, AttackHeight.Low);
|
||||
|
||||
_combat.CombatModeChanged += OnCombatModeChanged;
|
||||
_attacks.StateChanged += OnAttackStateChanged;
|
||||
SyncControls();
|
||||
}
|
||||
|
||||
public static CombatUiController? Bind(
|
||||
ImportedLayout layout,
|
||||
CombatState combat,
|
||||
CombatAttackController attacks,
|
||||
Action<bool> setWindowVisible)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(layout);
|
||||
ArgumentNullException.ThrowIfNull(combat);
|
||||
ArgumentNullException.ThrowIfNull(attacks);
|
||||
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)
|
||||
return null;
|
||||
|
||||
return new CombatUiController(
|
||||
layout, basic, advanced, power, high, medium, low,
|
||||
combat, attacks, 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;
|
||||
}
|
||||
|
||||
public void OnShown() => SyncControls();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_combat.CombatModeChanged -= OnCombatModeChanged;
|
||||
_attacks.StateChanged -= OnAttackStateChanged;
|
||||
_powerControl.ScalarChanged = null;
|
||||
_powerControl.ScalarFill = () => null;
|
||||
_high.OnPressed = null;
|
||||
_high.OnReleased = null;
|
||||
_medium.OnPressed = null;
|
||||
_medium.OnReleased = null;
|
||||
_low.OnPressed = null;
|
||||
_low.OnReleased = null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue