refactor(input): extract the gameplay frame
This commit is contained in:
parent
0bc9fda9de
commit
c557038353
24 changed files with 2433 additions and 559 deletions
180
src/AcDream.App/Combat/LiveCombatAttackOperations.cs
Normal file
180
src/AcDream.App/Combat/LiveCombatAttackOperations.cs
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
using AcDream.App.Input;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
||||
namespace AcDream.App.Combat;
|
||||
|
||||
internal interface ICombatAttackTargetSource
|
||||
{
|
||||
uint? SelectedObjectId { get; }
|
||||
uint? GetSelectedOrClosestCombatTarget(bool autoTarget);
|
||||
}
|
||||
|
||||
internal interface ICombatGameplaySettingsSource
|
||||
{
|
||||
bool AutoTarget { get; }
|
||||
bool AutoRepeatAttack { get; }
|
||||
}
|
||||
|
||||
internal sealed class GameplaySettingsState : ICombatGameplaySettingsSource
|
||||
{
|
||||
public GameplaySettings Value { get; set; } = GameplaySettings.Default;
|
||||
public bool AutoTarget => Value.AutoTarget;
|
||||
public bool AutoRepeatAttack => Value.AutoRepeatAttack;
|
||||
}
|
||||
|
||||
internal interface ICombatFeedbackSink
|
||||
{
|
||||
void Show(string message);
|
||||
}
|
||||
|
||||
internal sealed class CombatFeedbackSlot : ICombatFeedbackSink
|
||||
{
|
||||
public AcDream.UI.Abstractions.Panels.Debug.DebugVM? ViewModel { get; set; }
|
||||
public void Show(string message) => ViewModel?.AddToast(message);
|
||||
}
|
||||
|
||||
internal sealed class CombatAttackOperationsSlot : ICombatAttackOperations
|
||||
{
|
||||
private ICombatAttackOperations? _owner;
|
||||
|
||||
public void Bind(ICombatAttackOperations owner)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(owner);
|
||||
if (_owner is not null && !ReferenceEquals(_owner, owner))
|
||||
throw new InvalidOperationException(
|
||||
"Combat attack operations are already bound.");
|
||||
_owner = owner;
|
||||
}
|
||||
|
||||
public bool CanStartAttack() => _owner?.CanStartAttack() == true;
|
||||
public void PrepareAttackRequest() => _owner?.PrepareAttackRequest();
|
||||
public bool SendAttack(AttackHeight height, float power) =>
|
||||
_owner?.SendAttack(height, power) == true;
|
||||
public void SendCancelAttack() => _owner?.SendCancelAttack();
|
||||
public bool IsDualWield => _owner?.IsDualWield == true;
|
||||
public bool PlayerReadyForAttack => _owner?.PlayerReadyForAttack == true;
|
||||
public bool AutoRepeatAttack => _owner?.AutoRepeatAttack == true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Production combat request dependencies. The attack state machine retains
|
||||
/// this typed owner rather than callbacks into the application window.
|
||||
/// </summary>
|
||||
internal sealed class LiveCombatAttackOperations : ICombatAttackOperations
|
||||
{
|
||||
private readonly CombatState _combat;
|
||||
private readonly ICombatAttackTargetSource _targets;
|
||||
private readonly ICombatGameplaySettingsSource _settings;
|
||||
private readonly ILocalPlayerControllerSource _player;
|
||||
private readonly LocalPlayerOutboundController _outbound;
|
||||
private readonly ILiveInWorldSource _inWorld;
|
||||
private readonly ILiveWorldSessionSource _session;
|
||||
private readonly ICombatFeedbackSink _feedback;
|
||||
|
||||
public LiveCombatAttackOperations(
|
||||
CombatState combat,
|
||||
ICombatAttackTargetSource targets,
|
||||
ICombatGameplaySettingsSource settings,
|
||||
ILocalPlayerControllerSource player,
|
||||
LocalPlayerOutboundController outbound,
|
||||
ILiveInWorldSource inWorld,
|
||||
ILiveWorldSessionSource session,
|
||||
ICombatFeedbackSink feedback)
|
||||
{
|
||||
_combat = combat ?? throw new ArgumentNullException(nameof(combat));
|
||||
_targets = targets ?? throw new ArgumentNullException(nameof(targets));
|
||||
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
||||
_player = player ?? throw new ArgumentNullException(nameof(player));
|
||||
_outbound = outbound ?? throw new ArgumentNullException(nameof(outbound));
|
||||
_inWorld = inWorld ?? throw new ArgumentNullException(nameof(inWorld));
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
_feedback = feedback ?? throw new ArgumentNullException(nameof(feedback));
|
||||
}
|
||||
|
||||
public bool IsDualWield =>
|
||||
_player.Controller?.Motion.InterpretedState.CurrentStyle
|
||||
== CombatInputPlanner.DualWieldCombatStyle;
|
||||
|
||||
public bool PlayerReadyForAttack
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_player.Controller is not { } controller)
|
||||
return false;
|
||||
var motion = controller.Motion.InterpretedState;
|
||||
return CombatInputPlanner.PlayerInReadyPositionForAttack(
|
||||
_combat.CurrentMode,
|
||||
motion.CurrentStyle,
|
||||
motion.ForwardCommand);
|
||||
}
|
||||
}
|
||||
|
||||
public bool AutoRepeatAttack => _settings.AutoRepeatAttack;
|
||||
|
||||
public bool CanStartAttack()
|
||||
{
|
||||
if (!_inWorld.IsInWorld)
|
||||
return false;
|
||||
|
||||
if (!CombatInputPlanner.SupportsTargetedAttack(_combat.CurrentMode))
|
||||
{
|
||||
_feedback.Show("Enter melee or missile combat first");
|
||||
Console.WriteLine(
|
||||
"combat: attack ignored; not in melee/missile combat mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_targets.GetSelectedOrClosestCombatTarget(_settings.AutoTarget) is null)
|
||||
{
|
||||
_feedback.Show("No monster target");
|
||||
Console.WriteLine("combat: attack ignored; no creature target found");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SendAttack(AttackHeight height, float power)
|
||||
{
|
||||
if (!CanStartAttack()
|
||||
|| _session.CurrentSession is not { } session
|
||||
|| _targets.SelectedObjectId is not { } target)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
power = Math.Clamp(power, 0f, 1f);
|
||||
if (_combat.CurrentMode == CombatMode.Missile)
|
||||
{
|
||||
session.SendMissileAttack(target, height, power);
|
||||
Console.WriteLine(
|
||||
$"combat: missile attack target=0x{target:X8} height={height} accuracy={power:F2}");
|
||||
}
|
||||
else
|
||||
{
|
||||
session.SendMeleeAttack(target, height, power);
|
||||
Console.WriteLine(
|
||||
$"combat: melee attack target=0x{target:X8} height={height} power={power:F2}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SendCancelAttack() =>
|
||||
_session.CurrentSession?.SendCancelAttack();
|
||||
|
||||
public void PrepareAttackRequest()
|
||||
{
|
||||
if (_player.Controller is not { } controller
|
||||
|| !controller.PrepareForAttackRequest())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_outbound.TrySendMovement(
|
||||
_session.CurrentSession,
|
||||
controller,
|
||||
controller.CaptureMovementResult(mouseLookEvent: false));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue