acdream/src/AcDream.Runtime/Gameplay/RuntimeCombatTargetState.cs
Erik 20df9d155d refactor(runtime): own combat and magic intent
Move attack build/repeat state, combat-mode policy, authoritative auto-target transitions, and spell-cast intent beneath RuntimeActionState. Keep App as the input, world-query, DAT-policy, transport, and presentation adapter while preserving retail request and busy ordering. Add direct/graphical parity, reset, failure, and instance-isolation coverage.

Co-authored-by: Codex <noreply@openai.com>
2026-07-26 11:56:40 +02:00

77 lines
2.5 KiB
C#

using AcDream.Core.Combat;
using AcDream.Core.Physics;
using AcDream.Core.Selection;
namespace AcDream.Runtime.Gameplay;
public interface IRuntimeCombatTargetOperations
{
bool AutoTarget { get; }
uint? SelectClosestTarget();
}
/// <summary>
/// Owns combat-target lifecycle around the shared selection state.
/// </summary>
/// <remarks>
/// Retail <c>ClientCombatSystem::RecvNotice_SelectionChanged</c>
/// (0x0056BD80) invokes <c>AutoTarget</c> when selection becomes zero while
/// Auto Target is enabled in Melee/Missile combat. Authoritative Dead motion
/// makes the selected combat object unavailable and enters that same notice
/// path.
/// </remarks>
public sealed class RuntimeCombatTargetState : IDisposable
{
private readonly CombatState _combat;
private readonly SelectionState _selection;
private readonly IRuntimeCombatTargetOperations _operations;
private bool _disposed;
public bool IsDisposed => _disposed;
public RuntimeCombatTargetState(
CombatState combat,
SelectionState selection,
IRuntimeCombatTargetOperations operations)
{
_combat = combat ?? throw new ArgumentNullException(nameof(combat));
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
_operations = operations
?? throw new ArgumentNullException(nameof(operations));
_selection.Changed += OnSelectionChanged;
}
/// <summary>
/// Called after an accepted UpdateMotion has reached the entity's motion
/// table. Dead is a persistent substate, so CurrentMotion is the
/// authoritative availability signal rather than a damage prediction.
/// </summary>
public void OnMotionApplied(uint objectId, uint currentMotion)
{
if (currentMotion != MotionCommand.Dead
|| _selection.SelectedObjectId != objectId)
return;
_selection.Clear(
SelectionChangeSource.System,
SelectionChangeReason.CombatTargetDied);
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_selection.Changed -= OnSelectionChanged;
}
private void OnSelectionChanged(SelectionTransition transition)
{
if (transition.SelectedObjectId is not null
|| transition.Reason == SelectionChangeReason.SessionReset
|| !_operations.AutoTarget
|| !CombatInputPlanner.SupportsTargetedAttack(_combat.CurrentMode))
return;
_operations.SelectClosestTarget();
}
}