boss ready
This commit is contained in:
parent
89c3a0a077
commit
64a6ab694b
21 changed files with 857 additions and 249 deletions
|
|
@ -18,13 +18,16 @@ public class BossRunExecutor : GameExecutor
|
|||
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 BossHealthbarTemplate2 = Path.Combine("assets", "unique-boss-present2.png");
|
||||
private static readonly string BossHealthbarTemplate3 = Path.Combine("assets", "unique-boss-present3.png");
|
||||
private static readonly string NewInstanceTemplate = Path.Combine("assets", "new.png");
|
||||
private static readonly string ResurrectTemplate = Path.Combine("assets", "resurrect.png");
|
||||
|
||||
private BossRunState _state = BossRunState.Idle;
|
||||
private readonly IClientLogWatcher _logWatcher;
|
||||
private readonly BossDetector _bossDetector;
|
||||
private readonly HudReader _hudReader;
|
||||
private readonly NavigationExecutor _nav;
|
||||
private readonly CombatManager _combat;
|
||||
|
||||
public event Action<BossRunState>? StateChanged;
|
||||
|
||||
|
|
@ -35,7 +38,7 @@ public class BossRunExecutor : GameExecutor
|
|||
{
|
||||
_logWatcher = logWatcher;
|
||||
_bossDetector = bossDetector;
|
||||
_hudReader = hudReader;
|
||||
_combat = new CombatManager(game, hudReader, new FlaskManager(game, hudReader));
|
||||
_nav = nav;
|
||||
}
|
||||
|
||||
|
|
@ -56,8 +59,10 @@ public class BossRunExecutor : GameExecutor
|
|||
public async Task RunBossLoop()
|
||||
{
|
||||
_stopped = false;
|
||||
Log.Information("Starting boss run loop ({Count} invitations)", _config.Kulemak.InvitationCount);
|
||||
var runCount = _config.Kulemak.RunCount;
|
||||
Log.Information("Starting boss run loop ({Count} runs)", runCount);
|
||||
|
||||
// First run: deposit inventory and grab 1 invitation
|
||||
if (!await Prepare())
|
||||
{
|
||||
SetState(BossRunState.Failed);
|
||||
|
|
@ -67,11 +72,11 @@ public class BossRunExecutor : GameExecutor
|
|||
}
|
||||
|
||||
var completed = 0;
|
||||
for (var i = 0; i < _config.Kulemak.InvitationCount; i++)
|
||||
for (var i = 0; i < runCount; i++)
|
||||
{
|
||||
if (_stopped) break;
|
||||
|
||||
Log.Information("=== Boss run {N}/{Total} ===", i + 1, _config.Kulemak.InvitationCount);
|
||||
Log.Information("=== Boss run {N}/{Total} ===", i + 1, runCount);
|
||||
|
||||
if (!await TravelToZone())
|
||||
{
|
||||
|
|
@ -113,13 +118,14 @@ public class BossRunExecutor : GameExecutor
|
|||
}
|
||||
if (_stopped) break;
|
||||
|
||||
await StoreLoot();
|
||||
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, _config.Kulemak.InvitationCount);
|
||||
Log.Information("Boss run loop finished: {Completed}/{Total} runs completed", completed, runCount);
|
||||
SetState(BossRunState.Complete);
|
||||
await Helpers.Sleep(1000);
|
||||
SetState(BossRunState.Idle);
|
||||
|
|
@ -186,7 +192,7 @@ public class BossRunExecutor : GameExecutor
|
|||
(false, false) => "stash12",
|
||||
};
|
||||
|
||||
await _inventory.GrabItemsFromStash(layoutName, _config.Kulemak.InvitationCount, InvitationTemplate);
|
||||
await _inventory.GrabItemsFromStash(layoutName, 1, InvitationTemplate);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -310,7 +316,7 @@ public class BossRunExecutor : GameExecutor
|
|||
Log.Information("Fight phase starting");
|
||||
|
||||
// Wait for arena to settle
|
||||
await Helpers.Sleep(5500);
|
||||
await Helpers.Sleep(4500);
|
||||
if (_stopped) return;
|
||||
|
||||
// Find and click the cathedral door
|
||||
|
|
@ -336,7 +342,7 @@ public class BossRunExecutor : GameExecutor
|
|||
const double wellWorldX = -496;
|
||||
const double wellWorldY = -378;
|
||||
|
||||
await WalkToWorldPosition(fightWorldX, fightWorldY);
|
||||
await WalkToWorldPosition(fightWorldX, fightWorldY, cancelWhen: IsBossAlive);
|
||||
if (_stopped) return;
|
||||
|
||||
// 3x fight-then-well loop
|
||||
|
|
@ -356,14 +362,17 @@ public class BossRunExecutor : GameExecutor
|
|||
Log.Information("Fight area updated to ({X:F0},{Y:F0})", fightWorldX, fightWorldY);
|
||||
}
|
||||
|
||||
// Wait for death animation before looking for well
|
||||
await Helpers.Sleep(3000);
|
||||
|
||||
// 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 ClickClosestTemplateToCenter(CathedralWellTemplate);
|
||||
await Helpers.Sleep(2000);
|
||||
await Helpers.Sleep(200);
|
||||
|
||||
// Walk back to fight position for next phase
|
||||
await WalkToWorldPosition(fightWorldX, fightWorldY);
|
||||
await WalkToWorldPosition(fightWorldX, fightWorldY, cancelWhen: IsBossAlive);
|
||||
}
|
||||
|
||||
// 4th fight - no well after
|
||||
|
|
@ -372,7 +381,10 @@ public class BossRunExecutor : GameExecutor
|
|||
await AttackBossUntilGone();
|
||||
if (_stopped) return;
|
||||
|
||||
// Return the ring
|
||||
// Walk to ring area and return it
|
||||
await WalkToWorldPosition(-440, -330);
|
||||
if (_stopped) return;
|
||||
|
||||
Log.Information("Looking for Return the Ring...");
|
||||
var ring = await _screen.TemplateMatch(ReturnTheRingTemplate);
|
||||
if (ring == null)
|
||||
|
|
@ -419,13 +431,39 @@ public class BossRunExecutor : GameExecutor
|
|||
|
||||
/// <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.
|
||||
/// Healthbar spans (750,16) to (1818,112). Uses lower threshold (0.5) to tolerate YOLO overlay.
|
||||
/// </summary>
|
||||
private async Task<bool> IsBossAlive()
|
||||
{
|
||||
var topRegion = new Region(800, 0, 960, 120);
|
||||
var matches = await _screen.TemplateMatchAll(BossHealthbarTemplate, topRegion, threshold: 0.5);
|
||||
return matches.Count > 0;
|
||||
var topRegion = new Region(750, 16, 1068, 96);
|
||||
|
||||
// Check all three healthbar templates — boss is alive if ANY matches
|
||||
var m1 = await _screen.TemplateMatchAll(BossHealthbarTemplate, topRegion, threshold: 0.5);
|
||||
if (m1.Count > 0) return true;
|
||||
|
||||
var m2 = await _screen.TemplateMatchAll(BossHealthbarTemplate2, topRegion, threshold: 0.5);
|
||||
if (m2.Count > 0) return true;
|
||||
|
||||
var m3 = await _screen.TemplateMatchAll(BossHealthbarTemplate3, topRegion, threshold: 0.5);
|
||||
if (m3.Count > 0) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check for the "Resurrect at Checkpoint" button — means we died.
|
||||
/// If found, click it, wait for respawn, and return true.
|
||||
/// </summary>
|
||||
private async Task<bool> CheckDeath()
|
||||
{
|
||||
var match = await _screen.TemplateMatch(ResurrectTemplate);
|
||||
if (match == null) return false;
|
||||
|
||||
Log.Warning("Death detected! Clicking resurrect at ({X},{Y})", match.X, match.Y);
|
||||
await _combat.ReleaseAll();
|
||||
await _game.LeftClickAt(match.X, match.Y);
|
||||
await Helpers.Sleep(3000); // wait for respawn + loading
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -435,13 +473,13 @@ public class BossRunExecutor : GameExecutor
|
|||
{
|
||||
Log.Information("Waiting for boss healthbar to appear...");
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
const int screenCx = 1280;
|
||||
const int screenCy = 720;
|
||||
var atkX = 1280;
|
||||
var atkY = 720;
|
||||
|
||||
while (sw.ElapsedMilliseconds < timeoutMs)
|
||||
{
|
||||
if (_stopped) return false;
|
||||
if (await CheckDeath()) continue;
|
||||
|
||||
if (await IsBossAlive())
|
||||
{
|
||||
|
|
@ -449,19 +487,14 @@ public class BossRunExecutor : GameExecutor
|
|||
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));
|
||||
// Attack toward YOLO-detected boss if available, otherwise last known position
|
||||
var snapshot = _bossDetector.Latest;
|
||||
if (snapshot.Bosses.Count > 0)
|
||||
{
|
||||
atkX = snapshot.Bosses[0].Cx;
|
||||
atkY = snapshot.Bosses[0].Cy;
|
||||
}
|
||||
await _combat.Tick(atkX, atkY);
|
||||
}
|
||||
|
||||
Log.Warning("WaitForBossSpawn timed out after {Ms}ms", timeoutMs);
|
||||
|
|
@ -490,11 +523,9 @@ public class BossRunExecutor : GameExecutor
|
|||
await Helpers.Sleep(200);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
var consecutiveMisses = 0;
|
||||
var lastCheckMs = 0L;
|
||||
var holding = false;
|
||||
var manaStableCount = 0;
|
||||
|
||||
await _combat.Reset();
|
||||
Log.Information("Boss is alive, engaging");
|
||||
|
||||
try
|
||||
|
|
@ -520,82 +551,22 @@ public class BossRunExecutor : GameExecutor
|
|||
lastBossWorldPos = (bossWorldX, bossWorldY);
|
||||
}
|
||||
|
||||
// Re-check healthbar every ~1.5s, need 5 consecutive misses (~7.5s) to confirm phase end
|
||||
if (sw.ElapsedMilliseconds - lastCheckMs > 1500)
|
||||
// Re-check healthbar every ~0.5s, first miss = phase over
|
||||
if (sw.ElapsedMilliseconds - lastCheckMs > 500)
|
||||
{
|
||||
if (await CheckDeath()) continue;
|
||||
|
||||
var bossAlive = await IsBossAlive();
|
||||
lastCheckMs = sw.ElapsedMilliseconds;
|
||||
|
||||
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;
|
||||
Log.Information("Healthbar not found, boss phase over after {Ms}ms", sw.ElapsedMilliseconds);
|
||||
return lastBossWorldPos;
|
||||
}
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
await _combat.Tick(atkX, atkY);
|
||||
}
|
||||
|
||||
Log.Warning("AttackBossUntilGone timed out after {Ms}ms", timeoutMs);
|
||||
|
|
@ -603,34 +574,26 @@ public class BossRunExecutor : GameExecutor
|
|||
}
|
||||
finally
|
||||
{
|
||||
if (holding)
|
||||
{
|
||||
_game.LeftMouseUp();
|
||||
_game.RightMouseUp();
|
||||
}
|
||||
await _combat.ReleaseAll();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AttackAtPosition(int x, int y, int durationMs)
|
||||
{
|
||||
await _combat.Reset();
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
while (sw.ElapsedMilliseconds < durationMs)
|
||||
try
|
||||
{
|
||||
if (_stopped) return;
|
||||
|
||||
var targetX = x + Rng.Next(-20, 21);
|
||||
var targetY = y + Rng.Next(-20, 21);
|
||||
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));
|
||||
while (sw.ElapsedMilliseconds < durationMs)
|
||||
{
|
||||
if (_stopped) return;
|
||||
if (await CheckDeath()) continue;
|
||||
await _combat.Tick(x, y, jitter: 20);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _combat.ReleaseAll();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -665,7 +628,8 @@ public class BossRunExecutor : GameExecutor
|
|||
/// <summary>
|
||||
/// Walk to a world position using WASD keys, checking minimap position each iteration.
|
||||
/// </summary>
|
||||
private async Task WalkToWorldPosition(double worldX, double worldY, int timeoutMs = 10000, double arrivalDist = 15)
|
||||
private async Task WalkToWorldPosition(double worldX, double worldY, int timeoutMs = 10000,
|
||||
double arrivalDist = 15, Func<Task<bool>>? cancelWhen = null)
|
||||
{
|
||||
Log.Information("Walking to world ({X:F0},{Y:F0})", worldX, worldY);
|
||||
|
||||
|
|
@ -682,6 +646,12 @@ public class BossRunExecutor : GameExecutor
|
|||
{
|
||||
if (_stopped) break;
|
||||
|
||||
if (cancelWhen != null && await cancelWhen())
|
||||
{
|
||||
Log.Information("Walk cancelled early (cancel condition met)");
|
||||
break;
|
||||
}
|
||||
|
||||
var pos = _nav.WorldPosition;
|
||||
var dx = worldX - pos.X;
|
||||
var dy = worldY - pos.Y;
|
||||
|
|
@ -795,7 +765,7 @@ public class BossRunExecutor : GameExecutor
|
|||
return true;
|
||||
}
|
||||
|
||||
private async Task StoreLoot()
|
||||
private async Task StoreLoot(bool grabInvitation = false)
|
||||
{
|
||||
SetState(BossRunState.StoringLoot);
|
||||
Log.Information("Storing loot");
|
||||
|
|
@ -812,12 +782,11 @@ public class BossRunExecutor : GameExecutor
|
|||
}
|
||||
await Helpers.Sleep(Delays.PostStashOpen);
|
||||
|
||||
// Click loot tab
|
||||
// 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
|
||||
var scanResult = await _screen.Grid.Scan("inventory");
|
||||
if (scanResult.Occupied.Count > 0)
|
||||
{
|
||||
|
|
@ -835,6 +804,25 @@ public class BossRunExecutor : GameExecutor
|
|||
await Helpers.Sleep(Delays.PostEscape);
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
// Close stash
|
||||
await _game.PressEscape();
|
||||
_inventory.ResetStashTabState();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue