refactor(input): extract the gameplay frame

This commit is contained in:
Erik 2026-07-22 01:38:40 +02:00
parent 0bc9fda9de
commit c557038353
24 changed files with 2433 additions and 559 deletions

View file

@ -4,6 +4,56 @@ using AcDream.UI.Abstractions.Input;
namespace AcDream.App.Combat;
internal interface ICombatAttackOperations
{
bool CanStartAttack();
void PrepareAttackRequest();
bool SendAttack(AttackHeight height, float power);
void SendCancelAttack();
bool IsDualWield { get; }
bool PlayerReadyForAttack { get; }
bool AutoRepeatAttack { get; }
}
internal sealed class DelegateCombatAttackOperations : ICombatAttackOperations
{
private readonly Func<bool> _canStartAttack;
private readonly Action _prepareAttackRequest;
private readonly Func<AttackHeight, float, bool> _sendAttack;
private readonly Action _sendCancelAttack;
private readonly Func<bool> _isDualWield;
private readonly Func<bool> _playerReadyForAttack;
private readonly Func<bool> _autoRepeatAttack;
public DelegateCombatAttackOperations(
Func<bool> canStartAttack,
Func<AttackHeight, float, bool> sendAttack,
Action? prepareAttackRequest,
Action? sendCancelAttack,
Func<bool>? isDualWield,
Func<bool>? playerReadyForAttack,
Func<bool>? autoRepeatAttack)
{
_canStartAttack = canStartAttack
?? throw new ArgumentNullException(nameof(canStartAttack));
_sendAttack = sendAttack ?? throw new ArgumentNullException(nameof(sendAttack));
_prepareAttackRequest = prepareAttackRequest ?? (() => { });
_sendCancelAttack = sendCancelAttack ?? (() => { });
_isDualWield = isDualWield ?? (() => false);
_playerReadyForAttack = playerReadyForAttack ?? (() => true);
_autoRepeatAttack = autoRepeatAttack ?? (() => false);
}
public bool CanStartAttack() => _canStartAttack();
public void PrepareAttackRequest() => _prepareAttackRequest();
public bool SendAttack(AttackHeight height, float power) =>
_sendAttack(height, power);
public void SendCancelAttack() => _sendCancelAttack();
public bool IsDualWield => _isDualWield();
public bool PlayerReadyForAttack => _playerReadyForAttack();
public bool AutoRepeatAttack => _autoRepeatAttack();
}
/// <summary>
/// Owns retail's basic-combat attack request and power-bar state machine.
/// UI buttons and keyboard actions both enter through <see cref="PressAttack"/>
@ -26,13 +76,7 @@ public sealed class CombatAttackController : IDisposable
public const float DesiredPowerStep = 1f / 6f;
private readonly CombatState _combat;
private readonly Func<bool> _canStartAttack;
private readonly Action _prepareAttackRequest;
private readonly Func<AttackHeight, float, bool> _sendAttack;
private readonly Action _sendCancelAttack;
private readonly Func<bool> _isDualWield;
private readonly Func<bool> _playerReadyForAttack;
private readonly Func<bool> _autoRepeatAttack;
private readonly ICombatAttackOperations _operations;
private readonly Func<double> _now;
private bool _buildInProgress;
@ -57,15 +101,27 @@ public sealed class CombatAttackController : IDisposable
Func<bool>? playerReadyForAttack = null,
Func<bool>? autoRepeatAttack = null,
Func<double>? now = null)
: this(
combat,
new DelegateCombatAttackOperations(
canStartAttack,
sendAttack,
prepareAttackRequest,
sendCancelAttack,
isDualWield,
playerReadyForAttack,
autoRepeatAttack),
now)
{
}
internal CombatAttackController(
CombatState combat,
ICombatAttackOperations operations,
Func<double>? now = null)
{
_combat = combat ?? throw new ArgumentNullException(nameof(combat));
_canStartAttack = canStartAttack ?? throw new ArgumentNullException(nameof(canStartAttack));
_prepareAttackRequest = prepareAttackRequest ?? (() => { });
_sendAttack = sendAttack ?? throw new ArgumentNullException(nameof(sendAttack));
_sendCancelAttack = sendCancelAttack ?? (() => { });
_isDualWield = isDualWield ?? (() => false);
_playerReadyForAttack = playerReadyForAttack ?? (() => true);
_autoRepeatAttack = autoRepeatAttack ?? (() => false);
_operations = operations ?? throw new ArgumentNullException(nameof(operations));
_now = now ?? (() => Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency);
_combat.CombatModeChanged += OnCombatModeChanged;
@ -196,7 +252,7 @@ public sealed class CombatAttackController : IDisposable
&& !_repeatAttacking)
return;
_sendCancelAttack();
_operations.SendCancelAttack();
_repeatAttacking = false;
if (_buildInProgress)
@ -219,7 +275,7 @@ public sealed class CombatAttackController : IDisposable
if (!_buildInProgress)
return;
if (!_playerReadyForAttack())
if (!_operations.PlayerReadyForAttack)
{
if (_attackRequestInProgress)
{
@ -258,7 +314,7 @@ public sealed class CombatAttackController : IDisposable
private void StartAttackRequest()
{
if (!CombatInputPlanner.SupportsTargetedAttack(_combat.CurrentMode)
|| !_canStartAttack())
|| !_operations.CanStartAttack())
return;
// Retail StartAttackRequest (0x0056C040) stores request-in-progress
@ -267,7 +323,7 @@ public sealed class CombatAttackController : IDisposable
// player movement object and must run before any later attack send.
_attackRequestInProgress = true;
_requestedAttackPower = 1f;
_prepareAttackRequest();
_operations.PrepareAttackRequest();
_currentBuildIsAutomatic = false;
AttemptStartBuildingAttack();
}
@ -276,7 +332,7 @@ public sealed class CombatAttackController : IDisposable
{
if (_buildInProgress
|| _attackServerResponsePending
|| !_playerReadyForAttack())
|| !_operations.PlayerReadyForAttack)
return;
StartPowerBarBuild();
}
@ -292,7 +348,7 @@ public sealed class CombatAttackController : IDisposable
{
if (!_buildInProgress)
return 0f;
double duration = _isDualWield()
double duration = _operations.IsDualWield
? DualWieldPowerUpSeconds
: AttackPowerUpSeconds;
return (float)Math.Clamp((_now() - _buildStartTime) / duration, 0d, 1d);
@ -301,13 +357,15 @@ public sealed class CombatAttackController : IDisposable
private void ExecuteAttack(AttackHeight height, bool setServerPending)
{
StopBuild();
if (!_sendAttack(height, Math.Clamp(_requestedAttackPower, 0f, 1f)))
if (!_operations.SendAttack(
height,
Math.Clamp(_requestedAttackPower, 0f, 1f)))
{
ResetPowerBar();
return;
}
if (_autoRepeatAttack())
if (_operations.AutoRepeatAttack)
_repeatAttacking = true;
_attackServerResponsePending = setServerPending;
}
@ -330,7 +388,7 @@ public sealed class CombatAttackController : IDisposable
_repeatAttacking = false;
if (!_attackRequestInProgress
&& _autoRepeatAttack()
&& _operations.AutoRepeatAttack
&& _repeatAttacking)
{
if (Math.Abs(_requestedAttackPower - DesiredPower) >= 0.01f)
@ -338,7 +396,7 @@ public sealed class CombatAttackController : IDisposable
ExecuteAttack(RequestedHeight, setServerPending: false);
}
if (!_autoRepeatAttack() || !_repeatAttacking)
if (!_operations.AutoRepeatAttack || !_repeatAttacking)
{
_repeatAttacking = false;
ResetPowerBar();