test
This commit is contained in:
parent
152c74fa15
commit
5e1b5c2a48
10 changed files with 211 additions and 150 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 7.2 MiB After Width: | Height: | Size: 7.3 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.9 MiB After Width: | Height: | Size: 5.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 332 KiB After Width: | Height: | Size: 347 KiB |
|
|
@ -44,6 +44,11 @@ public class BossRunExecutor : GameExecutor
|
||||||
|
|
||||||
public BossRunState State => _state;
|
public BossRunState State => _state;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current fight position in world coordinates, or null if not fighting.
|
||||||
|
/// </summary>
|
||||||
|
public (double X, double Y)? FightPosition { get; private set; }
|
||||||
|
|
||||||
private void SetState(BossRunState s)
|
private void SetState(BossRunState s)
|
||||||
{
|
{
|
||||||
_state = s;
|
_state = s;
|
||||||
|
|
@ -58,77 +63,84 @@ public class BossRunExecutor : GameExecutor
|
||||||
|
|
||||||
public async Task RunBossLoop()
|
public async Task RunBossLoop()
|
||||||
{
|
{
|
||||||
_stopped = false;
|
ResetStop();
|
||||||
var runCount = _config.Kulemak.RunCount;
|
var runCount = _config.Kulemak.RunCount;
|
||||||
Log.Information("Starting boss run loop ({Count} runs)", runCount);
|
Log.Information("Starting boss run loop ({Count} runs)", runCount);
|
||||||
|
|
||||||
// First run: deposit inventory and grab 1 invitation
|
|
||||||
if (!await Prepare())
|
|
||||||
{
|
|
||||||
SetState(BossRunState.Failed);
|
|
||||||
await RecoverToHideout();
|
|
||||||
SetState(BossRunState.Idle);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var completed = 0;
|
var completed = 0;
|
||||||
for (var i = 0; i < runCount; i++)
|
try
|
||||||
{
|
{
|
||||||
if (_stopped) break;
|
// First run: deposit inventory and grab 1 invitation
|
||||||
|
if (!await Prepare())
|
||||||
Log.Information("=== Boss run {N}/{Total} ===", i + 1, runCount);
|
|
||||||
|
|
||||||
if (!await TravelToZone())
|
|
||||||
{
|
{
|
||||||
Log.Error("Failed to travel to zone");
|
SetState(BossRunState.Failed);
|
||||||
await RecoverToHideout();
|
await RecoverToHideout();
|
||||||
break;
|
SetState(BossRunState.Idle);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (_stopped) break;
|
|
||||||
|
|
||||||
var entrance = await WalkToEntrance();
|
for (var i = 0; i < runCount; i++)
|
||||||
if (entrance == null)
|
|
||||||
{
|
{
|
||||||
Log.Error("Failed to find Black Cathedral entrance");
|
if (_stopped) break;
|
||||||
await RecoverToHideout();
|
|
||||||
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(BossRunState.Looting);
|
||||||
|
await Sleep(1000); // wait for loot labels to render
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
if (_stopped) break;
|
}
|
||||||
|
catch (OperationCanceledException) when (_stopped)
|
||||||
if (!await UseInvitation(entrance.X, entrance.Y))
|
{
|
||||||
{
|
Log.Information("Boss run loop cancelled by user");
|
||||||
Log.Error("Failed to use invitation");
|
|
||||||
await RecoverToHideout();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (_stopped) break;
|
|
||||||
|
|
||||||
await Fight();
|
|
||||||
if (_stopped) break;
|
|
||||||
|
|
||||||
SetState(BossRunState.Looting);
|
|
||||||
await Helpers.Sleep(1000); // wait for loot labels to render
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Information("Boss run loop finished: {Completed}/{Total} runs completed", completed, runCount);
|
Log.Information("Boss run loop finished: {Completed}/{Total} runs completed", completed, runCount);
|
||||||
SetState(BossRunState.Complete);
|
SetState(BossRunState.Complete);
|
||||||
await Helpers.Sleep(1000);
|
await Helpers.Sleep(1000); // non-cancellable final delay
|
||||||
SetState(BossRunState.Idle);
|
SetState(BossRunState.Idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,7 +150,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
Log.Information("Preparing: depositing inventory and grabbing invitations");
|
Log.Information("Preparing: depositing inventory and grabbing invitations");
|
||||||
|
|
||||||
await _game.FocusGame();
|
await _game.FocusGame();
|
||||||
await Helpers.Sleep(Delays.PostFocus);
|
await Sleep(Delays.PostFocus);
|
||||||
|
|
||||||
// Open stash
|
// Open stash
|
||||||
var stashPos = await _inventory.FindAndClickNameplate("Stash");
|
var stashPos = await _inventory.FindAndClickNameplate("Stash");
|
||||||
|
|
@ -147,7 +159,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
Log.Error("Could not find Stash nameplate");
|
Log.Error("Could not find Stash nameplate");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await Helpers.Sleep(Delays.PostStashOpen);
|
await Sleep(Delays.PostStashOpen);
|
||||||
|
|
||||||
// Click loot tab and deposit all inventory items
|
// Click loot tab and deposit all inventory items
|
||||||
var (lootTab, lootFolder) = ResolveTabPath(_config.Kulemak.LootTabPath);
|
var (lootTab, lootFolder) = ResolveTabPath(_config.Kulemak.LootTabPath);
|
||||||
|
|
@ -166,11 +178,11 @@ public class BossRunExecutor : GameExecutor
|
||||||
{
|
{
|
||||||
var center = _screen.Grid.GetCellCenter(GridLayouts.Inventory, cell.Row, cell.Col);
|
var center = _screen.Grid.GetCellCenter(GridLayouts.Inventory, cell.Row, cell.Col);
|
||||||
await _game.LeftClickAt(center.X, center.Y);
|
await _game.LeftClickAt(center.X, center.Y);
|
||||||
await Helpers.Sleep(Delays.ClickInterval);
|
await Sleep(Delays.ClickInterval);
|
||||||
}
|
}
|
||||||
await _game.ReleaseCtrl();
|
await _game.ReleaseCtrl();
|
||||||
await _game.KeyUp(InputSender.VK.SHIFT);
|
await _game.KeyUp(InputSender.VK.SHIFT);
|
||||||
await Helpers.Sleep(Delays.PostEscape);
|
await Sleep(Delays.PostEscape);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -203,7 +215,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
// Close stash
|
// Close stash
|
||||||
await _game.PressEscape();
|
await _game.PressEscape();
|
||||||
_inventory.ResetStashTabState();
|
_inventory.ResetStashTabState();
|
||||||
await Helpers.Sleep(Delays.PostEscape);
|
await Sleep(Delays.PostEscape);
|
||||||
|
|
||||||
Log.Information("Preparation complete");
|
Log.Information("Preparation complete");
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -215,7 +227,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
Log.Information("Traveling to Well of Souls via waypoint");
|
Log.Information("Traveling to Well of Souls via waypoint");
|
||||||
|
|
||||||
await _game.FocusGame();
|
await _game.FocusGame();
|
||||||
await Helpers.Sleep(Delays.PostFocus);
|
await Sleep(Delays.PostFocus);
|
||||||
|
|
||||||
// Find and click Waypoint
|
// Find and click Waypoint
|
||||||
var wpPos = await _inventory.FindAndClickNameplate("Waypoint");
|
var wpPos = await _inventory.FindAndClickNameplate("Waypoint");
|
||||||
|
|
@ -224,7 +236,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
Log.Error("Could not find Waypoint nameplate");
|
Log.Error("Could not find Waypoint nameplate");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await Helpers.Sleep(500);
|
await Sleep(500);
|
||||||
|
|
||||||
// Template match well-of-souls.png and click
|
// Template match well-of-souls.png and click
|
||||||
var match = await _screen.TemplateMatch(WellOfSoulsTemplate);
|
var match = await _screen.TemplateMatch(WellOfSoulsTemplate);
|
||||||
|
|
@ -246,7 +258,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Helpers.Sleep(Delays.PostTravel);
|
await Sleep(Delays.PostTravel);
|
||||||
Log.Information("Arrived at Well of Souls");
|
Log.Information("Arrived at Well of Souls");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -266,9 +278,9 @@ public class BossRunExecutor : GameExecutor
|
||||||
|
|
||||||
// Hover first so the game registers the target, then use invitation
|
// Hover first so the game registers the target, then use invitation
|
||||||
await _game.MoveMouseTo(x, y);
|
await _game.MoveMouseTo(x, y);
|
||||||
await Helpers.Sleep(200);
|
await Sleep(200);
|
||||||
await _game.CtrlLeftClickAt(x, y);
|
await _game.CtrlLeftClickAt(x, y);
|
||||||
await Helpers.Sleep(500);
|
await Sleep(500);
|
||||||
|
|
||||||
// Find "NEW" button via template match — pick the leftmost
|
// Find "NEW" button via template match — pick the leftmost
|
||||||
var matches = await _screen.TemplateMatchAll(NewInstanceTemplate);
|
var matches = await _screen.TemplateMatchAll(NewInstanceTemplate);
|
||||||
|
|
@ -282,7 +294,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
Log.Information("Found {Count} 'NEW' matches, clicking leftmost at ({X},{Y}) conf={Conf:F3}",
|
Log.Information("Found {Count} 'NEW' matches, clicking leftmost at ({X},{Y}) conf={Conf:F3}",
|
||||||
matches.Count, target.X, target.Y, target.Confidence);
|
matches.Count, target.X, target.Y, target.Confidence);
|
||||||
await _game.MoveMouseTo(target.X, target.Y);
|
await _game.MoveMouseTo(target.X, target.Y);
|
||||||
await Helpers.Sleep(150);
|
await Sleep(150);
|
||||||
await _game.LeftClickAt(target.X, target.Y);
|
await _game.LeftClickAt(target.X, target.Y);
|
||||||
|
|
||||||
// Wait for area transition into boss arena
|
// Wait for area transition into boss arena
|
||||||
|
|
@ -293,7 +305,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Helpers.Sleep(Delays.PostTravel);
|
await Sleep(Delays.PostTravel);
|
||||||
Log.Information("Entered boss arena");
|
Log.Information("Entered boss arena");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -317,7 +329,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
Log.Information("Fight phase starting");
|
Log.Information("Fight phase starting");
|
||||||
|
|
||||||
// Wait for arena to settle
|
// Wait for arena to settle
|
||||||
await Helpers.Sleep(3000);
|
await Sleep(3000);
|
||||||
if (_stopped) return;
|
if (_stopped) return;
|
||||||
|
|
||||||
// Find and click the cathedral door
|
// Find and click the cathedral door
|
||||||
|
|
@ -332,7 +344,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
await _game.LeftClickAt(door.X, door.Y);
|
await _game.LeftClickAt(door.X, door.Y);
|
||||||
|
|
||||||
// Wait for cathedral interior to load
|
// Wait for cathedral interior to load
|
||||||
await Helpers.Sleep(14000);
|
await Sleep(14000);
|
||||||
if (_stopped) return;
|
if (_stopped) return;
|
||||||
|
|
||||||
StartBossDetection();
|
StartBossDetection();
|
||||||
|
|
@ -343,6 +355,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
const double wellWorldX = -496;
|
const double wellWorldX = -496;
|
||||||
const double wellWorldY = -378;
|
const double wellWorldY = -378;
|
||||||
|
|
||||||
|
FightPosition = (fightWorldX, fightWorldY);
|
||||||
await WalkToWorldPosition(fightWorldX, fightWorldY, cancelWhen: IsBossAlive);
|
await WalkToWorldPosition(fightWorldX, fightWorldY, cancelWhen: IsBossAlive);
|
||||||
_nav.Frozen = true; // Lock canvas — position tracking only
|
_nav.Frozen = true; // Lock canvas — position tracking only
|
||||||
if (_stopped) { _nav.Frozen = false; return; }
|
if (_stopped) { _nav.Frozen = false; return; }
|
||||||
|
|
@ -367,18 +380,19 @@ public class BossRunExecutor : GameExecutor
|
||||||
{
|
{
|
||||||
fightWorldX = lastBossPos.Value.X;
|
fightWorldX = lastBossPos.Value.X;
|
||||||
fightWorldY = lastBossPos.Value.Y;
|
fightWorldY = lastBossPos.Value.Y;
|
||||||
|
FightPosition = (fightWorldX, fightWorldY);
|
||||||
Log.Information("Fight area updated to ({X:F0},{Y:F0})", fightWorldX, fightWorldY);
|
Log.Information("Fight area updated to ({X:F0},{Y:F0})", fightWorldX, fightWorldY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for death animation before looking for well
|
// Wait for death animation before looking for well
|
||||||
await Helpers.Sleep(3000);
|
await Sleep(3000);
|
||||||
|
|
||||||
// Walk to well and click the closest match to screen center
|
// Walk to well and click the closest match to screen center
|
||||||
Log.Information("Phase {Phase} done, walking to well", phase);
|
Log.Information("Phase {Phase} done, walking to well", phase);
|
||||||
await WalkToWorldPosition(wellWorldX, wellWorldY);
|
await WalkToWorldPosition(wellWorldX, wellWorldY);
|
||||||
await Helpers.Sleep(500);
|
await Sleep(500);
|
||||||
await ClickClosestTemplateToCenter(CathedralWellTemplate);
|
await ClickClosestTemplateToCenter(CathedralWellTemplate);
|
||||||
await Helpers.Sleep(200);
|
await Sleep(200);
|
||||||
|
|
||||||
// Walk back to fight position for next phase
|
// Walk back to fight position for next phase
|
||||||
await WalkToWorldPosition(fightWorldX, fightWorldY, cancelWhen: IsBossAlive);
|
await WalkToWorldPosition(fightWorldX, fightWorldY, cancelWhen: IsBossAlive);
|
||||||
|
|
@ -405,12 +419,13 @@ public class BossRunExecutor : GameExecutor
|
||||||
{
|
{
|
||||||
fightWorldX = finalBossPos.Value.X;
|
fightWorldX = finalBossPos.Value.X;
|
||||||
fightWorldY = finalBossPos.Value.Y;
|
fightWorldY = finalBossPos.Value.Y;
|
||||||
|
FightPosition = (fightWorldX, fightWorldY);
|
||||||
}
|
}
|
||||||
Log.Information("Ring phase: using fightArea=({FX:F0},{FY:F0})", fightWorldX, fightWorldY);
|
Log.Information("Ring phase: using fightArea=({FX:F0},{FY:F0})", fightWorldX, fightWorldY);
|
||||||
|
|
||||||
// Walk to known ring position and look for the template
|
// Walk to known ring position and look for the template
|
||||||
await WalkToWorldPosition(-440, -330);
|
await WalkToWorldPosition(-440, -330);
|
||||||
await Helpers.Sleep(1000);
|
await Sleep(1000);
|
||||||
if (_stopped) return;
|
if (_stopped) return;
|
||||||
|
|
||||||
Log.Information("Looking for Return the Ring...");
|
Log.Information("Looking for Return the Ring...");
|
||||||
|
|
@ -423,7 +438,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
{
|
{
|
||||||
Log.Information("Found Return the Ring at ({X},{Y}), clicking", ring.X, ring.Y);
|
Log.Information("Found Return the Ring at ({X},{Y}), clicking", ring.X, ring.Y);
|
||||||
await _game.LeftClickAt(ring.X, ring.Y);
|
await _game.LeftClickAt(ring.X, ring.Y);
|
||||||
await Helpers.Sleep(500);
|
await Sleep(500);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -434,17 +449,14 @@ public class BossRunExecutor : GameExecutor
|
||||||
// Walk back to fight area — fightWorldX/Y carries position from all phases
|
// Walk back to fight area — fightWorldX/Y carries position from all phases
|
||||||
Log.Information("Walking to fight position ({X:F0},{Y:F0})", fightWorldX, fightWorldY);
|
Log.Information("Walking to fight position ({X:F0},{Y:F0})", fightWorldX, fightWorldY);
|
||||||
await WalkToWorldPosition(fightWorldX, fightWorldY);
|
await WalkToWorldPosition(fightWorldX, fightWorldY);
|
||||||
await Helpers.Sleep(300);
|
await Sleep(300);
|
||||||
await _game.PressKey(InputSender.VK.Q);
|
|
||||||
await Helpers.Sleep(300);
|
|
||||||
await _game.PressKey(InputSender.VK.E);
|
|
||||||
await Helpers.Sleep(300);
|
|
||||||
Log.Information("Attacking at ring fight position");
|
Log.Information("Attacking at ring fight position");
|
||||||
await AttackBossUntilGone(fightWorldX, fightWorldY);
|
await AttackBossUntilGone(fightWorldX, fightWorldY);
|
||||||
if (_stopped) return;
|
if (_stopped) return;
|
||||||
|
|
||||||
StopBossDetection();
|
StopBossDetection();
|
||||||
_nav.Frozen = false;
|
_nav.Frozen = false;
|
||||||
|
FightPosition = null;
|
||||||
Log.Information("Fight complete");
|
Log.Information("Fight complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -490,7 +502,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
Log.Warning("Death detected! Clicking resurrect at ({X},{Y})", match.X, match.Y);
|
Log.Warning("Death detected! Clicking resurrect at ({X},{Y})", match.X, match.Y);
|
||||||
await _combat.ReleaseAll();
|
await _combat.ReleaseAll();
|
||||||
await _game.LeftClickAt(match.X, match.Y);
|
await _game.LeftClickAt(match.X, match.Y);
|
||||||
await Helpers.Sleep(3000); // wait for respawn + loading
|
await Sleep(3000); // wait for respawn + loading
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -547,7 +559,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
var consecutive = 0;
|
var consecutive = 0;
|
||||||
while (sw.ElapsedMilliseconds < timeoutMs && consecutive < minConsecutive)
|
while (sw.ElapsedMilliseconds < timeoutMs && consecutive < minConsecutive)
|
||||||
{
|
{
|
||||||
await Helpers.Sleep(50);
|
await Sleep(50);
|
||||||
if (_nav.LastMatchSucceeded)
|
if (_nav.LastMatchSucceeded)
|
||||||
consecutive++;
|
consecutive++;
|
||||||
else
|
else
|
||||||
|
|
@ -566,46 +578,31 @@ public class BossRunExecutor : GameExecutor
|
||||||
Log.Information("Waiting for boss healthbar to appear...");
|
Log.Information("Waiting for boss healthbar to appear...");
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
var (combatTask, cts) = StartCombatLoop();
|
while (sw.ElapsedMilliseconds < timeoutMs)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
while (sw.ElapsedMilliseconds < timeoutMs)
|
if (_stopped) return false;
|
||||||
|
|
||||||
|
if (await CheckDeath()) continue;
|
||||||
|
|
||||||
|
if (await IsBossAlive())
|
||||||
{
|
{
|
||||||
if (_stopped) return false;
|
Log.Information("Boss healthbar detected after {Ms}ms", sw.ElapsedMilliseconds);
|
||||||
|
return true;
|
||||||
// Update target from YOLO (fast, no capture)
|
|
||||||
var snapshot = _bossDetector.Latest;
|
|
||||||
if (snapshot.Bosses.Count > 0)
|
|
||||||
{
|
|
||||||
_combatTargetX = snapshot.Bosses[0].Cx;
|
|
||||||
_combatTargetY = snapshot.Bosses[0].Cy;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check death + healthbar (blocks main thread, combat continues in background)
|
|
||||||
if (await CheckDeath()) continue;
|
|
||||||
|
|
||||||
if (await IsBossAlive())
|
|
||||||
{
|
|
||||||
Log.Information("Boss healthbar detected after {Ms}ms", sw.ElapsedMilliseconds);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final death check before giving up — force check ignoring throttle
|
await Sleep(50);
|
||||||
_lastDeathCheckMs = 0;
|
}
|
||||||
if (await CheckDeath())
|
|
||||||
{
|
|
||||||
Log.Warning("Died while waiting for boss spawn");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Warning("WaitForBossSpawn timed out after {Ms}ms", timeoutMs);
|
// Final death check before giving up — force check ignoring throttle
|
||||||
|
_lastDeathCheckMs = 0;
|
||||||
|
if (await CheckDeath())
|
||||||
|
{
|
||||||
|
Log.Warning("Died while waiting for boss spawn");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
Log.Warning("WaitForBossSpawn timed out after {Ms}ms", timeoutMs);
|
||||||
await StopCombatLoop(combatTask, cts);
|
return false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -688,8 +685,9 @@ public class BossRunExecutor : GameExecutor
|
||||||
_lastDeathCheckMs = 0;
|
_lastDeathCheckMs = 0;
|
||||||
if (await CheckDeath()) { healthbarMissCount = 0; continue; }
|
if (await CheckDeath()) { healthbarMissCount = 0; continue; }
|
||||||
|
|
||||||
Log.Information("Healthbar gone for {N} checks, boss phase over after {Ms}ms",
|
Log.Information("Healthbar gone for {N} checks, boss phase over after {Ms}ms, lastBossPos={Boss}",
|
||||||
healthbarMissCount, sw.ElapsedMilliseconds);
|
healthbarMissCount, sw.ElapsedMilliseconds,
|
||||||
|
lastBossWorldPos != null ? $"({lastBossWorldPos.Value.X:F1},{lastBossWorldPos.Value.Y:F1})" : "null");
|
||||||
return lastBossWorldPos;
|
return lastBossWorldPos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -715,7 +713,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
{
|
{
|
||||||
if (_stopped) return;
|
if (_stopped) return;
|
||||||
if (await CheckDeath()) continue;
|
if (await CheckDeath()) continue;
|
||||||
await Helpers.Sleep(500);
|
await Sleep(500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|
@ -731,7 +729,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
// Attack for at least 2s before checking healthbar
|
// Attack for at least 2s before checking healthbar
|
||||||
await Helpers.Sleep(2000);
|
await Sleep(2000);
|
||||||
while (sw.ElapsedMilliseconds < timeoutMs)
|
while (sw.ElapsedMilliseconds < timeoutMs)
|
||||||
{
|
{
|
||||||
if (_stopped) return;
|
if (_stopped) return;
|
||||||
|
|
@ -741,7 +739,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
Log.Information("Boss dead, stopping attack after {Ms}ms", sw.ElapsedMilliseconds);
|
Log.Information("Boss dead, stopping attack after {Ms}ms", sw.ElapsedMilliseconds);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await Helpers.Sleep(500);
|
await Sleep(500);
|
||||||
}
|
}
|
||||||
Log.Warning("AttackUntilBossDead timed out after {Ms}ms", timeoutMs);
|
Log.Warning("AttackUntilBossDead timed out after {Ms}ms", timeoutMs);
|
||||||
}
|
}
|
||||||
|
|
@ -784,9 +782,9 @@ public class BossRunExecutor : GameExecutor
|
||||||
Log.Warning("No center match for {Template} (attempt {A}/3), nudging", Path.GetFileName(templatePath), attempt + 1);
|
Log.Warning("No center match for {Template} (attempt {A}/3), nudging", Path.GetFileName(templatePath), attempt + 1);
|
||||||
var nudgeKey = attempt % 2 == 0 ? InputSender.VK.W : InputSender.VK.S;
|
var nudgeKey = attempt % 2 == 0 ? InputSender.VK.W : InputSender.VK.S;
|
||||||
await _game.KeyDown(nudgeKey);
|
await _game.KeyDown(nudgeKey);
|
||||||
await Helpers.Sleep(300);
|
await Sleep(300);
|
||||||
await _game.KeyUp(nudgeKey);
|
await _game.KeyUp(nudgeKey);
|
||||||
await Helpers.Sleep(500);
|
await Sleep(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Warning("No matches found for {Template} after nudges, clicking screen center", Path.GetFileName(templatePath));
|
Log.Warning("No matches found for {Template} after nudges, clicking screen center", Path.GetFileName(templatePath));
|
||||||
|
|
@ -836,15 +834,15 @@ public class BossRunExecutor : GameExecutor
|
||||||
var dirX = dx / len;
|
var dirX = dx / len;
|
||||||
var dirY = dy / len;
|
var dirY = dy / len;
|
||||||
|
|
||||||
// Blink toward destination every ~2.3s ± 0.3s
|
// Blink toward destination every ~2.3s ± 0.3s (only when far enough to avoid overshooting)
|
||||||
var blinkCooldown = 2300 + Rng.Next(-300, 301);
|
var blinkCooldown = 2300 + Rng.Next(-300, 301);
|
||||||
if (sw.ElapsedMilliseconds - lastBlinkMs >= blinkCooldown)
|
if (dist > 200 && sw.ElapsedMilliseconds - lastBlinkMs >= blinkCooldown)
|
||||||
{
|
{
|
||||||
// Move mouse in the travel direction so blink goes the right way
|
// Move mouse in the travel direction so blink goes the right way
|
||||||
var blinkX = screenCx + (int)(dirX * 400);
|
var blinkX = screenCx + (int)(dirX * 400);
|
||||||
var blinkY = screenCy + (int)(dirY * 400);
|
var blinkY = screenCy + (int)(dirY * 400);
|
||||||
await _game.MoveMouseFast(blinkX, blinkY);
|
await _game.MoveMouseFast(blinkX, blinkY);
|
||||||
await Helpers.Sleep(30);
|
await Sleep(30);
|
||||||
await _game.PressKey(InputSender.VK.SPACE);
|
await _game.PressKey(InputSender.VK.SPACE);
|
||||||
lastBlinkMs = sw.ElapsedMilliseconds;
|
lastBlinkMs = sw.ElapsedMilliseconds;
|
||||||
}
|
}
|
||||||
|
|
@ -869,7 +867,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
heldKeys.Add(key);
|
heldKeys.Add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Helpers.Sleep(100);
|
await Sleep(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sw.ElapsedMilliseconds >= timeoutMs)
|
if (sw.ElapsedMilliseconds >= timeoutMs)
|
||||||
|
|
@ -889,17 +887,17 @@ public class BossRunExecutor : GameExecutor
|
||||||
Log.Information("Returning home");
|
Log.Information("Returning home");
|
||||||
|
|
||||||
await _game.FocusGame();
|
await _game.FocusGame();
|
||||||
await Helpers.Sleep(Delays.PostFocus);
|
await Sleep(Delays.PostFocus);
|
||||||
|
|
||||||
// Walk away from loot (hold S briefly)
|
// Walk away from loot (hold S briefly)
|
||||||
await _game.KeyDown(InputSender.VK.S);
|
await _game.KeyDown(InputSender.VK.S);
|
||||||
await Helpers.Sleep(500);
|
await Sleep(500);
|
||||||
await _game.KeyUp(InputSender.VK.S);
|
await _game.KeyUp(InputSender.VK.S);
|
||||||
await Helpers.Sleep(200);
|
await Sleep(200);
|
||||||
|
|
||||||
// Press + to open portal, then click it at known position
|
// Press + to open portal, then click it at known position
|
||||||
await _game.PressPlus();
|
await _game.PressPlus();
|
||||||
await Helpers.Sleep(2500);
|
await Sleep(2500);
|
||||||
await _game.LeftClickAt(1280, 450);
|
await _game.LeftClickAt(1280, 450);
|
||||||
|
|
||||||
// Wait for area transition to caravan
|
// Wait for area transition to caravan
|
||||||
|
|
@ -909,7 +907,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
Log.Error("Timed out waiting for caravan transition");
|
Log.Error("Timed out waiting for caravan transition");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await Helpers.Sleep(Delays.PostTravel);
|
await Sleep(Delays.PostTravel);
|
||||||
|
|
||||||
// /hideout to go home
|
// /hideout to go home
|
||||||
var arrivedHome = await _inventory.WaitForAreaTransition(
|
var arrivedHome = await _inventory.WaitForAreaTransition(
|
||||||
|
|
@ -920,7 +918,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Helpers.Sleep(Delays.PostTravel);
|
await Sleep(Delays.PostTravel);
|
||||||
_inventory.SetLocation(true);
|
_inventory.SetLocation(true);
|
||||||
Log.Information("Arrived at hideout");
|
Log.Information("Arrived at hideout");
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -932,7 +930,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
Log.Information("Storing loot");
|
Log.Information("Storing loot");
|
||||||
|
|
||||||
await _game.FocusGame();
|
await _game.FocusGame();
|
||||||
await Helpers.Sleep(Delays.PostFocus);
|
await Sleep(Delays.PostFocus);
|
||||||
|
|
||||||
// Identify items at Doryani before stashing
|
// Identify items at Doryani before stashing
|
||||||
await _inventory.IdentifyItems();
|
await _inventory.IdentifyItems();
|
||||||
|
|
@ -944,7 +942,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
Log.Warning("Could not find Stash, skipping loot storage");
|
Log.Warning("Could not find Stash, skipping loot storage");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await Helpers.Sleep(Delays.PostStashOpen);
|
await Sleep(Delays.PostStashOpen);
|
||||||
|
|
||||||
// Click loot tab and deposit all inventory items
|
// Click loot tab and deposit all inventory items
|
||||||
var (lootTab, lootFolder) = ResolveTabPath(_config.Kulemak.LootTabPath);
|
var (lootTab, lootFolder) = ResolveTabPath(_config.Kulemak.LootTabPath);
|
||||||
|
|
@ -968,12 +966,12 @@ public class BossRunExecutor : GameExecutor
|
||||||
{
|
{
|
||||||
var center = _screen.Grid.GetCellCenter(GridLayouts.Inventory, item.Row, item.Col);
|
var center = _screen.Grid.GetCellCenter(GridLayouts.Inventory, item.Row, item.Col);
|
||||||
await _game.LeftClickAt(center.X, center.Y);
|
await _game.LeftClickAt(center.X, center.Y);
|
||||||
await Helpers.Sleep(Delays.ClickInterval);
|
await Sleep(Delays.ClickInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _game.ReleaseCtrl();
|
await _game.ReleaseCtrl();
|
||||||
await _game.KeyUp(InputSender.VK.SHIFT);
|
await _game.KeyUp(InputSender.VK.SHIFT);
|
||||||
await Helpers.Sleep(Delays.PostEscape);
|
await Sleep(Delays.PostEscape);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab 1 invitation for the next run while stash is still open
|
// Grab 1 invitation for the next run while stash is still open
|
||||||
|
|
@ -998,7 +996,7 @@ public class BossRunExecutor : GameExecutor
|
||||||
// Close stash
|
// Close stash
|
||||||
await _game.PressEscape();
|
await _game.PressEscape();
|
||||||
_inventory.ResetStashTabState();
|
_inventory.ResetStashTabState();
|
||||||
await Helpers.Sleep(Delays.PostEscape);
|
await Sleep(Delays.PostEscape);
|
||||||
|
|
||||||
Log.Information("Loot stored");
|
Log.Information("Loot stored");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ public class CombatManager
|
||||||
private long _lastOrbitMs;
|
private long _lastOrbitMs;
|
||||||
private int _nextOrbitMs = OrbitStepMinMs;
|
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
|
// Chase — walks toward a screen position instead of orbiting
|
||||||
private volatile int _chaseX = -1;
|
private volatile int _chaseX = -1;
|
||||||
private volatile int _chaseY = -1;
|
private volatile int _chaseY = -1;
|
||||||
|
|
@ -69,6 +72,7 @@ public class CombatManager
|
||||||
public async Task Tick(int x, int y, int jitter = 30)
|
public async Task Tick(int x, int y, int jitter = 30)
|
||||||
{
|
{
|
||||||
await _flasks.Tick();
|
await _flasks.Tick();
|
||||||
|
await UseAbilities();
|
||||||
|
|
||||||
if (_chaseX >= 0)
|
if (_chaseX >= 0)
|
||||||
{
|
{
|
||||||
|
|
@ -210,6 +214,35 @@ public class CombatManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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>
|
/// <summary>
|
||||||
/// Reset state for a new combat phase (releases held buttons if any).
|
/// Reset state for a new combat phase (releases held buttons if any).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ public abstract class GameExecutor
|
||||||
protected readonly IInventoryManager _inventory;
|
protected readonly IInventoryManager _inventory;
|
||||||
protected readonly SavedSettings _config;
|
protected readonly SavedSettings _config;
|
||||||
protected volatile bool _stopped;
|
protected volatile bool _stopped;
|
||||||
|
protected CancellationTokenSource _stopCts = new();
|
||||||
|
|
||||||
|
/// <summary>Cancellation token that fires when Stop() is called.</summary>
|
||||||
|
protected CancellationToken StopToken => _stopCts.Token;
|
||||||
|
|
||||||
protected GameExecutor(IGameController game, IScreenReader screen,
|
protected GameExecutor(IGameController game, IScreenReader screen,
|
||||||
IInventoryManager inventory, SavedSettings config)
|
IInventoryManager inventory, SavedSettings config)
|
||||||
|
|
@ -33,8 +37,20 @@ public abstract class GameExecutor
|
||||||
public virtual void Stop()
|
public virtual void Stop()
|
||||||
{
|
{
|
||||||
_stopped = true;
|
_stopped = true;
|
||||||
|
_stopCts.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Reset stopped state for a new run.</summary>
|
||||||
|
protected void ResetStop()
|
||||||
|
{
|
||||||
|
_stopped = false;
|
||||||
|
_stopCts.Dispose();
|
||||||
|
_stopCts = new CancellationTokenSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Cancellable sleep that throws OperationCanceledException when stopped.</summary>
|
||||||
|
protected Task Sleep(int ms) => Helpers.Sleep(ms, _stopCts.Token);
|
||||||
|
|
||||||
// ------ Loot pickup ------
|
// ------ Loot pickup ------
|
||||||
|
|
||||||
// Tiers to skip (noise, low-value, or hidden by filter)
|
// Tiers to skip (noise, low-value, or hidden by filter)
|
||||||
|
|
@ -53,11 +69,11 @@ public abstract class GameExecutor
|
||||||
|
|
||||||
// Move mouse out of the way so it doesn't cover labels
|
// Move mouse out of the way so it doesn't cover labels
|
||||||
_game.MoveMouseInstant(0, 1440);
|
_game.MoveMouseInstant(0, 1440);
|
||||||
await Helpers.Sleep(100);
|
await Sleep(100);
|
||||||
|
|
||||||
// Hold Alt, capture, detect
|
// Hold Alt, capture, detect
|
||||||
await _game.KeyDown(InputSender.VK.MENU);
|
await _game.KeyDown(InputSender.VK.MENU);
|
||||||
await Helpers.Sleep(250);
|
await Sleep(250);
|
||||||
|
|
||||||
using var capture = _screen.CaptureRawBitmap();
|
using var capture = _screen.CaptureRawBitmap();
|
||||||
var labels = _screen.DetectLootLabels(capture, capture);
|
var labels = _screen.DetectLootLabels(capture, capture);
|
||||||
|
|
@ -81,14 +97,14 @@ public abstract class GameExecutor
|
||||||
label.Tier, label.AvgR, label.AvgG, label.AvgB, label.CenterX, label.CenterY);
|
label.Tier, label.AvgR, label.AvgG, label.AvgB, label.CenterX, label.CenterY);
|
||||||
await _game.LeftClickAt(label.CenterX, label.CenterY);
|
await _game.LeftClickAt(label.CenterX, label.CenterY);
|
||||||
totalPicked++;
|
totalPicked++;
|
||||||
await Helpers.Sleep(200);
|
await Sleep(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quick check: capture small region around each clicked label to see if
|
// Quick check: capture small region around each clicked label to see if
|
||||||
// new labels appeared underneath. If none changed, we're done.
|
// new labels appeared underneath. If none changed, we're done.
|
||||||
await Helpers.Sleep(300);
|
await Sleep(300);
|
||||||
_game.MoveMouseInstant(0, 1440);
|
_game.MoveMouseInstant(0, 1440);
|
||||||
await Helpers.Sleep(100);
|
await Sleep(100);
|
||||||
|
|
||||||
using var recheck = _screen.CaptureRawBitmap();
|
using var recheck = _screen.CaptureRawBitmap();
|
||||||
var newLabels = _screen.DetectLootLabels(recheck, recheck);
|
var newLabels = _screen.DetectLootLabels(recheck, recheck);
|
||||||
|
|
@ -103,7 +119,7 @@ public abstract class GameExecutor
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Information("Quick recheck: {Count} new labels appeared, continuing", newPickups.Count);
|
Log.Information("Quick recheck: {Count} new labels appeared, continuing", newPickups.Count);
|
||||||
await Helpers.Sleep(300);
|
await Sleep(300);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Information("Loot pickup complete ({Count} items)", totalPicked);
|
Log.Information("Loot pickup complete ({Count} items)", totalPicked);
|
||||||
|
|
@ -118,9 +134,9 @@ public abstract class GameExecutor
|
||||||
Log.Information("Recovering: escaping and going to hideout");
|
Log.Information("Recovering: escaping and going to hideout");
|
||||||
await _game.FocusGame();
|
await _game.FocusGame();
|
||||||
await _game.PressEscape();
|
await _game.PressEscape();
|
||||||
await Helpers.Sleep(Delays.PostEscape);
|
await Sleep(Delays.PostEscape);
|
||||||
await _game.PressEscape();
|
await _game.PressEscape();
|
||||||
await Helpers.Sleep(Delays.PostEscape);
|
await Sleep(Delays.PostEscape);
|
||||||
|
|
||||||
var arrived = await _inventory.WaitForAreaTransition(
|
var arrived = await _inventory.WaitForAreaTransition(
|
||||||
_config.TravelTimeoutMs, () => _game.GoToHideout());
|
_config.TravelTimeoutMs, () => _game.GoToHideout());
|
||||||
|
|
@ -160,7 +176,7 @@ public abstract class GameExecutor
|
||||||
var match = await _screen.TemplateMatch(templatePath);
|
var match = await _screen.TemplateMatch(templatePath);
|
||||||
if (match == null)
|
if (match == null)
|
||||||
{
|
{
|
||||||
await Helpers.Sleep(500);
|
await Sleep(500);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,7 +198,7 @@ public abstract class GameExecutor
|
||||||
// Stop, settle, re-match for accurate position
|
// Stop, settle, re-match for accurate position
|
||||||
await _game.KeyUp(vk2);
|
await _game.KeyUp(vk2);
|
||||||
await _game.KeyUp(vk1);
|
await _game.KeyUp(vk1);
|
||||||
await Helpers.Sleep(300);
|
await Sleep(300);
|
||||||
|
|
||||||
var fresh = await _screen.TemplateMatch(templatePath);
|
var fresh = await _screen.TemplateMatch(templatePath);
|
||||||
if (fresh != null)
|
if (fresh != null)
|
||||||
|
|
@ -194,7 +210,7 @@ public abstract class GameExecutor
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Helpers.Sleep(200);
|
await Sleep(200);
|
||||||
}
|
}
|
||||||
Log.Error("WalkAndMatch timed out after {Ms}ms (spotted={Spotted})", timeoutMs, spotted);
|
Log.Error("WalkAndMatch timed out after {Ms}ms (spotted={Spotted})", timeoutMs, spotted);
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ public static class Helpers
|
||||||
{
|
{
|
||||||
private static readonly Random Rng = new();
|
private static readonly Random Rng = new();
|
||||||
|
|
||||||
public static Task Sleep(int ms)
|
public static Task Sleep(int ms, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var variance = Math.Max(1, ms / 10); // ±10%
|
var variance = Math.Max(1, ms / 10); // ±10%
|
||||||
var actual = ms + Rng.Next(-variance, variance + 1);
|
var actual = ms + Rng.Next(-variance, variance + 1);
|
||||||
return Task.Delay(Math.Max(1, actual));
|
return Task.Delay(Math.Max(1, actual), ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task RandomDelay(int minMs, int maxMs)
|
public static Task RandomDelay(int minMs, int maxMs)
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,7 @@ public sealed class D2dOverlay
|
||||||
ShowHudDebug: _bot.Store.Settings.ShowHudDebug,
|
ShowHudDebug: _bot.Store.Settings.ShowHudDebug,
|
||||||
ShowLootDebug: showLoot,
|
ShowLootDebug: showLoot,
|
||||||
LootLabels: _bot.LootDebugDetector.Latest,
|
LootLabels: _bot.LootDebugDetector.Latest,
|
||||||
|
FightPosition: _bot.BossRunExecutor.FightPosition,
|
||||||
Fps: fps,
|
Fps: fps,
|
||||||
Timing: timing);
|
Timing: timing);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ public record OverlayState(
|
||||||
bool ShowHudDebug,
|
bool ShowHudDebug,
|
||||||
bool ShowLootDebug,
|
bool ShowLootDebug,
|
||||||
IReadOnlyList<LootLabel> LootLabels,
|
IReadOnlyList<LootLabel> LootLabels,
|
||||||
|
(double X, double Y)? FightPosition,
|
||||||
double Fps,
|
double Fps,
|
||||||
RenderTiming? Timing);
|
RenderTiming? Timing);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,18 @@ internal sealed class D2dEnemyBoxLayer : ID2dOverlayLayer, IDisposable
|
||||||
|
|
||||||
rt.DrawTextLayout(new System.Numerics.Vector2(labelX, labelY), layout, ctx.Cyan);
|
rt.DrawTextLayout(new System.Numerics.Vector2(labelX, labelY), layout, ctx.Cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fight position — red circle on screen where the fight area is
|
||||||
|
if (state.FightPosition is var (fx, fy))
|
||||||
|
{
|
||||||
|
const double worldToScreen = 835.0 / 97.0; // inverse of screenToWorld
|
||||||
|
const int screenCx = 1280, screenCy = 720;
|
||||||
|
var wp = state.NavPosition;
|
||||||
|
var sx = (float)(screenCx + (fx - wp.X) * worldToScreen);
|
||||||
|
var sy = (float)(screenCy + (fy - wp.Y) * worldToScreen);
|
||||||
|
var ellipse = new Ellipse(new System.Numerics.Vector2(sx, sy), 40f, 40f);
|
||||||
|
rt.DrawEllipse(ellipse, ctx.Red, 3f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue