boss getting close

This commit is contained in:
Boki 2026-02-21 20:57:22 -05:00
parent f914443d86
commit aee3a7f22c
19 changed files with 422 additions and 119 deletions

View file

@ -107,6 +107,7 @@ public class BossRunExecutor : GameExecutor
if (_stopped) break;
SetState(BossRunState.Looting);
await Helpers.Sleep(1000); // wait for loot labels to render
await Loot();
if (_stopped) break;
@ -316,7 +317,7 @@ public class BossRunExecutor : GameExecutor
Log.Information("Fight phase starting");
// Wait for arena to settle
await Helpers.Sleep(4500);
await Helpers.Sleep(3000);
if (_stopped) return;
// Find and click the cathedral door
@ -343,7 +344,8 @@ public class BossRunExecutor : GameExecutor
const double wellWorldY = -378;
await WalkToWorldPosition(fightWorldX, fightWorldY, cancelWhen: IsBossAlive);
if (_stopped) return;
_nav.Frozen = true; // Lock canvas — position tracking only
if (_stopped) { _nav.Frozen = false; return; }
// 3x fight-then-well loop
for (var phase = 1; phase <= 3; phase++)
@ -351,7 +353,7 @@ public class BossRunExecutor : GameExecutor
if (_stopped) return;
Log.Information("=== Boss phase {Phase}/4 ===", phase);
var lastBossPos = await AttackBossUntilGone();
var lastBossPos = await AttackBossUntilGone(fightWorldX, fightWorldY);
if (_stopped) return;
// Update fight area to where the boss was last seen
@ -368,7 +370,7 @@ public class BossRunExecutor : GameExecutor
// Walk to well and click the closest match to screen center
Log.Information("Phase {Phase} done, walking to well", phase);
await WalkToWorldPosition(wellWorldX, wellWorldY);
await Helpers.Sleep(1000);
await Helpers.Sleep(500);
await ClickClosestTemplateToCenter(CathedralWellTemplate);
await Helpers.Sleep(200);
@ -379,29 +381,32 @@ public class BossRunExecutor : GameExecutor
// 4th fight - no well after
if (_stopped) return;
Log.Information("=== Boss phase 4/4 ===");
var phase4BossPos = await AttackBossUntilGone();
var finalBossPos = await AttackBossUntilGone(fightWorldX, fightWorldY);
if (_stopped) return;
// Walk toward where the boss died (ring spawns there)
var ringX = phase4BossPos?.X ?? fightWorldX;
var ringY = phase4BossPos?.Y ?? fightWorldY;
Log.Information("Walking to ring area ({X:F0},{Y:F0})", ringX, ringY);
await WalkToWorldPosition(ringX, ringY);
// Update fight area from phase 4 if we got detections
if (finalBossPos != null)
{
fightWorldX = finalBossPos.Value.X;
fightWorldY = finalBossPos.Value.Y;
}
// Walk to known ring position and look for the template
await WalkToWorldPosition(-440, -330);
await Helpers.Sleep(1000);
if (_stopped) return;
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);
await Helpers.Sleep(500);
}
else
{
@ -409,27 +414,20 @@ public class BossRunExecutor : GameExecutor
}
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);
// Walk back to fight area — fightWorldX/Y carries position from all phases
Log.Information("Walking to fight position ({X:F0},{Y:F0})", fightWorldX, fightWorldY);
await WalkToWorldPosition(fightWorldX, fightWorldY);
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);
await Helpers.Sleep(300);
await _game.PressKey(InputSender.VK.E);
await Helpers.Sleep(300);
Log.Information("Attacking at ring fight position");
await AttackBossUntilGone(fightWorldX, fightWorldY);
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);
StopBossDetection();
_nav.Frozen = false;
Log.Information("Fight complete");
}
@ -468,7 +466,8 @@ public class BossRunExecutor : GameExecutor
if (now - _lastDeathCheckMs < 2000) return false;
_lastDeathCheckMs = now;
var match = await _screen.TemplateMatch(ResurrectTemplate);
var deathRegion = new Region(1090, 1030, 370, 60);
var match = await _screen.TemplateMatch(ResurrectTemplate, deathRegion);
if (match == null) return false;
Log.Warning("Death detected! Clicking resurrect at ({X},{Y})", match.X, match.Y);
@ -522,6 +521,27 @@ public class BossRunExecutor : GameExecutor
try { await combatTask; } catch { /* expected */ }
await _combat.ReleaseAll();
cts.Dispose();
await WaitForStablePosition();
}
private async Task WaitForStablePosition(int minConsecutive = 5, int timeoutMs = 2000)
{
var sw = Stopwatch.StartNew();
var consecutive = 0;
while (sw.ElapsedMilliseconds < timeoutMs && consecutive < minConsecutive)
{
await Helpers.Sleep(50);
if (_nav.LastMatchSucceeded)
consecutive++;
else
consecutive = 0;
}
if (consecutive >= minConsecutive)
Log.Information("Position stabilized ({Count} consecutive matches in {Ms}ms)",
consecutive, sw.ElapsedMilliseconds);
else
Log.Warning("Position stabilization timed out ({Count}/{Min} after {Ms}ms)",
consecutive, minConsecutive, sw.ElapsedMilliseconds);
}
private async Task<bool> WaitForBossSpawn(int timeoutMs = 30_000)
@ -554,6 +574,14 @@ public class BossRunExecutor : GameExecutor
}
}
// 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;
}
Log.Warning("WaitForBossSpawn timed out after {Ms}ms", timeoutMs);
return false;
}
@ -567,7 +595,8 @@ public class BossRunExecutor : GameExecutor
/// Wait for boss to spawn, then attack until healthbar disappears.
/// Returns the last world position where YOLO spotted the boss, or null.
/// </summary>
private async Task<(double X, double Y)?> AttackBossUntilGone(int timeoutMs = 120_000)
private async Task<(double X, double Y)?> AttackBossUntilGone(
double fightAreaX = double.NaN, double fightAreaY = double.NaN, int timeoutMs = 120_000)
{
// Wait for boss to actually appear before attacking
if (!await WaitForBossSpawn())
@ -575,6 +604,7 @@ public class BossRunExecutor : GameExecutor
const int screenCx = 1280;
const int screenCy = 720;
const double screenToWorld = 97.0 / 835.0;
(double X, double Y)? lastBossWorldPos = null;
Log.Information("Boss is alive, engaging");
@ -583,6 +613,9 @@ public class BossRunExecutor : GameExecutor
try
{
var sw = Stopwatch.StartNew();
var healthbarMissCount = 0;
const int healthbarMissThreshold = 3; // require 3 consecutive misses
while (sw.ElapsedMilliseconds < timeoutMs)
{
if (_stopped) return lastBossWorldPos;
@ -595,19 +628,55 @@ public class BossRunExecutor : GameExecutor
_combatTargetX = boss.Cx;
_combatTargetY = boss.Cy;
const double screenToWorld = 97.0 / 835.0;
var wp = _nav.WorldPosition;
lastBossWorldPos = (
wp.X + (boss.Cx - screenCx) * screenToWorld,
wp.Y + (boss.Cy - screenCy) * screenToWorld);
// Walk toward boss to stay as close as possible
var bossDx = boss.Cx - screenCx;
var bossDy = boss.Cy - screenCy;
var bossDist = Math.Sqrt(bossDx * bossDx + bossDy * bossDy);
if (bossDist > 50)
{
var dirX = bossDx / bossDist;
var dirY = bossDy / bossDist;
var keys = new List<int>();
if (dirY < -0.3) keys.Add(InputSender.VK.W);
if (dirY > 0.3) keys.Add(InputSender.VK.S);
if (dirX < -0.3) keys.Add(InputSender.VK.A);
if (dirX > 0.3) keys.Add(InputSender.VK.D);
foreach (var k in keys) await _game.KeyDown(k);
await Helpers.Sleep(150);
foreach (var k in keys) await _game.KeyUp(k);
}
}
// Check death + healthbar (combat keeps running in background)
if (await CheckDeath()) continue;
if (await CheckDeath()) { healthbarMissCount = 0; continue; }
if (!await IsBossAlive())
if (await IsBossAlive())
{
Log.Information("Healthbar not found, boss phase over after {Ms}ms", sw.ElapsedMilliseconds);
healthbarMissCount = 0;
}
else
{
healthbarMissCount++;
if (healthbarMissCount < healthbarMissThreshold)
{
Log.Debug("Healthbar miss {N}/{Threshold}", healthbarMissCount, healthbarMissThreshold);
continue;
}
// Confirm: did we die or did boss phase actually end?
_lastDeathCheckMs = 0;
if (await CheckDeath()) { healthbarMissCount = 0; continue; }
Log.Information("Healthbar gone for {N} checks, boss phase over after {Ms}ms",
healthbarMissCount, sw.ElapsedMilliseconds);
return lastBossWorldPos;
}
}
@ -640,6 +709,33 @@ public class BossRunExecutor : GameExecutor
}
}
private async Task AttackUntilBossDead(int x, int y, int timeoutMs)
{
var (combatTask, cts) = StartCombatLoop(x, y, jitter: 20);
try
{
var sw = Stopwatch.StartNew();
// Attack for at least 2s before checking healthbar
await Helpers.Sleep(2000);
while (sw.ElapsedMilliseconds < timeoutMs)
{
if (_stopped) return;
if (await CheckDeath()) continue;
if (!await IsBossAlive())
{
Log.Information("Boss dead, stopping attack after {Ms}ms", sw.ElapsedMilliseconds);
return;
}
await Helpers.Sleep(500);
}
Log.Warning("AttackUntilBossDead timed out after {Ms}ms", timeoutMs);
}
finally
{
await StopCombatLoop(combatTask, cts);
}
}
/// <summary>
/// Find all template matches and click the one closest to screen center.
/// </summary>
@ -648,24 +744,38 @@ public class BossRunExecutor : GameExecutor
const int screenCx = 2560 / 2;
const int screenCy = 1440 / 2;
var matches = await _screen.TemplateMatchAll(templatePath);
if (matches.Count == 0)
// Search center region only to avoid clicking distant matches
var centerRegion = new Region(850, 50, 860, 550);
for (var attempt = 0; attempt < 3; attempt++)
{
Log.Warning("No matches found for {Template}, clicking screen center", Path.GetFileName(templatePath));
await _game.LeftClickAt(screenCx, screenCy);
return;
var matches = await _screen.TemplateMatchAll(templatePath, centerRegion);
if (matches.Count > 0)
{
var closest = matches.OrderBy(m =>
{
var dx = m.X - screenCx;
var dy = m.Y - screenCy;
return dx * dx + dy * dy;
}).First();
Log.Information("Clicking closest match at ({X},{Y}) conf={Conf:F3} (of {Count} matches, attempt {A})",
closest.X, closest.Y, closest.Confidence, matches.Count, attempt + 1);
await _game.LeftClickAt(closest.X, closest.Y);
return;
}
// Nudge character a bit and retry
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;
await _game.KeyDown(nudgeKey);
await Helpers.Sleep(300);
await _game.KeyUp(nudgeKey);
await Helpers.Sleep(500);
}
var closest = matches.OrderBy(m =>
{
var dx = m.X - screenCx;
var dy = m.Y - screenCy;
return dx * dx + dy * dy;
}).First();
Log.Information("Clicking closest match at ({X},{Y}) conf={Conf:F3} (of {Count} matches)",
closest.X, closest.Y, closest.Confidence, matches.Count);
await _game.LeftClickAt(closest.X, closest.Y);
Log.Warning("No matches found for {Template} after nudges, clicking screen center", Path.GetFileName(templatePath));
await _game.LeftClickAt(screenCx, screenCy);
}
/// <summary>
@ -772,17 +882,10 @@ public class BossRunExecutor : GameExecutor
await _game.KeyUp(InputSender.VK.S);
await Helpers.Sleep(200);
// Press + to open portal
// Press + to open portal, then click it at known position
await _game.PressPlus();
await Helpers.Sleep(800);
// 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;
}
await Helpers.Sleep(2500);
await _game.LeftClickAt(1280, 450);
// Wait for area transition to caravan
var arrivedCaravan = await _inventory.WaitForAreaTransition(_config.TravelTimeoutMs);
@ -816,6 +919,9 @@ public class BossRunExecutor : GameExecutor
await _game.FocusGame();
await Helpers.Sleep(Delays.PostFocus);
// Identify items at Doryani before stashing
await _inventory.IdentifyItems();
// Open stash
var stashPos = await _inventory.FindAndClickNameplate("Stash");
if (stashPos == null)
@ -830,18 +936,26 @@ public class BossRunExecutor : GameExecutor
if (lootTab != null)
await _inventory.ClickStashTab(lootTab, lootFolder);
var scanResult = await _screen.Grid.Scan("inventory");
if (scanResult.Occupied.Count > 0)
for (var pass = 0; pass < 3; pass++)
{
Log.Information("Depositing {Count} items to loot tab", scanResult.Occupied.Count);
var scanResult = await _screen.Grid.Scan("inventory");
if (scanResult.Items.Count == 0)
{
if (pass > 0) Log.Information("Inventory clear after {Pass} passes", pass);
break;
}
Log.Information("Depositing {Count} items to loot tab (pass {Pass})", scanResult.Items.Count, pass + 1);
await _game.KeyDown(InputSender.VK.SHIFT);
await _game.HoldCtrl();
foreach (var cell in scanResult.Occupied)
foreach (var item in scanResult.Items)
{
var center = _screen.Grid.GetCellCenter(GridLayouts.Inventory, cell.Row, cell.Col);
var center = _screen.Grid.GetCellCenter(GridLayouts.Inventory, item.Row, item.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);