using AcDream.Core.Combat; using AcDream.Core.Physics; using AcDream.Core.Selection; namespace AcDream.App.Combat; /// /// Owns combat-target lifecycle around the shared selection state. /// /// /// Retail ClientCombatSystem::RecvNotice_SelectionChanged /// (0x0056BD80) invokes AutoTarget 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. /// public sealed class CombatTargetController : IDisposable { private readonly CombatState _combat; private readonly SelectionState _selection; private readonly Func _autoTarget; private readonly Func _selectClosestTarget; private bool _disposed; public CombatTargetController( CombatState combat, SelectionState selection, Func autoTarget, Func selectClosestTarget) { _combat = combat ?? throw new ArgumentNullException(nameof(combat)); _selection = selection ?? throw new ArgumentNullException(nameof(selection)); _autoTarget = autoTarget ?? throw new ArgumentNullException(nameof(autoTarget)); _selectClosestTarget = selectClosestTarget ?? throw new ArgumentNullException(nameof(selectClosestTarget)); _selection.Changed += OnSelectionChanged; } /// /// 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. /// 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 || !_autoTarget() || !CombatInputPlanner.SupportsTargetedAttack(_combat.CurrentMode)) return; _selectClosestTarget(); } }