282 lines
7.8 KiB
C#
282 lines
7.8 KiB
C#
using System.Diagnostics;
|
|
using Nexus.Core;
|
|
using Nexus.Game;
|
|
using Nexus.Screen;
|
|
using Serilog;
|
|
|
|
namespace Nexus.Bot;
|
|
|
|
/// <summary>
|
|
/// Manages the attack state machine (click → hold) with mana monitoring and flask usage.
|
|
/// Call <see cref="Tick"/> each combat loop iteration, <see cref="Reset"/> between phases,
|
|
/// and <see cref="ReleaseAll"/> when done.
|
|
/// </summary>
|
|
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<int> _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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// One combat iteration: flask check, mana-based click/hold, mouse jitter toward target.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Walk toward chase target using held WASD keys. Replaces orbit when active.
|
|
/// </summary>
|
|
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<int>();
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cycle WASD directions to orbit in a small circle while attacking.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Press Q and E abilities every ~6 seconds.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset state for a new combat phase (releases held buttons if any).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Release any held mouse buttons and movement keys. Call in finally blocks.
|
|
/// </summary>
|
|
public async Task ReleaseAll()
|
|
{
|
|
if (_holding)
|
|
{
|
|
_game.LeftMouseUp();
|
|
_game.RightMouseUp();
|
|
_holding = false;
|
|
}
|
|
await ReleaseOrbit();
|
|
await ReleaseChaseKeys();
|
|
_chaseX = -1;
|
|
_chaseY = -1;
|
|
}
|
|
}
|