using System.Diagnostics; using Nexus.Core; using Nexus.Game; using Nexus.Screen; using Serilog; namespace Nexus.Bot; /// /// Manages the attack state machine (click → hold) with mana monitoring and flask usage. /// Call each combat loop iteration, between phases, /// and when done. /// public class CombatManager { private static readonly Random Rng = new(); // Orbit: cycle W→D→S→A to dodge in a small circle private static readonly int[] OrbitKeys = [InputSender.VK.W, InputSender.VK.D, InputSender.VK.S, InputSender.VK.A]; private const int OrbitStepMinMs = 60; // short tap per direction → ~10px radius private const int OrbitStepMaxMs = 120; private readonly IGameController _game; private readonly HudReader _hudReader; private readonly FlaskManager _flasks; private bool _holding; private int _manaStableCount; private readonly Stopwatch _orbitSw = Stopwatch.StartNew(); private int _orbitIndex = -1; private long _lastOrbitMs; private int _nextOrbitMs = OrbitStepMinMs; // Ability rotation — press Q and E every ~6 seconds private long _lastAbilityMs; // Chase — walks toward a screen position instead of orbiting private volatile int _chaseX = -1; private volatile int _chaseY = -1; private readonly HashSet _chaseKeys = new(); // Smoothed mouse position — lerps toward target to avoid jitter private double _smoothX = 1280; private double _smoothY = 660; private const double SmoothFactor = 0.25; // 0=no movement, 1=instant snap public bool IsHolding => _holding; public void SetChaseTarget(int screenX, int screenY) { _chaseX = screenX; _chaseY = screenY; } public void ClearChaseTarget() { _chaseX = -1; _chaseY = -1; } public CombatManager(IGameController game, HudReader hudReader, FlaskManager flasks) { _game = game; _hudReader = hudReader; _flasks = flasks; } /// /// One combat iteration: flask check, mana-based click/hold, mouse jitter toward target. /// public async Task Tick(int x, int y, int jitter = 0) { await _flasks.Tick(); await UseAbilities(); if (_chaseX >= 0) { await UpdateChase(); } else { if (_chaseKeys.Count > 0) await ReleaseChaseKeys(); await UpdateOrbit(); } // Lerp smoothed position toward target _smoothX += (x - _smoothX) * SmoothFactor; _smoothY += (y - _smoothY) * SmoothFactor; var mouseX = (int)_smoothX; var mouseY = (int)_smoothY; var mana = _hudReader.Current.ManaPct; if (!_holding) { if (mana >= 0.80f) _manaStableCount++; else _manaStableCount = 0; _game.MoveMouseInstant(mouseX, mouseY); _game.LeftMouseDown(); await Helpers.Sleep(Rng.Next(20, 35)); _game.LeftMouseUp(); _game.RightMouseDown(); await Helpers.Sleep(Rng.Next(20, 35)); _game.RightMouseUp(); await Helpers.Sleep(Rng.Next(50, 80)); if (_manaStableCount >= 5) { Log.Information("Mana stable at {Mana:P0}, switching to hold attack", mana); _game.LeftMouseDown(); _game.RightMouseDown(); _holding = true; } } else { _game.MoveMouseInstant(mouseX, mouseY); if (mana < 0.30f) { Log.Information("Mana dropped to {Mana:P0}, releasing to recover", mana); _game.LeftMouseUp(); _game.RightMouseUp(); _holding = false; _manaStableCount = 0; } await Helpers.Sleep(10); } } /// /// Walk toward chase target using held WASD keys. Replaces orbit when active. /// private async Task UpdateChase() { // Release orbit key when entering chase mode if (_orbitIndex >= 0) { await _game.KeyUp(OrbitKeys[_orbitIndex]); _orbitIndex = -1; } const int screenCx = 1280, screenCy = 660; var cx = _chaseX; var cy = _chaseY; var dx = cx - screenCx; var dy = cy - screenCy; var dist = Math.Sqrt(dx * dx + dy * dy); var wanted = new HashSet(); if (dist > 100) { var dirX = dx / dist; var dirY = dy / dist; if (dirY < -0.3) wanted.Add(InputSender.VK.W); if (dirY > 0.3) wanted.Add(InputSender.VK.S); if (dirX < -0.3) wanted.Add(InputSender.VK.A); if (dirX > 0.3) wanted.Add(InputSender.VK.D); } foreach (var k in _chaseKeys.Except(wanted).ToList()) { await _game.KeyUp(k); _chaseKeys.Remove(k); } foreach (var k in wanted.Except(_chaseKeys).ToList()) { await _game.KeyDown(k); _chaseKeys.Add(k); } } private async Task ReleaseChaseKeys() { foreach (var k in _chaseKeys) await _game.KeyUp(k); _chaseKeys.Clear(); } /// /// Cycle WASD directions to orbit in a small circle while attacking. /// private async Task UpdateOrbit() { var now = _orbitSw.ElapsedMilliseconds; if (now - _lastOrbitMs < _nextOrbitMs) return; _lastOrbitMs = now; _nextOrbitMs = Rng.Next(OrbitStepMinMs, OrbitStepMaxMs + 1); // Release previous direction if (_orbitIndex >= 0) await _game.KeyUp(OrbitKeys[_orbitIndex]); // Occasionally skip a direction to make movement less predictable var skip = Rng.Next(0, 5) == 0 ? 2 : 1; _orbitIndex = (_orbitIndex + skip) % OrbitKeys.Length; await _game.KeyDown(OrbitKeys[_orbitIndex]); } private async Task ReleaseOrbit() { if (_orbitIndex >= 0) { await _game.KeyUp(OrbitKeys[_orbitIndex]); _orbitIndex = -1; } } /// /// Press Q and E abilities every ~6 seconds. /// private async Task UseAbilities() { var now = _orbitSw.ElapsedMilliseconds; if (now - _lastAbilityMs < 6000) return; _lastAbilityMs = now; // Release held attacks before abilities if (_holding) { _game.LeftMouseUp(); _game.RightMouseUp(); } await _game.PressKey(InputSender.VK.Q); await Helpers.Sleep(Rng.Next(80, 120)); await _game.PressKey(InputSender.VK.E); await Helpers.Sleep(Rng.Next(80, 120)); // Re-engage attacks if (_holding) { _game.LeftMouseDown(); _game.RightMouseDown(); } } /// /// Reset state for a new combat phase (releases held buttons if any). /// public async Task Reset() { if (_holding) { _game.LeftMouseUp(); _game.RightMouseUp(); } _holding = false; _manaStableCount = 0; _smoothX = 1280; _smoothY = 660; await ReleaseOrbit(); await ReleaseChaseKeys(); _chaseX = -1; _chaseY = -1; } /// /// Release any held mouse buttons and movement keys. Call in finally blocks. /// public async Task ReleaseAll() { if (_holding) { _game.LeftMouseUp(); _game.RightMouseUp(); _holding = false; } await ReleaseOrbit(); await ReleaseChaseKeys(); _chaseX = -1; _chaseY = -1; } }