work on well of souls and yolo detection

This commit is contained in:
Boki 2026-02-20 16:40:50 -05:00
parent 3456e0d62a
commit 40d30115bf
41 changed files with 3031 additions and 148 deletions

View file

@ -0,0 +1,529 @@
using System.Diagnostics;
using Poe2Trade.Core;
using Poe2Trade.Game;
using Poe2Trade.GameLog;
using Poe2Trade.Inventory;
using Poe2Trade.Screen;
using Serilog;
namespace Poe2Trade.Bot;
public class BossRunExecutor
{
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 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;
public event Action<BossRunState>? StateChanged;
public BossRunExecutor(IGameController game, IScreenReader screen,
IInventoryManager inventory, IClientLogWatcher logWatcher, SavedSettings config,
BossDetector bossDetector)
{
_game = game;
_screen = screen;
_inventory = inventory;
_logWatcher = logWatcher;
_config = config;
_bossDetector = bossDetector;
}
public BossRunState State => _state;
private void SetState(BossRunState s)
{
_state = s;
StateChanged?.Invoke(s);
}
public void Stop()
{
_stopped = true;
Log.Information("Boss run executor stop requested");
}
public async Task RunBossLoop()
{
_stopped = false;
_bossDetector.SetBoss("kulemak");
Log.Information("Starting boss run loop ({Count} invitations)", _config.Kulemak.InvitationCount);
if (!await Prepare())
{
SetState(BossRunState.Failed);
await RecoverToHideout();
SetState(BossRunState.Idle);
return;
}
var completed = 0;
for (var i = 0; i < _config.Kulemak.InvitationCount; i++)
{
if (_stopped) break;
Log.Information("=== Boss run {N}/{Total} ===", i + 1, _config.Kulemak.InvitationCount);
if (!await TravelToZone())
{
Log.Error("Failed to travel to zone");
await RecoverToHideout();
break;
}
if (_stopped) break;
var entrance = await WalkToEntrance();
if (entrance == null)
{
Log.Error("Failed to find Black Cathedral entrance");
await RecoverToHideout();
break;
}
if (_stopped) break;
if (!await UseInvitation(entrance.X, entrance.Y))
{
Log.Error("Failed to use invitation");
await RecoverToHideout();
break;
}
if (_stopped) break;
await Fight();
if (_stopped) break;
await Loot();
if (_stopped) break;
if (!await ReturnHome())
{
Log.Error("Failed to return home");
await RecoverToHideout();
break;
}
if (_stopped) break;
await StoreLoot();
completed++;
if (_stopped) break;
}
Log.Information("Boss run loop finished: {Completed}/{Total} runs completed", completed, _config.Kulemak.InvitationCount);
SetState(BossRunState.Complete);
await Helpers.Sleep(1000);
SetState(BossRunState.Idle);
}
private async Task<bool> Prepare()
{
SetState(BossRunState.Preparing);
Log.Information("Preparing: depositing inventory and grabbing invitations");
await _game.FocusGame();
await Helpers.Sleep(Delays.PostFocus);
// Open stash
var stashPos = await _inventory.FindAndClickNameplate("Stash");
if (stashPos == null)
{
Log.Error("Could not find Stash nameplate");
return false;
}
await Helpers.Sleep(Delays.PostStashOpen);
// Click loot tab and deposit all inventory items
var (lootTab, lootFolder) = ResolveTabPath(_config.Kulemak.LootTabPath);
if (lootTab != null)
{
await _inventory.ClickStashTab(lootTab, lootFolder);
// Deposit all inventory items via ctrl+click
var scanResult = await _screen.Grid.Scan("inventory");
if (scanResult.Occupied.Count > 0)
{
Log.Information("Depositing {Count} inventory items to loot tab", scanResult.Occupied.Count);
await _game.KeyDown(InputSender.VK.SHIFT);
await _game.HoldCtrl();
foreach (var cell in scanResult.Occupied)
{
var center = _screen.Grid.GetCellCenter(GridLayouts.Inventory, cell.Row, cell.Col);
await _game.LeftClickAt(center.X, center.Y);
await Helpers.Sleep(Delays.ClickInterval);
}
await _game.ReleaseCtrl();
await _game.KeyUp(InputSender.VK.SHIFT);
await Helpers.Sleep(Delays.PostEscape);
}
}
else
{
Log.Warning("Loot tab path not configured or not found, skipping deposit");
}
// Click invitation tab and grab invitations
var (invTab, invFolder) = ResolveTabPath(_config.Kulemak.InvitationTabPath);
if (invTab != null)
{
await _inventory.ClickStashTab(invTab, invFolder);
// Determine layout name based on tab config
var layoutName = (invTab.GridCols == 24, invFolder != null) switch
{
(true, true) => "stash24_folder",
(true, false) => "stash24",
(false, true) => "stash12_folder",
(false, false) => "stash12",
};
await _inventory.GrabItemsFromStash(layoutName, _config.Kulemak.InvitationCount, InvitationTemplate);
}
else
{
Log.Warning("Invitation tab path not configured or not found, skipping grab");
}
// Close stash
await _game.PressEscape();
await Helpers.Sleep(Delays.PostEscape);
Log.Information("Preparation complete");
return true;
}
private async Task<bool> TravelToZone()
{
SetState(BossRunState.TravelingToZone);
Log.Information("Traveling to Well of Souls via waypoint");
await _game.FocusGame();
await Helpers.Sleep(Delays.PostFocus);
// Find and click Waypoint
var wpPos = await _inventory.FindAndClickNameplate("Waypoint");
if (wpPos == null)
{
Log.Error("Could not find Waypoint nameplate");
return false;
}
await Helpers.Sleep(1000);
// Template match well-of-souls.png and click
var match = await _screen.TemplateMatch(WellOfSoulsTemplate);
if (match == null)
{
Log.Error("Could not find Well of Souls on waypoint map");
await _game.PressEscape();
return false;
}
Log.Information("Found Well of Souls at ({X},{Y}), clicking", match.X, match.Y);
await _game.LeftClickAt(match.X, match.Y);
// Wait for area transition
var arrived = await _inventory.WaitForAreaTransition(_config.TravelTimeoutMs);
if (!arrived)
{
Log.Error("Timed out waiting for Well of Souls transition");
return false;
}
await Helpers.Sleep(Delays.PostTravel);
Log.Information("Arrived at Well of Souls");
return true;
}
private async Task<TemplateMatchResult?> WalkToEntrance()
{
SetState(BossRunState.WalkingToEntrance);
Log.Information("Walking to Black Cathedral entrance (W+D)");
return await WalkAndMatch(BlackCathedralTemplate, InputSender.VK.W, InputSender.VK.D, 15000);
}
private async Task<bool> UseInvitation(int x, int y)
{
SetState(BossRunState.UsingInvitation);
Log.Information("Using invitation at ({X},{Y})", x, y);
// Hover first so the game registers the target, then use invitation
await _game.MoveMouseTo(x, y);
await Helpers.Sleep(500);
await _game.CtrlLeftClickAt(x, y);
await Helpers.Sleep(1000);
// Find "NEW" text — pick the leftmost instance
var ocr = await _screen.Ocr();
var newWords = ocr.Lines
.SelectMany(l => l.Words)
.Where(w => w.Text.Equals("NEW", StringComparison.OrdinalIgnoreCase)
|| w.Text.Equals("New", StringComparison.Ordinal))
.OrderBy(w => w.X)
.ToList();
if (newWords.Count == 0)
{
Log.Error("Could not find 'NEW' text for instance selection");
return false;
}
var target = newWords[0];
var clickX = target.X + target.Width / 2;
var clickY = target.Y + target.Height / 2;
Log.Information("Found {Count} 'NEW' matches, clicking leftmost at ({X},{Y})", newWords.Count, clickX, clickY);
await _game.MoveMouseTo(clickX, clickY);
await Helpers.Sleep(500);
await _game.LeftClickAt(clickX, clickY);
// Wait for area transition into boss arena
var arrived = await _inventory.WaitForAreaTransition(_config.TravelTimeoutMs);
if (!arrived)
{
Log.Error("Timed out waiting for boss arena transition");
return false;
}
await Helpers.Sleep(Delays.PostTravel);
Log.Information("Entered boss arena");
return true;
}
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);
}
private async Task Loot()
{
SetState(BossRunState.Looting);
Log.Information("[PLACEHOLDER] Loot phase - waiting for manual looting");
// Placeholder: user handles looting manually for now
await Helpers.Sleep(1000);
}
private async Task<bool> ReturnHome()
{
SetState(BossRunState.Returning);
Log.Information("Returning home");
await _game.FocusGame();
await Helpers.Sleep(Delays.PostFocus);
// Walk away from loot (hold S briefly)
await _game.KeyDown(InputSender.VK.S);
await Helpers.Sleep(1000);
await _game.KeyUp(InputSender.VK.S);
await Helpers.Sleep(300);
// Press + to open portal
await _game.PressPlus();
await Helpers.Sleep(1500);
// Find "The Ardura Caravan" and click it
var caravanPos = await _inventory.FindAndClickNameplate("The Ardura Caravan", maxRetries: 5, retryDelayMs: 1500);
if (caravanPos == null)
{
Log.Error("Could not find 'The Ardura Caravan' portal");
return false;
}
// Wait for area transition to caravan
var arrivedCaravan = await _inventory.WaitForAreaTransition(_config.TravelTimeoutMs);
if (!arrivedCaravan)
{
Log.Error("Timed out waiting for caravan transition");
return false;
}
await Helpers.Sleep(Delays.PostTravel);
// /hideout to go home
var arrivedHome = await _inventory.WaitForAreaTransition(
_config.TravelTimeoutMs, () => _game.GoToHideout());
if (!arrivedHome)
{
Log.Error("Timed out going to hideout");
return false;
}
await Helpers.Sleep(Delays.PostTravel);
_inventory.SetLocation(true);
Log.Information("Arrived at hideout");
return true;
}
private async Task StoreLoot()
{
SetState(BossRunState.StoringLoot);
Log.Information("Storing loot");
await _game.FocusGame();
await Helpers.Sleep(Delays.PostFocus);
// Open stash
var stashPos = await _inventory.FindAndClickNameplate("Stash");
if (stashPos == null)
{
Log.Warning("Could not find Stash, skipping loot storage");
return;
}
await Helpers.Sleep(Delays.PostStashOpen);
// Click loot tab
var (lootTab, lootFolder) = ResolveTabPath(_config.Kulemak.LootTabPath);
if (lootTab != null)
await _inventory.ClickStashTab(lootTab, lootFolder);
// Deposit all inventory items
var scanResult = await _screen.Grid.Scan("inventory");
if (scanResult.Occupied.Count > 0)
{
Log.Information("Depositing {Count} items to loot tab", scanResult.Occupied.Count);
await _game.KeyDown(InputSender.VK.SHIFT);
await _game.HoldCtrl();
foreach (var cell in scanResult.Occupied)
{
var center = _screen.Grid.GetCellCenter(GridLayouts.Inventory, cell.Row, cell.Col);
await _game.LeftClickAt(center.X, center.Y);
await Helpers.Sleep(Delays.ClickInterval);
}
await _game.ReleaseCtrl();
await _game.KeyUp(InputSender.VK.SHIFT);
await Helpers.Sleep(Delays.PostEscape);
}
// Close stash
await _game.PressEscape();
await Helpers.Sleep(Delays.PostEscape);
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");
}
}
}