rwork on kulemak bot and cleanup

This commit is contained in:
Boki 2026-02-21 09:18:10 -05:00
parent c75b2b27f0
commit 053a016c8b
15 changed files with 727 additions and 160 deletions

View file

@ -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");
}
}
}