work on first boss
This commit is contained in:
parent
053a016c8b
commit
89c3a0a077
16 changed files with 702 additions and 150 deletions
|
|
@ -15,7 +15,10 @@ public class BossRunExecutor : GameExecutor
|
|||
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 BossHealthbarTemplate = Path.Combine("assets", "unique-boss-present.png");
|
||||
private static readonly string NewInstanceTemplate = Path.Combine("assets", "new.png");
|
||||
|
||||
private BossRunState _state = BossRunState.Idle;
|
||||
private readonly IClientLogWatcher _logWatcher;
|
||||
|
|
@ -53,7 +56,6 @@ public class BossRunExecutor : GameExecutor
|
|||
public async Task RunBossLoop()
|
||||
{
|
||||
_stopped = false;
|
||||
_bossDetector.SetBoss("kulemak");
|
||||
Log.Information("Starting boss run loop ({Count} invitations)", _config.Kulemak.InvitationCount);
|
||||
|
||||
if (!await Prepare())
|
||||
|
|
@ -193,6 +195,7 @@ public class BossRunExecutor : GameExecutor
|
|||
|
||||
// Close stash
|
||||
await _game.PressEscape();
|
||||
_inventory.ResetStashTabState();
|
||||
await Helpers.Sleep(Delays.PostEscape);
|
||||
|
||||
Log.Information("Preparation complete");
|
||||
|
|
@ -214,7 +217,7 @@ public class BossRunExecutor : GameExecutor
|
|||
Log.Error("Could not find Waypoint nameplate");
|
||||
return false;
|
||||
}
|
||||
await Helpers.Sleep(1000);
|
||||
await Helpers.Sleep(500);
|
||||
|
||||
// Template match well-of-souls.png and click
|
||||
var match = await _screen.TemplateMatch(WellOfSoulsTemplate);
|
||||
|
|
@ -256,32 +259,24 @@ public class BossRunExecutor : GameExecutor
|
|||
|
||||
// Hover first so the game registers the target, then use invitation
|
||||
await _game.MoveMouseTo(x, y);
|
||||
await Helpers.Sleep(500);
|
||||
await Helpers.Sleep(200);
|
||||
await _game.CtrlLeftClickAt(x, y);
|
||||
await Helpers.Sleep(1000);
|
||||
await Helpers.Sleep(500);
|
||||
|
||||
// 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)
|
||||
// Find "NEW" button via template match — pick the leftmost
|
||||
var matches = await _screen.TemplateMatchAll(NewInstanceTemplate);
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
Log.Error("Could not find 'NEW' text for instance selection");
|
||||
Log.Error("Could not find 'NEW' template 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);
|
||||
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 Helpers.Sleep(150);
|
||||
await _game.LeftClickAt(target.X, target.Y);
|
||||
|
||||
// Wait for area transition into boss arena
|
||||
var arrived = await _inventory.WaitForAreaTransition(_config.TravelTimeoutMs);
|
||||
|
|
@ -296,13 +291,26 @@ public class BossRunExecutor : GameExecutor
|
|||
return true;
|
||||
}
|
||||
|
||||
private void StartBossDetection()
|
||||
{
|
||||
_bossDetector.SetBoss("kulemak");
|
||||
_bossDetector.Enabled = true;
|
||||
Log.Information("Boss detection started");
|
||||
}
|
||||
|
||||
private void StopBossDetection()
|
||||
{
|
||||
_bossDetector.Enabled = false;
|
||||
Log.Information("Boss detection stopped");
|
||||
}
|
||||
|
||||
private async Task Fight()
|
||||
{
|
||||
SetState(BossRunState.Fighting);
|
||||
Log.Information("Fight phase starting");
|
||||
|
||||
// Wait for arena to settle
|
||||
await Helpers.Sleep(6000);
|
||||
await Helpers.Sleep(5500);
|
||||
if (_stopped) return;
|
||||
|
||||
// Find and click the cathedral door
|
||||
|
|
@ -317,12 +325,14 @@ public class BossRunExecutor : GameExecutor
|
|||
await _game.LeftClickAt(door.X, door.Y);
|
||||
|
||||
// Wait for cathedral interior to load
|
||||
await Helpers.Sleep(12000);
|
||||
await Helpers.Sleep(14000);
|
||||
if (_stopped) return;
|
||||
|
||||
StartBossDetection();
|
||||
|
||||
// Walk to fight area (world coords)
|
||||
const double fightWorldX = -454;
|
||||
const double fightWorldY = -332;
|
||||
var fightWorldX = -454.0;
|
||||
var fightWorldY = -332.0;
|
||||
const double wellWorldX = -496;
|
||||
const double wellWorldY = -378;
|
||||
|
||||
|
|
@ -335,14 +345,21 @@ public class BossRunExecutor : GameExecutor
|
|||
if (_stopped) return;
|
||||
Log.Information("=== Boss phase {Phase}/4 ===", phase);
|
||||
|
||||
await AttackBossUntilGone();
|
||||
var lastBossPos = await AttackBossUntilGone();
|
||||
if (_stopped) return;
|
||||
|
||||
// Walk to well and click it
|
||||
// Update fight area to where the boss was last seen
|
||||
if (lastBossPos != null)
|
||||
{
|
||||
fightWorldX = lastBossPos.Value.X;
|
||||
fightWorldY = lastBossPos.Value.Y;
|
||||
Log.Information("Fight area updated to ({X:F0},{Y:F0})", fightWorldX, fightWorldY);
|
||||
}
|
||||
|
||||
// Walk to well and click the closest match to screen center
|
||||
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 ClickClosestTemplateToCenter(CathedralWellTemplate);
|
||||
await Helpers.Sleep(2000);
|
||||
|
||||
// Walk back to fight position for next phase
|
||||
|
|
@ -396,70 +413,208 @@ public class BossRunExecutor : GameExecutor
|
|||
await Helpers.Sleep(500);
|
||||
await AttackAtPosition(1280, 720, 7000);
|
||||
|
||||
StopBossDetection();
|
||||
Log.Information("Fight complete");
|
||||
}
|
||||
|
||||
private async Task AttackBossUntilGone(int timeoutMs = 120_000)
|
||||
/// <summary>
|
||||
/// Check top-of-screen region for the unique boss healthbar frame.
|
||||
/// Uses lower threshold (0.5) to tolerate partial overlay from YOLO bounding boxes.
|
||||
/// </summary>
|
||||
private async Task<bool> IsBossAlive()
|
||||
{
|
||||
// Move mouse to screen center initially
|
||||
await _game.MoveMouseFast(1280, 720);
|
||||
var topRegion = new Region(800, 0, 960, 120);
|
||||
var matches = await _screen.TemplateMatchAll(BossHealthbarTemplate, topRegion, threshold: 0.5);
|
||||
return matches.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for the boss healthbar to appear (boss spawns/becomes active).
|
||||
/// </summary>
|
||||
private async Task<bool> WaitForBossSpawn(int timeoutMs = 30_000)
|
||||
{
|
||||
Log.Information("Waiting for boss healthbar to appear...");
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
const int screenCx = 1280;
|
||||
const int screenCy = 720;
|
||||
|
||||
while (sw.ElapsedMilliseconds < timeoutMs)
|
||||
{
|
||||
if (_stopped) return false;
|
||||
|
||||
if (await IsBossAlive())
|
||||
{
|
||||
Log.Information("Boss healthbar detected after {Ms}ms", sw.ElapsedMilliseconds);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Attack at center while waiting — boss often spawns on top of us
|
||||
var targetX = screenCx + Rng.Next(-30, 31);
|
||||
var targetY = screenCy + Rng.Next(-30, 31);
|
||||
await _game.MoveMouseFast(targetX, targetY);
|
||||
|
||||
_game.LeftMouseDown();
|
||||
await Helpers.Sleep(Rng.Next(20, 35));
|
||||
_game.LeftMouseUp();
|
||||
_game.RightMouseDown();
|
||||
await Helpers.Sleep(Rng.Next(20, 35));
|
||||
_game.RightMouseUp();
|
||||
|
||||
await Helpers.Sleep(Rng.Next(50, 80));
|
||||
}
|
||||
|
||||
Log.Warning("WaitForBossSpawn timed out after {Ms}ms", timeoutMs);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
{
|
||||
// Wait for boss to actually appear before attacking
|
||||
if (!await WaitForBossSpawn())
|
||||
return null;
|
||||
|
||||
const int screenCx = 1280;
|
||||
const int screenCy = 720;
|
||||
|
||||
// Attack target — defaults to screen center, updated by YOLO
|
||||
var atkX = screenCx;
|
||||
var atkY = screenCy;
|
||||
(double X, double Y)? lastBossWorldPos = null;
|
||||
|
||||
await _game.MoveMouseFast(atkX, atkY);
|
||||
await Helpers.Sleep(200);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var consecutiveMisses = 0;
|
||||
var lastCheckMs = 0L;
|
||||
var holding = false;
|
||||
var manaStableCount = 0;
|
||||
|
||||
while (sw.ElapsedMilliseconds < timeoutMs)
|
||||
Log.Information("Boss is alive, engaging");
|
||||
|
||||
try
|
||||
{
|
||||
if (_stopped) return;
|
||||
|
||||
var snapshot = _bossDetector.Latest;
|
||||
if (snapshot.Bosses.Count > 0)
|
||||
while (sw.ElapsedMilliseconds < timeoutMs)
|
||||
{
|
||||
consecutiveMisses = 0;
|
||||
var boss = snapshot.Bosses[0];
|
||||
if (_stopped) return lastBossWorldPos;
|
||||
|
||||
// Check mana before attacking
|
||||
var hud = _hudReader.Current;
|
||||
if (hud.ManaPct < 0.80f)
|
||||
// Update attack target from YOLO when available
|
||||
var snapshot = _bossDetector.Latest;
|
||||
if (snapshot.Bosses.Count > 0)
|
||||
{
|
||||
await Helpers.Sleep(200);
|
||||
continue;
|
||||
var boss = snapshot.Bosses[0];
|
||||
atkX = boss.Cx;
|
||||
atkY = boss.Cy;
|
||||
|
||||
// Convert boss screen offset to world position
|
||||
// Scale: sqrt(72²+65²)/835 ≈ 0.116 minimap px per screen px
|
||||
const double screenToWorld = 97.0 / 835.0;
|
||||
var wp = _nav.WorldPosition;
|
||||
var bossWorldX = wp.X + (boss.Cx - screenCx) * screenToWorld;
|
||||
var bossWorldY = wp.Y + (boss.Cy - screenCy) * screenToWorld;
|
||||
lastBossWorldPos = (bossWorldX, bossWorldY);
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Re-check healthbar every ~1.5s, need 5 consecutive misses (~7.5s) to confirm phase end
|
||||
if (sw.ElapsedMilliseconds - lastCheckMs > 1500)
|
||||
{
|
||||
var bossAlive = await IsBossAlive();
|
||||
lastCheckMs = sw.ElapsedMilliseconds;
|
||||
|
||||
_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();
|
||||
if (!bossAlive)
|
||||
{
|
||||
consecutiveMisses++;
|
||||
Log.Information("Healthbar not found ({Misses}/5 consecutive misses, {Ms}ms elapsed)",
|
||||
consecutiveMisses, sw.ElapsedMilliseconds);
|
||||
if (consecutiveMisses >= 5)
|
||||
{
|
||||
Log.Information("Boss phase over after {Ms}ms", sw.ElapsedMilliseconds);
|
||||
return lastBossWorldPos;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (consecutiveMisses > 0)
|
||||
Log.Information("Healthbar found again, resetting miss count (was {Misses})", consecutiveMisses);
|
||||
consecutiveMisses = 0;
|
||||
}
|
||||
}
|
||||
|
||||
await Helpers.Sleep(Rng.Next(100, 150));
|
||||
// Check mana to decide hold vs click
|
||||
var mana = _hudReader.Current.ManaPct;
|
||||
|
||||
if (!holding)
|
||||
{
|
||||
// Ramp up: click cycle until mana is stable above 80%
|
||||
if (mana >= 0.80f)
|
||||
manaStableCount++;
|
||||
else
|
||||
manaStableCount = 0;
|
||||
|
||||
var targetX = atkX + Rng.Next(-30, 31);
|
||||
var targetY = atkY + Rng.Next(-30, 31);
|
||||
await _game.MoveMouseFast(targetX, targetY);
|
||||
|
||||
_game.LeftMouseDown();
|
||||
await Helpers.Sleep(Rng.Next(20, 35));
|
||||
_game.LeftMouseUp();
|
||||
_game.RightMouseDown();
|
||||
await Helpers.Sleep(Rng.Next(20, 35));
|
||||
_game.RightMouseUp();
|
||||
|
||||
await Helpers.Sleep(Rng.Next(50, 80));
|
||||
|
||||
// After 5 consecutive checks above 80%, switch to hold
|
||||
if (manaStableCount >= 5)
|
||||
{
|
||||
Log.Information("Mana stable at {Mana:P0}, switching to hold attack", mana);
|
||||
_game.LeftMouseDown();
|
||||
_game.RightMouseDown();
|
||||
holding = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Holding: nudge mouse toward boss and monitor mana
|
||||
var targetX = atkX + Rng.Next(-30, 31);
|
||||
var targetY = atkY + Rng.Next(-30, 31);
|
||||
await _game.MoveMouseFast(targetX, targetY);
|
||||
|
||||
if (mana < 0.30f)
|
||||
{
|
||||
Log.Information("Mana dropped to {Mana:P0}, releasing to recover", mana);
|
||||
_game.LeftMouseUp();
|
||||
_game.RightMouseUp();
|
||||
holding = false;
|
||||
manaStableCount = 0;
|
||||
}
|
||||
|
||||
await Helpers.Sleep(Rng.Next(80, 120));
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
Log.Warning("AttackBossUntilGone timed out after {Ms}ms", timeoutMs);
|
||||
return lastBossWorldPos;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (holding)
|
||||
{
|
||||
consecutiveMisses++;
|
||||
if (consecutiveMisses >= 15)
|
||||
{
|
||||
Log.Information("Boss gone after {Ms}ms ({Misses} consecutive misses)",
|
||||
sw.ElapsedMilliseconds, consecutiveMisses);
|
||||
return;
|
||||
}
|
||||
await Helpers.Sleep(200);
|
||||
_game.LeftMouseUp();
|
||||
_game.RightMouseUp();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
@ -469,17 +624,44 @@ public class BossRunExecutor : GameExecutor
|
|||
await _game.MoveMouseFast(targetX, targetY);
|
||||
|
||||
_game.LeftMouseDown();
|
||||
await Helpers.Sleep(Rng.Next(30, 50));
|
||||
await Helpers.Sleep(Rng.Next(20, 35));
|
||||
_game.LeftMouseUp();
|
||||
await Helpers.Sleep(Rng.Next(20, 40));
|
||||
_game.RightMouseDown();
|
||||
await Helpers.Sleep(Rng.Next(30, 50));
|
||||
await Helpers.Sleep(Rng.Next(20, 35));
|
||||
_game.RightMouseUp();
|
||||
|
||||
await Helpers.Sleep(Rng.Next(100, 150));
|
||||
await Helpers.Sleep(Rng.Next(50, 80));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find all template matches and click the one closest to screen center.
|
||||
/// </summary>
|
||||
private async Task ClickClosestTemplateToCenter(string templatePath)
|
||||
{
|
||||
const int screenCx = 2560 / 2;
|
||||
const int screenCy = 1440 / 2;
|
||||
|
||||
var matches = await _screen.TemplateMatchAll(templatePath);
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
Log.Warning("No matches found for {Template}, clicking screen center", Path.GetFileName(templatePath));
|
||||
await _game.LeftClickAt(screenCx, screenCy);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Walk to a world position using WASD keys, checking minimap position each iteration.
|
||||
/// </summary>
|
||||
|
|
@ -487,8 +669,12 @@ public class BossRunExecutor : GameExecutor
|
|||
{
|
||||
Log.Information("Walking to world ({X:F0},{Y:F0})", worldX, worldY);
|
||||
|
||||
const int screenCx = 1280;
|
||||
const int screenCy = 720;
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var heldKeys = new HashSet<int>();
|
||||
var lastBlinkMs = -2300L; // allow immediate first blink
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -512,6 +698,19 @@ public class BossRunExecutor : GameExecutor
|
|||
var dirX = dx / len;
|
||||
var dirY = dy / len;
|
||||
|
||||
// Blink toward destination every ~2.3s ± 0.3s
|
||||
var blinkCooldown = 2300 + Rng.Next(-300, 301);
|
||||
if (sw.ElapsedMilliseconds - lastBlinkMs >= blinkCooldown)
|
||||
{
|
||||
// Move mouse in the travel direction so blink goes the right way
|
||||
var blinkX = screenCx + (int)(dirX * 400);
|
||||
var blinkY = screenCy + (int)(dirY * 400);
|
||||
await _game.MoveMouseFast(blinkX, blinkY);
|
||||
await Helpers.Sleep(30);
|
||||
await _game.PressKey(InputSender.VK.SPACE);
|
||||
lastBlinkMs = sw.ElapsedMilliseconds;
|
||||
}
|
||||
|
||||
// Map direction to WASD keys
|
||||
var wanted = new HashSet<int>();
|
||||
if (dirY < -0.3) wanted.Add(InputSender.VK.W); // up
|
||||
|
|
@ -556,13 +755,13 @@ public class BossRunExecutor : GameExecutor
|
|||
|
||||
// Walk away from loot (hold S briefly)
|
||||
await _game.KeyDown(InputSender.VK.S);
|
||||
await Helpers.Sleep(1000);
|
||||
await Helpers.Sleep(500);
|
||||
await _game.KeyUp(InputSender.VK.S);
|
||||
await Helpers.Sleep(300);
|
||||
await Helpers.Sleep(200);
|
||||
|
||||
// Press + to open portal
|
||||
await _game.PressPlus();
|
||||
await Helpers.Sleep(1500);
|
||||
await Helpers.Sleep(800);
|
||||
|
||||
// Find "The Ardura Caravan" and click it
|
||||
var caravanPos = await _inventory.FindAndClickNameplate("The Ardura Caravan", maxRetries: 5, retryDelayMs: 1500);
|
||||
|
|
@ -638,6 +837,7 @@ public class BossRunExecutor : GameExecutor
|
|||
|
||||
// Close stash
|
||||
await _game.PressEscape();
|
||||
_inventory.ResetStashTabState();
|
||||
await Helpers.Sleep(Delays.PostEscape);
|
||||
|
||||
Log.Information("Loot stored");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue