work on well of souls and yolo detection

This commit is contained in:
Boki 2026-02-20 16:40:50 -05:00
parent 3456e0d62a
commit 40d30115bf
41 changed files with 3031 additions and 148 deletions

View file

@ -103,6 +103,7 @@ public class MapRequirementsConverter : IValueConverter
MapType.TrialOfChaos => "Trial Token x1",
MapType.Temple => "Identity Scroll x20",
MapType.Endgame => "Identity Scroll x20",
MapType.Kulemak => "Invitation x1",
_ => "",
};
}

View file

@ -181,13 +181,16 @@ public sealed class D2dOverlay
private OverlayState BuildState(double fps, RenderTiming timing)
{
var detection = _bot.EnemyDetector.Latest;
var bossDetection = _bot.BossDetector.Latest;
return new OverlayState(
Enemies: detection.Enemies,
Bosses: bossDetection.Bosses,
InferenceMs: detection.InferenceMs,
Hud: _bot.HudReader.Current,
NavState: _bot.Navigation.State,
NavPosition: _bot.Navigation.Position,
IsExploring: _bot.Navigation.IsExploring,
ShowHudDebug: _bot.Store.Settings.ShowHudDebug,
Fps: fps,
Timing: timing);
}

View file

@ -24,11 +24,13 @@ public sealed class D2dRenderContext : IDisposable
// Pre-created brushes
public ID2D1SolidColorBrush Red { get; private set; } = null!;
public ID2D1SolidColorBrush Yellow { get; private set; } = null!;
public ID2D1SolidColorBrush Cyan { get; private set; } = null!;
public ID2D1SolidColorBrush Green { get; private set; } = null!;
public ID2D1SolidColorBrush White { get; private set; } = null!;
public ID2D1SolidColorBrush Gray { get; private set; } = null!;
public ID2D1SolidColorBrush LifeBrush { get; private set; } = null!;
public ID2D1SolidColorBrush ManaBrush { get; private set; } = null!;
public ID2D1SolidColorBrush ShieldBrush { get; private set; } = null!;
public ID2D1SolidColorBrush BarBgBrush { get; private set; } = null!;
public ID2D1SolidColorBrush LabelBgBrush { get; private set; } = null!;
public ID2D1SolidColorBrush DebugTextBrush { get; private set; } = null!;
@ -79,11 +81,13 @@ public sealed class D2dRenderContext : IDisposable
{
Red = RenderTarget.CreateSolidColorBrush(new Color4(1f, 0f, 0f, 1f));
Yellow = RenderTarget.CreateSolidColorBrush(new Color4(1f, 1f, 0f, 1f));
Cyan = RenderTarget.CreateSolidColorBrush(new Color4(0f, 1f, 1f, 1f));
Green = RenderTarget.CreateSolidColorBrush(new Color4(0.31f, 1f, 0.31f, 1f)); // 80,255,80
White = RenderTarget.CreateSolidColorBrush(new Color4(1f, 1f, 1f, 1f));
Gray = RenderTarget.CreateSolidColorBrush(new Color4(0.5f, 0.5f, 0.5f, 1f));
LifeBrush = RenderTarget.CreateSolidColorBrush(new Color4(200 / 255f, 40 / 255f, 40 / 255f, 1f));
ManaBrush = RenderTarget.CreateSolidColorBrush(new Color4(40 / 255f, 80 / 255f, 200 / 255f, 1f));
ShieldBrush = RenderTarget.CreateSolidColorBrush(new Color4(100 / 255f, 180 / 255f, 220 / 255f, 1f));
BarBgBrush = RenderTarget.CreateSolidColorBrush(new Color4(20 / 255f, 20 / 255f, 20 / 255f, 140 / 255f));
LabelBgBrush = RenderTarget.CreateSolidColorBrush(new Color4(0f, 0f, 0f, 160 / 255f));
DebugTextBrush = RenderTarget.CreateSolidColorBrush(new Color4(80 / 255f, 1f, 80 / 255f, 1f));
@ -95,11 +99,13 @@ public sealed class D2dRenderContext : IDisposable
{
Red?.Dispose();
Yellow?.Dispose();
Cyan?.Dispose();
Green?.Dispose();
White?.Dispose();
Gray?.Dispose();
LifeBrush?.Dispose();
ManaBrush?.Dispose();
ShieldBrush?.Dispose();
BarBgBrush?.Dispose();
LabelBgBrush?.Dispose();
DebugTextBrush?.Dispose();

View file

@ -5,11 +5,13 @@ namespace Poe2Trade.Ui.Overlay;
public record OverlayState(
IReadOnlyList<DetectedEnemy> Enemies,
IReadOnlyList<DetectedBoss> Bosses,
float InferenceMs,
HudSnapshot? Hud,
NavigationState NavState,
MapPosition NavPosition,
bool IsExploring,
bool ShowHudDebug,
double Fps,
RenderTiming? Timing);

View file

@ -27,7 +27,7 @@ internal sealed class D2dDebugTextLayer : ID2dOverlayLayer, IDisposable
UpdateCache(ctx, _left, ref lc, $"Pos: ({state.NavPosition.X:F0}, {state.NavPosition.Y:F0})", ctx.DebugTextBrush);
UpdateCache(ctx, _left, ref lc, $"Enemies: {state.Enemies.Count} YOLO: {state.InferenceMs:F1}ms", ctx.DebugTextBrush);
if (state.Hud is { Timestamp: > 0 } hud)
UpdateCache(ctx, _left, ref lc, $"HP: {hud.LifePct:P0} MP: {hud.ManaPct:P0}", ctx.DebugTextBrush);
UpdateCache(ctx, _left, ref lc, $"HP: {hud.LifePct:P0} ES: {hud.ShieldPct:P0} MP: {hud.ManaPct:P0}", ctx.DebugTextBrush);
// Right column: timing
if (state.Timing != null)

View file

@ -11,8 +11,13 @@ internal sealed class D2dEnemyBoxLayer : ID2dOverlayLayer, IDisposable
private readonly IDWriteTextLayout[] _confirmedLabels = new IDWriteTextLayout[101];
private readonly IDWriteTextLayout[] _unconfirmedLabels = new IDWriteTextLayout[101];
// Boss labels: cached by "classname NN%" string
private readonly Dictionary<string, IDWriteTextLayout> _bossLabels = new();
private readonly D2dRenderContext _ctx;
public D2dEnemyBoxLayer(D2dRenderContext ctx)
{
_ctx = ctx;
for (int i = 0; i <= 100; i++)
{
var text = $"{i}%";
@ -41,18 +46,43 @@ internal sealed class D2dEnemyBoxLayer : ID2dOverlayLayer, IDisposable
var labelX = enemy.X;
var labelY = enemy.Y - m.Height - 2;
// Background behind label
rt.FillRectangle(
new RectangleF(labelX - 1, labelY - 1, m.Width + 2, m.Height + 2),
ctx.LabelBgBrush);
rt.DrawTextLayout(new System.Numerics.Vector2(labelX, labelY), layout, textBrush);
}
// Boss bounding boxes (cyan)
foreach (var boss in state.Bosses)
{
var rect = new RectangleF(boss.X, boss.Y, boss.Width, boss.Height);
rt.DrawRectangle(rect, ctx.Cyan, 3f);
var pct = Math.Clamp((int)(boss.Confidence * 100), 0, 100);
var key = $"{boss.ClassName} {pct}%";
if (!_bossLabels.TryGetValue(key, out var layout))
{
layout = _ctx.CreateTextLayout(key, _ctx.LabelFormat);
_bossLabels[key] = layout;
}
var m = layout.Metrics;
var labelX = boss.X;
var labelY = boss.Y - m.Height - 2;
rt.FillRectangle(
new RectangleF(labelX - 1, labelY - 1, m.Width + 2, m.Height + 2),
ctx.LabelBgBrush);
rt.DrawTextLayout(new System.Numerics.Vector2(labelX, labelY), layout, ctx.Cyan);
}
}
public void Dispose()
{
foreach (var l in _confirmedLabels) l?.Dispose();
foreach (var l in _unconfirmedLabels) l?.Dispose();
foreach (var l in _bossLabels.Values) l?.Dispose();
}
}

View file

@ -7,15 +7,20 @@ namespace Poe2Trade.Ui.Overlay.Layers;
internal sealed class D2dHudInfoLayer : ID2dOverlayLayer, IDisposable
{
private const float BarWidth = 200;
private const float BarWidth = 160;
private const float BarHeight = 16;
private const float BarY = 1300;
private const float LifeBarX = 1130;
private const float ManaBarX = 1230;
private const float BarGap = 8;
private const float BarY = 1416; // near bottom of 1440
// 3 bars centered: total = 160*3 + 8*2 = 496, start = (2560-496)/2 = 1032
private const float LifeBarX = 1032;
private const float ShieldBarX = LifeBarX + BarWidth + BarGap;
private const float ManaBarX = ShieldBarX + BarWidth + BarGap;
// Cached bar value layouts
private string? _lifeLabel;
private IDWriteTextLayout? _lifeLayout;
private string? _shieldLabel;
private IDWriteTextLayout? _shieldLayout;
private string? _manaLabel;
private IDWriteTextLayout? _manaLayout;
@ -23,14 +28,24 @@ internal sealed class D2dHudInfoLayer : ID2dOverlayLayer, IDisposable
{
if (state.Hud == null || state.Hud.Timestamp == 0) return;
DrawBar(ctx, LifeBarX, BarY, state.Hud.LifePct, ctx.LifeBrush, state.Hud.Life,
DrawBar(ctx, LifeBarX, BarY, state.Hud.LifePct, ctx.LifeBrush,
ref _lifeLabel, ref _lifeLayout);
DrawBar(ctx, ManaBarX, BarY, state.Hud.ManaPct, ctx.ManaBrush, state.Hud.Mana,
DrawBar(ctx, ShieldBarX, BarY, state.Hud.ShieldPct, ctx.ShieldBrush,
ref _shieldLabel, ref _shieldLayout);
DrawBar(ctx, ManaBarX, BarY, state.Hud.ManaPct, ctx.ManaBrush,
ref _manaLabel, ref _manaLayout);
// DEBUG: draw sampling lines
if (state.ShowHudDebug)
{
DrawShieldArc(ctx);
DrawSampleLine(ctx, 167, 1185, 1411, ctx.LifeBrush); // life
DrawSampleLine(ctx, 2394, 1185, 1411, ctx.ManaBrush); // mana
}
}
private static void DrawBar(D2dRenderContext ctx, float x, float y, float pct,
ID2D1SolidColorBrush fillBrush, Screen.HudValues? values,
ID2D1SolidColorBrush fillBrush,
ref string? cachedLabel, ref IDWriteTextLayout? cachedLayout)
{
var rt = ctx.RenderTarget;
@ -42,31 +57,57 @@ internal sealed class D2dHudInfoLayer : ID2dOverlayLayer, IDisposable
rt.DrawRectangle(outer, ctx.Gray, 1f);
// Fill
var fillWidth = BarWidth * Math.Clamp(pct, 0, 1);
var clamped = Math.Clamp(pct, 0, 1);
var fillWidth = BarWidth * clamped;
if (fillWidth > 0)
rt.FillRectangle(new RectangleF(x, y, fillWidth, BarHeight), fillBrush);
// Value text
if (values != null)
// Percentage text
var label = $"{clamped:P0}";
if (label != cachedLabel)
{
var label = $"{values.Current}/{values.Max}";
if (label != cachedLabel)
{
cachedLayout?.Dispose();
cachedLabel = label;
cachedLayout = ctx.CreateTextLayout(label, ctx.BarValueFormat);
}
var m = cachedLayout!.Metrics;
var textX = x + (BarWidth - m.Width) / 2;
var textY = y + (BarHeight - m.Height) / 2;
rt.DrawTextLayout(new System.Numerics.Vector2(textX, textY), cachedLayout, ctx.White);
cachedLayout?.Dispose();
cachedLabel = label;
cachedLayout = ctx.CreateTextLayout(label, ctx.BarValueFormat);
}
var m = cachedLayout!.Metrics;
var textX = x + (BarWidth - m.Width) / 2;
var textY = y + (BarHeight - m.Height) / 2;
rt.DrawTextLayout(new System.Numerics.Vector2(textX, textY), cachedLayout, ctx.White);
}
private static void DrawShieldArc(D2dRenderContext ctx)
{
const float cx = 170, cy = 1298, r = 130;
var rt = ctx.RenderTarget;
// Draw dots along the right semicircle (-90° to +90°)
for (int deg = -90; deg <= 90; deg += 2)
{
var rad = deg * Math.PI / 180.0;
var x = (float)(cx + r * Math.Cos(rad));
var y = (float)(cy + r * Math.Sin(rad));
rt.FillRectangle(new RectangleF(x - 1, y - 1, 3, 3), ctx.Yellow);
}
// Draw center cross
rt.FillRectangle(new RectangleF(cx - 3, cy - 1, 7, 3), ctx.Yellow);
rt.FillRectangle(new RectangleF(cx - 1, cy - 3, 3, 7), ctx.Yellow);
}
private static void DrawSampleLine(D2dRenderContext ctx, float x, float yTop, float yBot, ID2D1SolidColorBrush brush)
{
ctx.RenderTarget.DrawLine(
new System.Numerics.Vector2(x, yTop),
new System.Numerics.Vector2(x, yBot),
brush, 2f);
}
public void Dispose()
{
_lifeLayout?.Dispose();
_shieldLayout?.Dispose();
_manaLayout?.Dispose();
}
}

View file

@ -12,6 +12,11 @@ public partial class DebugViewModel : ObservableObject
[ObservableProperty] private string _findText = "";
[ObservableProperty] private string _debugResult = "";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(BurstCaptureLabel))]
private bool _isBurstCapturing;
public string BurstCaptureLabel => IsBurstCapturing ? "Stop Capture" : "Burst Capture";
[ObservableProperty] private string _selectedGridLayout = "inventory";
[ObservableProperty] private decimal? _clickX;
[ObservableProperty] private decimal? _clickY;
@ -148,6 +153,15 @@ public partial class DebugViewModel : ObservableObject
}
}
[RelayCommand]
private void DetectionStatus()
{
var enemy = _bot.EnemyDetector.Latest;
var boss = _bot.BossDetector.Latest;
DebugResult = $"Enemy: enabled={_bot.EnemyDetector.Enabled}, count={enemy.Enemies.Count}, ms={enemy.InferenceMs:F1}\n" +
$"Boss: enabled={_bot.BossDetector.Enabled}, count={boss.Bosses.Count}, ms={boss.InferenceMs:F1}";
}
[RelayCommand]
private void SaveMinimapDebug()
{
@ -187,6 +201,116 @@ public partial class DebugViewModel : ObservableObject
catch (Exception ex) { DebugResult = $"Failed: {ex.Message}"; }
}
[RelayCommand]
private async Task AttackTest()
{
const int VK_Q = 0x51;
const int DurationMs = 30_000;
const int PollMs = 100;
const float ManaLow = 0.50f;
const float ManaResume = 0.75f;
const float ManaQThreshold = 0.60f;
const int QPhaseStableMs = 2_000;
const int QCooldownMs = 5_000;
var rng = new Random();
try
{
DebugResult = "Attack test: focusing game...";
await _bot.Game.FocusGame();
await _bot.Game.MoveMouseTo(1280, 720);
await Task.Delay(300);
var holding = true;
_bot.Game.LeftMouseDown();
_bot.Game.RightMouseDown();
var sw = System.Diagnostics.Stopwatch.StartNew();
var manaStableStart = (long?)null;
var qPhase = false;
long lastQTime = -QCooldownMs;
while (sw.ElapsedMilliseconds < DurationMs)
{
var mana = _bot.HudReader.Current.ManaPct;
var elapsed = sw.ElapsedMilliseconds;
// Mana management
if (holding && mana < ManaLow)
{
_bot.Game.LeftMouseUp();
_bot.Game.RightMouseUp();
holding = false;
DebugResult = $"Attack test: mana low ({mana:P0}), waiting...";
await Task.Delay(50 + rng.Next(100));
}
else if (!holding && mana >= ManaResume)
{
await Task.Delay(50 + rng.Next(100));
_bot.Game.LeftMouseDown();
_bot.Game.RightMouseDown();
holding = true;
DebugResult = $"Attack test: mana recovered ({mana:P0}), attacking...";
}
// Track Q phase activation
if (!qPhase)
{
if (mana > ManaQThreshold)
{
manaStableStart ??= elapsed;
if (elapsed - manaStableStart.Value >= QPhaseStableMs)
{
qPhase = true;
DebugResult = "Attack test: Q phase activated";
}
}
else
{
manaStableStart = null;
}
}
// Press Q+E periodically
if (qPhase && holding && elapsed - lastQTime >= QCooldownMs)
{
await _bot.Game.PressKey(VK_Q);
await Task.Delay(100 + rng.Next(100));
_bot.Game.LeftMouseUp();
_bot.Game.RightMouseUp();
await Task.Delay(200 + rng.Next(100));
_bot.Game.LeftMouseDown();
_bot.Game.RightMouseDown();
lastQTime = elapsed;
}
await Task.Delay(PollMs + rng.Next(100));
}
DebugResult = "Attack test: completed (30s)";
}
catch (Exception ex)
{
DebugResult = $"Attack test failed: {ex.Message}";
Log.Error(ex, "Attack test failed");
}
finally
{
_bot.Game.LeftMouseUp();
_bot.Game.RightMouseUp();
}
}
[RelayCommand]
private void ToggleBurstCapture()
{
IsBurstCapturing = !IsBurstCapturing;
_bot.FrameSaver.BurstMode = IsBurstCapturing;
DebugResult = IsBurstCapturing
? "Burst capture ON — saving every 200ms to training-data/raw/"
: $"Burst capture OFF — {_bot.FrameSaver.SavedCount} frames saved";
}
[RelayCommand]
private async Task ClickSalvage()
{

View file

@ -194,11 +194,12 @@ public partial class MainWindowViewModel : ObservableObject
{
Log.Information("END pressed — emergency stop");
await _bot.Navigation.Stop();
_bot.BossRunExecutor.Stop();
_bot.Pause();
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
IsPaused = true;
State = "Stopped (F12)";
State = "Stopped (END)";
});
}
f12WasDown = endDown;

View file

@ -1,3 +1,4 @@
using System.Collections.ObjectModel;
using Timer = System.Timers.Timer;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
@ -19,16 +20,33 @@ public partial class MappingViewModel : ObservableObject, IDisposable
[ObservableProperty] private int _enemiesDetected;
[ObservableProperty] private float _inferenceMs;
[ObservableProperty] private bool _hasModel;
[ObservableProperty] private bool _isKulemak;
[ObservableProperty] private bool _kulemakEnabled;
[ObservableProperty] private string _invitationTabPath = "";
[ObservableProperty] private string _lootTabPath = "";
[ObservableProperty] private decimal? _invitationCount = 15;
public static MapType[] MapTypes { get; } = [MapType.TrialOfChaos, MapType.Temple, MapType.Endgame];
public static MapType[] MapTypes { get; } = [MapType.TrialOfChaos, MapType.Temple, MapType.Endgame, MapType.Kulemak];
public ObservableCollection<string> StashTabPaths { get; } = [];
private static readonly string ModelPath = Path.GetFullPath("tools/python-detect/models/enemy-v1.pt");
private static readonly string ModelsDir = Path.GetFullPath("tools/python-detect/models");
private static bool AnyModelExists() =>
Directory.Exists(ModelsDir) && Directory.GetFiles(ModelsDir, "*.pt").Length > 0;
public MappingViewModel(BotOrchestrator bot)
{
_bot = bot;
_selectedMapType = bot.Config.MapType;
_hasModel = File.Exists(ModelPath);
_isKulemak = _selectedMapType == MapType.Kulemak;
_hasModel = AnyModelExists();
// Load Kulemak settings
_kulemakEnabled = bot.Config.Kulemak.Enabled;
_invitationTabPath = bot.Config.Kulemak.InvitationTabPath;
_lootTabPath = bot.Config.Kulemak.LootTabPath;
_invitationCount = bot.Config.Kulemak.InvitationCount;
LoadStashTabPaths();
_bot.EnemyDetector.DetectionUpdated += OnDetectionUpdated;
@ -40,6 +58,47 @@ public partial class MappingViewModel : ObservableObject, IDisposable
partial void OnSelectedMapTypeChanged(MapType value)
{
_bot.Store.UpdateSettings(s => s.MapType = value);
IsKulemak = value == MapType.Kulemak;
}
partial void OnKulemakEnabledChanged(bool value)
{
_bot.Store.UpdateSettings(s => s.Kulemak.Enabled = value);
}
partial void OnInvitationTabPathChanged(string value)
{
_bot.Store.UpdateSettings(s => s.Kulemak.InvitationTabPath = value);
}
partial void OnLootTabPathChanged(string value)
{
_bot.Store.UpdateSettings(s => s.Kulemak.LootTabPath = value);
}
partial void OnInvitationCountChanged(decimal? value)
{
_bot.Store.UpdateSettings(s => s.Kulemak.InvitationCount = (int)(value ?? 15));
}
private void LoadStashTabPaths()
{
StashTabPaths.Clear();
StashTabPaths.Add(""); // empty = not configured
var s = _bot.Store.Settings;
if (s.StashCalibration == null) return;
foreach (var tab in s.StashCalibration.Tabs)
{
if (tab.IsFolder)
{
foreach (var sub in tab.SubTabs)
StashTabPaths.Add($"{tab.Name}/{sub.Name}");
}
else
{
StashTabPaths.Add(tab.Name);
}
}
}
partial void OnIsFrameSaverEnabledChanged(bool value)
@ -50,6 +109,7 @@ public partial class MappingViewModel : ObservableObject, IDisposable
partial void OnIsDetectionEnabledChanged(bool value)
{
_bot.EnemyDetector.Enabled = value;
_bot.BossDetector.Enabled = value;
}
private void OnDetectionUpdated(DetectionSnapshot snapshot)
@ -64,7 +124,7 @@ public partial class MappingViewModel : ObservableObject, IDisposable
private void RefreshStats()
{
FramesSaved = _bot.FrameSaver.SavedCount;
HasModel = File.Exists(ModelPath);
HasModel = AnyModelExists();
}
public void Dispose()

View file

@ -19,6 +19,7 @@ public partial class SettingsViewModel : ObservableObject
[ObservableProperty] private decimal? _waitForMoreItemsMs = 20000;
[ObservableProperty] private decimal? _betweenTradesDelayMs = 5000;
[ObservableProperty] private bool _headless = true;
[ObservableProperty] private bool _showHudDebug;
[ObservableProperty] private bool _isSaved;
[ObservableProperty] private string _calibrationStatus = "";
[ObservableProperty] private string _stashCalibratedAt = "";
@ -44,6 +45,7 @@ public partial class SettingsViewModel : ObservableObject
WaitForMoreItemsMs = s.WaitForMoreItemsMs;
BetweenTradesDelayMs = s.BetweenTradesDelayMs;
Headless = s.Headless;
ShowHudDebug = s.ShowHudDebug;
}
private void LoadTabs()
@ -94,6 +96,7 @@ public partial class SettingsViewModel : ObservableObject
s.WaitForMoreItemsMs = (int)(WaitForMoreItemsMs ?? 20000);
s.BetweenTradesDelayMs = (int)(BetweenTradesDelayMs ?? 5000);
s.Headless = Headless;
s.ShowHudDebug = ShowHudDebug;
});
IsSaved = true;
@ -206,4 +209,5 @@ public partial class SettingsViewModel : ObservableObject
partial void OnWaitForMoreItemsMsChanged(decimal? value) => IsSaved = false;
partial void OnBetweenTradesDelayMsChanged(decimal? value) => IsSaved = false;
partial void OnHeadlessChanged(bool value) => IsSaved = false;
partial void OnShowHudDebugChanged(bool value) => IsSaved = false;
}

View file

@ -233,6 +233,36 @@
</StackPanel>
</Border>
<!-- Kulemak Settings (visible when Kulemak selected) -->
<Border IsVisible="{Binding IsKulemak}" Background="#161b22"
BorderBrush="#30363d" BorderThickness="1"
CornerRadius="8" Padding="10">
<StackPanel Spacing="8">
<TextBlock Text="KULEMAK" FontSize="11" FontWeight="SemiBold"
Foreground="#8b949e" />
<CheckBox IsChecked="{Binding KulemakEnabled}" Content="Enabled"
Foreground="#e6edf3" />
<DockPanel>
<TextBlock Text="Invitation Tab" FontSize="11" Foreground="#8b949e"
Width="140" VerticalAlignment="Center" />
<ComboBox ItemsSource="{Binding StashTabPaths}"
SelectedItem="{Binding InvitationTabPath}" />
</DockPanel>
<DockPanel>
<TextBlock Text="Loot Tab" FontSize="11" Foreground="#8b949e"
Width="140" VerticalAlignment="Center" />
<ComboBox ItemsSource="{Binding StashTabPaths}"
SelectedItem="{Binding LootTabPath}" />
</DockPanel>
<DockPanel>
<TextBlock Text="Invitations per batch" FontSize="11" Foreground="#8b949e"
Width="140" VerticalAlignment="Center" />
<NumericUpDown Value="{Binding InvitationCount}"
Minimum="1" Maximum="60" Increment="1" Width="100" />
</DockPanel>
</StackPanel>
</Border>
<!-- Training Data -->
<Border Background="#161b22" BorderBrush="#30363d" BorderThickness="1"
CornerRadius="8" Padding="10">
@ -298,6 +328,10 @@
<Button Content="ANGE" Command="{Binding ClickAngeCommand}" />
<Button Content="STASH" Command="{Binding ClickStashCommand}" />
<Button Content="SALVAGE" Command="{Binding ClickSalvageCommand}" />
<Button Content="Attack Test" Command="{Binding AttackTestCommand}" />
<Button Content="Detection?" Command="{Binding DetectionStatusCommand}" />
<Button Content="{Binding BurstCaptureLabel}"
Command="{Binding ToggleBurstCaptureCommand}" />
</StackPanel>
</StackPanel>
</Border>
@ -408,6 +442,8 @@
<CheckBox IsChecked="{Binding Headless}" Content="Headless browser"
Foreground="#e6edf3" Margin="0,4,0,0" />
<CheckBox IsChecked="{Binding ShowHudDebug}" Content="Show HUD debug overlay"
Foreground="#e6edf3" />
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,2,0,0">
<Button Content="Save Settings" Command="{Binding SaveSettingsCommand}" />
@ -564,6 +600,7 @@
</ItemsControl>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>