rename
This commit is contained in:
parent
bef61f841d
commit
c3de5fdb63
107 changed files with 0 additions and 0 deletions
|
|
@ -1,473 +0,0 @@
|
|||
using Poe2Trade.Core;
|
||||
using Poe2Trade.Game;
|
||||
using Poe2Trade.GameLog;
|
||||
using Poe2Trade.Inventory;
|
||||
using Poe2Trade.Navigation;
|
||||
using Poe2Trade.Screen;
|
||||
using Serilog;
|
||||
|
||||
namespace Poe2Trade.Bot;
|
||||
|
||||
/// <summary>
|
||||
/// Kulemak-specific boss run executor: scripted 4-phase + ring fight,
|
||||
/// invitations, cathedral navigation.
|
||||
/// </summary>
|
||||
public class KulemakExecutor : MappingExecutor
|
||||
{
|
||||
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 CathedralWellTemplate = Path.Combine("assets", "black-cathedral-well.png");
|
||||
private static readonly string ReturnTheRingTemplate = Path.Combine("assets", "return-the-ring.png");
|
||||
private static readonly string NewInstanceTemplate = Path.Combine("assets", "new.png");
|
||||
|
||||
public KulemakExecutor(IGameController game, IScreenReader screen,
|
||||
IInventoryManager inventory, IClientLogWatcher logWatcher, SavedSettings config,
|
||||
BossDetector bossDetector, HudReader hudReader, NavigationExecutor nav)
|
||||
: base(game, screen, inventory, logWatcher, config, bossDetector, hudReader, nav)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task RunBossLoop()
|
||||
{
|
||||
ResetStop();
|
||||
var runCount = _config.Kulemak.RunCount;
|
||||
Log.Information("Starting boss run loop ({Count} runs)", runCount);
|
||||
|
||||
var completed = 0;
|
||||
try
|
||||
{
|
||||
if (!await Prepare())
|
||||
{
|
||||
SetState(MappingState.Failed);
|
||||
await RecoverToHideout();
|
||||
SetState(MappingState.Idle);
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < runCount; i++)
|
||||
{
|
||||
if (_stopped) break;
|
||||
|
||||
Log.Information("=== Boss run {N}/{Total} ===", i + 1, runCount);
|
||||
|
||||
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;
|
||||
|
||||
SetState(MappingState.Looting);
|
||||
await Sleep(1000);
|
||||
await Loot();
|
||||
if (_stopped) break;
|
||||
|
||||
if (!await ReturnHome())
|
||||
{
|
||||
Log.Error("Failed to return home");
|
||||
await RecoverToHideout();
|
||||
break;
|
||||
}
|
||||
if (_stopped) break;
|
||||
|
||||
bool isLastRun = i == runCount - 1 || _stopped;
|
||||
await StoreLoot(grabInvitation: !isLastRun);
|
||||
completed++;
|
||||
|
||||
if (_stopped) break;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (_stopped)
|
||||
{
|
||||
Log.Information("Boss run loop cancelled by user");
|
||||
}
|
||||
|
||||
Log.Information("Boss run loop finished: {Completed}/{Total} runs completed", completed, runCount);
|
||||
SetState(MappingState.Complete);
|
||||
await Helpers.Sleep(1000);
|
||||
SetState(MappingState.Idle);
|
||||
}
|
||||
|
||||
private async Task<bool> Prepare()
|
||||
{
|
||||
SetState(MappingState.Preparing);
|
||||
Log.Information("Preparing: depositing inventory and grabbing invitations");
|
||||
|
||||
await _game.FocusGame();
|
||||
await Sleep(Delays.PostFocus);
|
||||
|
||||
var (invTab, invFolder) = ResolveTabPath(_config.Kulemak.InvitationTabPath);
|
||||
|
||||
var stashPos = await _inventory.FindAndClickNameplate("Stash");
|
||||
if (stashPos == null)
|
||||
{
|
||||
Log.Error("Could not find Stash nameplate");
|
||||
return false;
|
||||
}
|
||||
await Sleep(Delays.PostStashOpen);
|
||||
|
||||
var (lootTab, lootFolder) = ResolveTabPath(_config.Kulemak.LootTabPath);
|
||||
if (lootTab == null)
|
||||
{
|
||||
Log.Warning("Loot tab path not configured or not found, skipping deposit");
|
||||
}
|
||||
else
|
||||
{
|
||||
await _inventory.ClickStashTab(lootTab, lootFolder);
|
||||
await _inventory.DepositAllToOpenStash();
|
||||
}
|
||||
|
||||
if (invTab != null)
|
||||
{
|
||||
await _inventory.ClickStashTab(invTab, invFolder);
|
||||
|
||||
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, 1, InvitationTemplate);
|
||||
}
|
||||
|
||||
await _game.PressEscape();
|
||||
_inventory.ResetStashTabState();
|
||||
await Sleep(Delays.PostEscape);
|
||||
|
||||
Log.Information("Preparation complete");
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> TravelToZone()
|
||||
{
|
||||
SetState(MappingState.TravelingToZone);
|
||||
Log.Information("Traveling to Well of Souls via waypoint");
|
||||
|
||||
await _game.FocusGame();
|
||||
await Sleep(Delays.PostFocus);
|
||||
|
||||
var wpPos = await _inventory.FindAndClickNameplate("Waypoint");
|
||||
if (wpPos == null)
|
||||
{
|
||||
Log.Error("Could not find Waypoint nameplate");
|
||||
return false;
|
||||
}
|
||||
await Sleep(1500);
|
||||
|
||||
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);
|
||||
|
||||
var arrived = await _inventory.WaitForAreaTransition(_config.TravelTimeoutMs);
|
||||
if (!arrived)
|
||||
{
|
||||
Log.Error("Timed out waiting for Well of Souls transition");
|
||||
return false;
|
||||
}
|
||||
|
||||
await Sleep(Delays.PostTravel);
|
||||
Log.Information("Arrived at Well of Souls");
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<TemplateMatchResult?> WalkToEntrance()
|
||||
{
|
||||
SetState(MappingState.WalkingToEntrance);
|
||||
Log.Information("Walking to Black Cathedral entrance (W+D)");
|
||||
|
||||
return await WalkAndMatch(BlackCathedralTemplate, InputSender.VK.W, InputSender.VK.D, 10000);
|
||||
}
|
||||
|
||||
private async Task<bool> UseInvitation(int x, int y)
|
||||
{
|
||||
SetState(MappingState.UsingInvitation);
|
||||
Log.Information("Using invitation at ({X},{Y})", x, y);
|
||||
|
||||
await _game.MoveMouseTo(x, y);
|
||||
await Sleep(200);
|
||||
await _game.CtrlLeftClickAt(x, y);
|
||||
await Sleep(500);
|
||||
|
||||
var matches = await _screen.TemplateMatchAll(NewInstanceTemplate);
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
Log.Error("Could not find 'NEW' template for instance selection");
|
||||
return false;
|
||||
}
|
||||
|
||||
var target = matches.OrderBy(m => m.X).First();
|
||||
Log.Information("Found {Count} 'NEW' matches, clicking leftmost at ({X},{Y}) conf={Conf:F3}",
|
||||
matches.Count, target.X, target.Y, target.Confidence);
|
||||
await _game.MoveMouseTo(target.X, target.Y);
|
||||
await Sleep(150);
|
||||
await _game.LeftClickAt(target.X, target.Y);
|
||||
|
||||
var arrived = await _inventory.WaitForAreaTransition(_config.TravelTimeoutMs);
|
||||
if (!arrived)
|
||||
{
|
||||
Log.Error("Timed out waiting for boss arena transition");
|
||||
return false;
|
||||
}
|
||||
|
||||
await Sleep(Delays.PostTravel);
|
||||
Log.Information("Entered boss arena");
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task Fight()
|
||||
{
|
||||
SetState(MappingState.Fighting);
|
||||
Log.Information("Fight phase starting");
|
||||
|
||||
await Sleep(3000);
|
||||
if (_stopped) return;
|
||||
|
||||
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);
|
||||
|
||||
await Sleep(14000);
|
||||
if (_stopped) return;
|
||||
|
||||
StartBossDetection("kulemak");
|
||||
|
||||
var fightWorldX = -454.0;
|
||||
var fightWorldY = -332.0;
|
||||
const double wellWorldX = -496;
|
||||
const double wellWorldY = -378;
|
||||
|
||||
FightPosition = (fightWorldX, fightWorldY);
|
||||
await WalkToWorldPosition(fightWorldX, fightWorldY, cancelWhen: IsBossAlive);
|
||||
_nav.Frozen = true;
|
||||
|
||||
try
|
||||
{
|
||||
if (_stopped) return;
|
||||
|
||||
// 3x fight-then-well loop
|
||||
for (var phase = 1; phase <= 3; phase++)
|
||||
{
|
||||
if (_stopped) return;
|
||||
var preWp = _nav.WorldPosition;
|
||||
Log.Information("=== Boss phase {Phase}/4 === fightArea=({FX:F0},{FY:F0}) charPos=({CX:F1},{CY:F1})",
|
||||
phase, fightWorldX, fightWorldY, preWp.X, preWp.Y);
|
||||
|
||||
var lastBossPos = await AttackBossUntilGone(fightWorldX, fightWorldY);
|
||||
if (_stopped) return;
|
||||
|
||||
var postWp = _nav.WorldPosition;
|
||||
Log.Information("Phase {Phase} ended: charPos=({CX:F1},{CY:F1}) lastBossPos={Boss}",
|
||||
phase, postWp.X, postWp.Y,
|
||||
lastBossPos != null ? $"({lastBossPos.Value.X:F1},{lastBossPos.Value.Y:F1})" : "null");
|
||||
if (lastBossPos != null)
|
||||
{
|
||||
fightWorldX = lastBossPos.Value.X;
|
||||
fightWorldY = lastBossPos.Value.Y;
|
||||
FightPosition = (fightWorldX, fightWorldY);
|
||||
Log.Information("Fight area updated to ({X:F0},{Y:F0})", fightWorldX, fightWorldY);
|
||||
}
|
||||
|
||||
var deathPos = await PollYoloDuringWait(2000);
|
||||
if (deathPos != null)
|
||||
{
|
||||
fightWorldX = deathPos.Value.X;
|
||||
fightWorldY = deathPos.Value.Y;
|
||||
}
|
||||
|
||||
Log.Information("Phase {Phase} done, walking to well", phase);
|
||||
await Sleep(100);
|
||||
await WalkToWorldPosition(wellWorldX, wellWorldY);
|
||||
await Sleep(100);
|
||||
for (var attempt = 0; attempt < 5; attempt++)
|
||||
{
|
||||
if (await TryClickWell()) break;
|
||||
Log.Warning("Well not found (attempt {Attempt}), walking A+W to get closer", attempt + 1);
|
||||
await _game.KeyDown(InputSender.VK.A);
|
||||
if(attempt == 0) await _game.KeyDown(InputSender.VK.W);
|
||||
await Sleep(1000);
|
||||
await _game.KeyUp(InputSender.VK.A);
|
||||
if(attempt == 0) await _game.KeyUp(InputSender.VK.W);
|
||||
await Sleep(100);
|
||||
}
|
||||
await Sleep(1500);
|
||||
|
||||
await WalkToWorldPosition(fightWorldX + 20, fightWorldY +20, cancelWhen: IsBossAlive);
|
||||
}
|
||||
|
||||
// 4th fight - no well after
|
||||
if (_stopped) return;
|
||||
{
|
||||
var p4wp = _nav.WorldPosition;
|
||||
Log.Information("=== Boss phase 4/4 === fightArea=({FX:F0},{FY:F0}) charPos=({CX:F1},{CY:F1})",
|
||||
fightWorldX, fightWorldY, p4wp.X, p4wp.Y);
|
||||
}
|
||||
var finalBossPos = await AttackBossUntilGone(fightWorldX, fightWorldY);
|
||||
if (_stopped) return;
|
||||
|
||||
{
|
||||
var p4postWp = _nav.WorldPosition;
|
||||
Log.Information("Phase 4 ended: charPos=({CX:F1},{CY:F1}) finalBossPos={Boss}",
|
||||
p4postWp.X, p4postWp.Y,
|
||||
finalBossPos != null ? $"({finalBossPos.Value.X:F1},{finalBossPos.Value.Y:F1})" : "null");
|
||||
}
|
||||
if (finalBossPos != null)
|
||||
{
|
||||
fightWorldX = finalBossPos.Value.X;
|
||||
fightWorldY = finalBossPos.Value.Y;
|
||||
FightPosition = (fightWorldX, fightWorldY);
|
||||
}
|
||||
Log.Information("Ring phase: using fightArea=({FX:F0},{FY:F0})", fightWorldX, fightWorldY);
|
||||
|
||||
await WalkToWorldPosition(-450, -340);
|
||||
await Sleep(1000);
|
||||
if (_stopped) return;
|
||||
|
||||
Log.Information("Looking for Return the Ring...");
|
||||
var ring = await _screen.TemplateMatch(ReturnTheRingTemplate);
|
||||
if (ring == null)
|
||||
{
|
||||
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 Sleep(500);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("Could not find Return the Ring template");
|
||||
}
|
||||
if (_stopped) return;
|
||||
|
||||
Log.Information("Walking to fight position ({X:F0},{Y:F0}) + offset", fightWorldX, fightWorldY);
|
||||
await WalkToWorldPosition(fightWorldX , fightWorldY , cancelWhen: IsBossAlive);
|
||||
await Sleep(300);
|
||||
Log.Information("Attacking at ring fight position (no chase — boss will come to us)");
|
||||
await AttackBossUntilGone(fightWorldX, fightWorldY, chase: false);
|
||||
if (_stopped) return;
|
||||
|
||||
StopBossDetection();
|
||||
Log.Information("Fight complete");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_nav.Frozen = false;
|
||||
FightPosition = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> TryClickWell()
|
||||
{
|
||||
const int screenCx = 1280;
|
||||
const int screenCy = 660;
|
||||
var centerRegion = new Region(850, 50, 860, 550);
|
||||
|
||||
var matches = await _screen.TemplateMatchAll(CathedralWellTemplate, centerRegion);
|
||||
if (matches.Count == 0) return false;
|
||||
|
||||
var closest = matches.OrderBy(m =>
|
||||
{
|
||||
var dx = m.X - screenCx;
|
||||
var dy = m.Y - screenCy;
|
||||
return dx * dx + dy * dy;
|
||||
}).First();
|
||||
|
||||
Log.Information("Clicking well at ({X},{Y}) conf={Conf:F3}", closest.X, closest.Y, closest.Confidence);
|
||||
await _game.LeftClickAt(closest.X, closest.Y);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store loot with optional invitation grab for the next run.
|
||||
/// </summary>
|
||||
private async Task StoreLoot(bool grabInvitation)
|
||||
{
|
||||
// Use base StoreLoot for the deposit logic, but we need the stash to stay open
|
||||
// for invitation grab, so we inline the full flow here.
|
||||
SetState(MappingState.StoringLoot);
|
||||
Log.Information("Storing loot");
|
||||
|
||||
await _game.FocusGame();
|
||||
await Sleep(Delays.PostFocus);
|
||||
|
||||
await _inventory.IdentifyItems();
|
||||
|
||||
var stashPos = await _inventory.FindAndClickNameplate("Stash");
|
||||
if (stashPos == null)
|
||||
{
|
||||
Log.Warning("Could not find Stash, skipping loot storage");
|
||||
return;
|
||||
}
|
||||
await Sleep(Delays.PostStashOpen);
|
||||
|
||||
var (lootTab, lootFolder) = ResolveTabPath(_config.Kulemak.LootTabPath);
|
||||
if (lootTab != null)
|
||||
await _inventory.ClickStashTab(lootTab, lootFolder);
|
||||
|
||||
await _inventory.DepositAllToOpenStash();
|
||||
|
||||
// Grab 1 invitation for the next run while stash is still open
|
||||
if (grabInvitation)
|
||||
{
|
||||
var (invTab, invFolder) = ResolveTabPath(_config.Kulemak.InvitationTabPath);
|
||||
if (invTab != null)
|
||||
{
|
||||
await _inventory.ClickStashTab(invTab, invFolder);
|
||||
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, 1, InvitationTemplate);
|
||||
Log.Information("Grabbed 1 invitation for next run");
|
||||
}
|
||||
}
|
||||
|
||||
await _game.PressEscape();
|
||||
_inventory.ResetStashTabState();
|
||||
await Sleep(Delays.PostEscape);
|
||||
|
||||
Log.Information("Loot stored");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue