rwork on kulemak bot and cleanup
This commit is contained in:
parent
c75b2b27f0
commit
053a016c8b
15 changed files with 727 additions and 160 deletions
|
|
@ -3,38 +3,37 @@ using Poe2Trade.Core;
|
|||
using Poe2Trade.Game;
|
||||
using Poe2Trade.GameLog;
|
||||
using Poe2Trade.Inventory;
|
||||
using Poe2Trade.Navigation;
|
||||
using Poe2Trade.Screen;
|
||||
using Serilog;
|
||||
|
||||
namespace Poe2Trade.Bot;
|
||||
|
||||
public class BossRunExecutor
|
||||
public class BossRunExecutor : GameExecutor
|
||||
{
|
||||
private static readonly string WellOfSoulsTemplate = Path.Combine("assets", "well-of-souls.png");
|
||||
private static readonly string BlackCathedralTemplate = Path.Combine("assets", "black-cathedral.png");
|
||||
private static readonly string InvitationTemplate = Path.Combine("assets", "invitation.png");
|
||||
private static readonly string CathedralDoorTemplate = Path.Combine("assets", "black-cathedral-door.png");
|
||||
private static readonly string ReturnTheRingTemplate = Path.Combine("assets", "return-the-ring.png");
|
||||
|
||||
private BossRunState _state = BossRunState.Idle;
|
||||
private bool _stopped;
|
||||
private readonly IGameController _game;
|
||||
private readonly IScreenReader _screen;
|
||||
private readonly IInventoryManager _inventory;
|
||||
private readonly IClientLogWatcher _logWatcher;
|
||||
private readonly SavedSettings _config;
|
||||
private readonly BossDetector _bossDetector;
|
||||
private readonly HudReader _hudReader;
|
||||
private readonly NavigationExecutor _nav;
|
||||
|
||||
public event Action<BossRunState>? StateChanged;
|
||||
|
||||
public BossRunExecutor(IGameController game, IScreenReader screen,
|
||||
IInventoryManager inventory, IClientLogWatcher logWatcher, SavedSettings config,
|
||||
BossDetector bossDetector)
|
||||
BossDetector bossDetector, HudReader hudReader, NavigationExecutor nav)
|
||||
: base(game, screen, inventory, config)
|
||||
{
|
||||
_game = game;
|
||||
_screen = screen;
|
||||
_inventory = inventory;
|
||||
_logWatcher = logWatcher;
|
||||
_config = config;
|
||||
_bossDetector = bossDetector;
|
||||
_hudReader = hudReader;
|
||||
_nav = nav;
|
||||
}
|
||||
|
||||
public BossRunState State => _state;
|
||||
|
|
@ -45,9 +44,9 @@ public class BossRunExecutor
|
|||
StateChanged?.Invoke(s);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
public override void Stop()
|
||||
{
|
||||
_stopped = true;
|
||||
base.Stop();
|
||||
Log.Information("Boss run executor stop requested");
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +99,7 @@ public class BossRunExecutor
|
|||
await Fight();
|
||||
if (_stopped) break;
|
||||
|
||||
SetState(BossRunState.Looting);
|
||||
await Loot();
|
||||
if (_stopped) break;
|
||||
|
||||
|
|
@ -299,17 +299,251 @@ public class BossRunExecutor
|
|||
private async Task Fight()
|
||||
{
|
||||
SetState(BossRunState.Fighting);
|
||||
Log.Information("[PLACEHOLDER] Fight phase - waiting for manual combat");
|
||||
// Placeholder: user handles combat manually for now
|
||||
await Helpers.Sleep(1000);
|
||||
Log.Information("Fight phase starting");
|
||||
|
||||
// Wait for arena to settle
|
||||
await Helpers.Sleep(6000);
|
||||
if (_stopped) return;
|
||||
|
||||
// Find and click the cathedral door
|
||||
Log.Information("Looking for cathedral door...");
|
||||
var door = await _screen.TemplateMatch(CathedralDoorTemplate);
|
||||
if (door == null)
|
||||
{
|
||||
Log.Error("Could not find cathedral door template");
|
||||
return;
|
||||
}
|
||||
Log.Information("Found cathedral door at ({X},{Y}), clicking", door.X, door.Y);
|
||||
await _game.LeftClickAt(door.X, door.Y);
|
||||
|
||||
// Wait for cathedral interior to load
|
||||
await Helpers.Sleep(12000);
|
||||
if (_stopped) return;
|
||||
|
||||
// Walk to fight area (world coords)
|
||||
const double fightWorldX = -454;
|
||||
const double fightWorldY = -332;
|
||||
const double wellWorldX = -496;
|
||||
const double wellWorldY = -378;
|
||||
|
||||
await WalkToWorldPosition(fightWorldX, fightWorldY);
|
||||
if (_stopped) return;
|
||||
|
||||
// 3x fight-then-well loop
|
||||
for (var phase = 1; phase <= 3; phase++)
|
||||
{
|
||||
if (_stopped) return;
|
||||
Log.Information("=== Boss phase {Phase}/4 ===", phase);
|
||||
|
||||
await AttackBossUntilGone();
|
||||
if (_stopped) return;
|
||||
|
||||
// Walk to well and click it
|
||||
Log.Information("Phase {Phase} done, walking to well", phase);
|
||||
await WalkToWorldPosition(wellWorldX, wellWorldY);
|
||||
// Click at screen center (well should be near character)
|
||||
await _game.LeftClickAt(1280, 720);
|
||||
await Helpers.Sleep(2000);
|
||||
|
||||
// Walk back to fight position for next phase
|
||||
await WalkToWorldPosition(fightWorldX, fightWorldY);
|
||||
}
|
||||
|
||||
// 4th fight - no well after
|
||||
if (_stopped) return;
|
||||
Log.Information("=== Boss phase 4/4 ===");
|
||||
await AttackBossUntilGone();
|
||||
if (_stopped) return;
|
||||
|
||||
// Return the ring
|
||||
Log.Information("Looking for Return the Ring...");
|
||||
var ring = await _screen.TemplateMatch(ReturnTheRingTemplate);
|
||||
if (ring == null)
|
||||
{
|
||||
Log.Warning("Could not find Return the Ring template, retrying after 2s...");
|
||||
await Helpers.Sleep(2000);
|
||||
ring = await _screen.TemplateMatch(ReturnTheRingTemplate);
|
||||
}
|
||||
if (ring != null)
|
||||
{
|
||||
Log.Information("Found Return the Ring at ({X},{Y}), clicking", ring.X, ring.Y);
|
||||
await _game.LeftClickAt(ring.X, ring.Y);
|
||||
await Helpers.Sleep(2000);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("Could not find Return the Ring template");
|
||||
}
|
||||
if (_stopped) return;
|
||||
|
||||
// Walk up and press Q
|
||||
Log.Information("Walking up and pressing Q");
|
||||
await _game.KeyDown(InputSender.VK.W);
|
||||
await Helpers.Sleep(1500);
|
||||
await _game.KeyUp(InputSender.VK.W);
|
||||
await Helpers.Sleep(300);
|
||||
await _game.PressKey(InputSender.VK.Q);
|
||||
await Helpers.Sleep(500);
|
||||
|
||||
// Spam L+R at position for 7s
|
||||
Log.Information("Attacking at ring fight position (Q phase)");
|
||||
await AttackAtPosition(1280, 720, 7000);
|
||||
if (_stopped) return;
|
||||
|
||||
// Press E, spam L+R at same position for 7s
|
||||
Log.Information("Pressing E and continuing attack");
|
||||
await _game.PressKey(InputSender.VK.E);
|
||||
await Helpers.Sleep(500);
|
||||
await AttackAtPosition(1280, 720, 7000);
|
||||
|
||||
Log.Information("Fight complete");
|
||||
}
|
||||
|
||||
private async Task Loot()
|
||||
private async Task AttackBossUntilGone(int timeoutMs = 120_000)
|
||||
{
|
||||
SetState(BossRunState.Looting);
|
||||
Log.Information("[PLACEHOLDER] Loot phase - waiting for manual looting");
|
||||
// Placeholder: user handles looting manually for now
|
||||
await Helpers.Sleep(1000);
|
||||
// Move mouse to screen center initially
|
||||
await _game.MoveMouseFast(1280, 720);
|
||||
await Helpers.Sleep(200);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var consecutiveMisses = 0;
|
||||
|
||||
while (sw.ElapsedMilliseconds < timeoutMs)
|
||||
{
|
||||
if (_stopped) return;
|
||||
|
||||
var snapshot = _bossDetector.Latest;
|
||||
if (snapshot.Bosses.Count > 0)
|
||||
{
|
||||
consecutiveMisses = 0;
|
||||
var boss = snapshot.Bosses[0];
|
||||
|
||||
// Check mana before attacking
|
||||
var hud = _hudReader.Current;
|
||||
if (hud.ManaPct < 0.80f)
|
||||
{
|
||||
await Helpers.Sleep(200);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Move to boss and attack
|
||||
var targetX = boss.Cx + Rng.Next(-10, 11);
|
||||
var targetY = boss.Cy + Rng.Next(-10, 11);
|
||||
await _game.MoveMouseFast(targetX, targetY);
|
||||
|
||||
_game.LeftMouseDown();
|
||||
await Helpers.Sleep(Rng.Next(30, 50));
|
||||
_game.LeftMouseUp();
|
||||
await Helpers.Sleep(Rng.Next(20, 40));
|
||||
_game.RightMouseDown();
|
||||
await Helpers.Sleep(Rng.Next(30, 50));
|
||||
_game.RightMouseUp();
|
||||
|
||||
await Helpers.Sleep(Rng.Next(100, 150));
|
||||
}
|
||||
else
|
||||
{
|
||||
consecutiveMisses++;
|
||||
if (consecutiveMisses >= 15)
|
||||
{
|
||||
Log.Information("Boss gone after {Ms}ms ({Misses} consecutive misses)",
|
||||
sw.ElapsedMilliseconds, consecutiveMisses);
|
||||
return;
|
||||
}
|
||||
await Helpers.Sleep(200);
|
||||
}
|
||||
}
|
||||
|
||||
Log.Warning("AttackBossUntilGone timed out after {Ms}ms", timeoutMs);
|
||||
}
|
||||
|
||||
private async Task AttackAtPosition(int x, int y, int durationMs)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
while (sw.ElapsedMilliseconds < durationMs)
|
||||
{
|
||||
if (_stopped) return;
|
||||
|
||||
var targetX = x + Rng.Next(-20, 21);
|
||||
var targetY = y + Rng.Next(-20, 21);
|
||||
await _game.MoveMouseFast(targetX, targetY);
|
||||
|
||||
_game.LeftMouseDown();
|
||||
await Helpers.Sleep(Rng.Next(30, 50));
|
||||
_game.LeftMouseUp();
|
||||
await Helpers.Sleep(Rng.Next(20, 40));
|
||||
_game.RightMouseDown();
|
||||
await Helpers.Sleep(Rng.Next(30, 50));
|
||||
_game.RightMouseUp();
|
||||
|
||||
await Helpers.Sleep(Rng.Next(100, 150));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Walk to a world position using WASD keys, checking minimap position each iteration.
|
||||
/// </summary>
|
||||
private async Task WalkToWorldPosition(double worldX, double worldY, int timeoutMs = 10000, double arrivalDist = 15)
|
||||
{
|
||||
Log.Information("Walking to world ({X:F0},{Y:F0})", worldX, worldY);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var heldKeys = new HashSet<int>();
|
||||
|
||||
try
|
||||
{
|
||||
while (sw.ElapsedMilliseconds < timeoutMs)
|
||||
{
|
||||
if (_stopped) break;
|
||||
|
||||
var pos = _nav.WorldPosition;
|
||||
var dx = worldX - pos.X;
|
||||
var dy = worldY - pos.Y;
|
||||
var dist = Math.Sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist <= arrivalDist)
|
||||
{
|
||||
Log.Information("Arrived at ({X:F0},{Y:F0}), dist={Dist:F0}", pos.X, pos.Y, dist);
|
||||
break;
|
||||
}
|
||||
|
||||
// Normalize direction
|
||||
var len = Math.Sqrt(dx * dx + dy * dy);
|
||||
var dirX = dx / len;
|
||||
var dirY = dy / len;
|
||||
|
||||
// Map direction to WASD keys
|
||||
var wanted = new HashSet<int>();
|
||||
if (dirY < -0.3) wanted.Add(InputSender.VK.W); // up
|
||||
if (dirY > 0.3) wanted.Add(InputSender.VK.S); // down
|
||||
if (dirX < -0.3) wanted.Add(InputSender.VK.A); // left
|
||||
if (dirX > 0.3) wanted.Add(InputSender.VK.D); // right
|
||||
|
||||
// Release keys no longer needed
|
||||
foreach (var key in heldKeys.Except(wanted).ToList())
|
||||
{
|
||||
await _game.KeyUp(key);
|
||||
heldKeys.Remove(key);
|
||||
}
|
||||
// Press new keys
|
||||
foreach (var key in wanted.Except(heldKeys).ToList())
|
||||
{
|
||||
await _game.KeyDown(key);
|
||||
heldKeys.Add(key);
|
||||
}
|
||||
|
||||
await Helpers.Sleep(100);
|
||||
}
|
||||
|
||||
if (sw.ElapsedMilliseconds >= timeoutMs)
|
||||
Log.Warning("WalkToWorldPosition timed out after {Ms}ms", timeoutMs);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Release all held keys
|
||||
foreach (var key in heldKeys)
|
||||
await _game.KeyUp(key);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ReturnHome()
|
||||
|
|
@ -408,122 +642,4 @@ public class BossRunExecutor
|
|||
|
||||
Log.Information("Loot stored");
|
||||
}
|
||||
|
||||
private async Task<TemplateMatchResult?> WalkAndMatch(string templatePath, int vk1, int vk2,
|
||||
int timeoutMs = 15000, int closeRadius = 350)
|
||||
{
|
||||
const int screenCx = 2560 / 2;
|
||||
const int screenCy = 1440 / 2;
|
||||
|
||||
await _game.KeyDown(vk1);
|
||||
await _game.KeyDown(vk2);
|
||||
try
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
bool spotted = false;
|
||||
while (sw.ElapsedMilliseconds < timeoutMs)
|
||||
{
|
||||
if (_stopped) return null;
|
||||
var match = await _screen.TemplateMatch(templatePath);
|
||||
if (match == null)
|
||||
{
|
||||
await Helpers.Sleep(500);
|
||||
continue;
|
||||
}
|
||||
|
||||
var dx = match.X - screenCx;
|
||||
var dy = match.Y - screenCy;
|
||||
var dist = Math.Sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (!spotted)
|
||||
{
|
||||
Log.Information("Template spotted at ({X},{Y}), dist={Dist:F0}px from center, approaching...",
|
||||
match.X, match.Y, dist);
|
||||
spotted = true;
|
||||
}
|
||||
|
||||
if (dist <= closeRadius)
|
||||
{
|
||||
Log.Information("Close enough at ({X},{Y}), dist={Dist:F0}px, stopping", match.X, match.Y, dist);
|
||||
|
||||
// Stop, settle, re-match for accurate position
|
||||
await _game.KeyUp(vk2);
|
||||
await _game.KeyUp(vk1);
|
||||
await Helpers.Sleep(300);
|
||||
|
||||
var fresh = await _screen.TemplateMatch(templatePath);
|
||||
if (fresh != null)
|
||||
{
|
||||
Log.Information("Final position at ({X},{Y})", fresh.X, fresh.Y);
|
||||
return fresh;
|
||||
}
|
||||
Log.Warning("Re-match failed, using last known position");
|
||||
return match;
|
||||
}
|
||||
|
||||
await Helpers.Sleep(200);
|
||||
}
|
||||
Log.Error("WalkAndMatch timed out after {Ms}ms (spotted={Spotted})", timeoutMs, spotted);
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _game.KeyUp(vk2);
|
||||
await _game.KeyUp(vk1);
|
||||
}
|
||||
}
|
||||
|
||||
private (StashTabInfo? Tab, StashTabInfo? Folder) ResolveTabPath(string tabPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tabPath) || _config.StashCalibration == null)
|
||||
return (null, null);
|
||||
|
||||
var parts = tabPath.Split('/');
|
||||
if (parts.Length == 1)
|
||||
{
|
||||
// Simple tab name
|
||||
var tab = _config.StashCalibration.Tabs.FirstOrDefault(t => t.Name == parts[0]);
|
||||
return (tab, null);
|
||||
}
|
||||
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
// Folder/SubTab
|
||||
var folder = _config.StashCalibration.Tabs.FirstOrDefault(t => t.Name == parts[0] && t.IsFolder);
|
||||
if (folder == null) return (null, null);
|
||||
var subTab = folder.SubTabs.FirstOrDefault(t => t.Name == parts[1]);
|
||||
return (subTab, folder);
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
private async Task RecoverToHideout()
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Information("Recovering: escaping and going to hideout");
|
||||
await _game.FocusGame();
|
||||
await _game.PressEscape();
|
||||
await Helpers.Sleep(Delays.PostEscape);
|
||||
await _game.PressEscape();
|
||||
await Helpers.Sleep(Delays.PostEscape);
|
||||
|
||||
var arrived = await _inventory.WaitForAreaTransition(
|
||||
_config.TravelTimeoutMs, () => _game.GoToHideout());
|
||||
if (arrived)
|
||||
{
|
||||
_inventory.SetLocation(true);
|
||||
Log.Information("Recovery: arrived at hideout");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning("Recovery: timed out going to hideout");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Recovery failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue