started adding navigation
This commit is contained in:
parent
32781b1462
commit
468e0a7246
20 changed files with 844 additions and 31 deletions
|
|
@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Poe2Trade.Inventory", "src\
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Poe2Trade.Ui", "src\Poe2Trade.Ui\Poe2Trade.Ui.csproj", "{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Poe2Trade.Navigation", "src\Poe2Trade.Navigation\Poe2Trade.Navigation.csproj", "{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -68,6 +70,10 @@ Global
|
|||
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{6432F6A5-11A0-4960-AFFC-E810D4325C35} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
|
|
@ -79,5 +85,6 @@ Global
|
|||
{188C4F87-153F-4182-B816-9FB56F08CF3A} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{F186DDC8-6843-43E9-8BD3-9F914C5E784E} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
|||
BIN
assets/minimap-x.png
Normal file
BIN
assets/minimap-x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 253 B |
|
|
@ -3,6 +3,7 @@ using Poe2Trade.Core;
|
|||
using Poe2Trade.Game;
|
||||
using Poe2Trade.Inventory;
|
||||
using Poe2Trade.GameLog;
|
||||
using Poe2Trade.Navigation;
|
||||
using Poe2Trade.Screen;
|
||||
using Poe2Trade.Trade;
|
||||
using Serilog;
|
||||
|
|
@ -38,6 +39,7 @@ public class BotOrchestrator : IAsyncDisposable
|
|||
public IInventoryManager Inventory { get; }
|
||||
public TradeExecutor TradeExecutor { get; }
|
||||
public TradeQueue TradeQueue { get; }
|
||||
public NavigationExecutor Navigation { get; }
|
||||
private readonly Dictionary<string, ScrapExecutor> _scrapExecutors = new();
|
||||
|
||||
// Events
|
||||
|
|
@ -58,12 +60,24 @@ public class BotOrchestrator : IAsyncDisposable
|
|||
TradeExecutor = tradeExecutor;
|
||||
TradeQueue = tradeQueue;
|
||||
Links = links;
|
||||
Navigation = new NavigationExecutor(game);
|
||||
_paused = store.Settings.Paused;
|
||||
}
|
||||
|
||||
public bool IsReady => _started;
|
||||
public bool IsPaused => _paused;
|
||||
|
||||
public BotMode Mode
|
||||
{
|
||||
get => Config.Mode;
|
||||
set
|
||||
{
|
||||
if (Config.Mode == value) return;
|
||||
Store.UpdateSettings(s => s.Mode = value);
|
||||
StatusUpdated?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public string State
|
||||
{
|
||||
get => _state;
|
||||
|
|
@ -142,6 +156,11 @@ public class BotOrchestrator : IAsyncDisposable
|
|||
return;
|
||||
}
|
||||
}
|
||||
if (Navigation.State != NavigationState.Idle)
|
||||
{
|
||||
State = Navigation.State.ToString();
|
||||
return;
|
||||
}
|
||||
State = "Idle";
|
||||
}
|
||||
|
||||
|
|
@ -183,6 +202,7 @@ public class BotOrchestrator : IAsyncDisposable
|
|||
|
||||
// Wire executor events
|
||||
TradeExecutor.StateChanged += _ => UpdateExecutorState();
|
||||
Navigation.StateChanged += _ => UpdateExecutorState();
|
||||
TradeQueue.TradeCompleted += () => { _tradesCompleted++; StatusUpdated?.Invoke(); };
|
||||
TradeQueue.TradeFailed += () => { _tradesFailed++; StatusUpdated?.Invoke(); };
|
||||
Inventory.Updated += () => StatusUpdated?.Invoke();
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@
|
|||
<ProjectReference Include="..\Poe2Trade.Trade\Poe2Trade.Trade.csproj" />
|
||||
<ProjectReference Include="..\Poe2Trade.Log\Poe2Trade.Log.csproj" />
|
||||
<ProjectReference Include="..\Poe2Trade.Inventory\Poe2Trade.Inventory.csproj" />
|
||||
<ProjectReference Include="..\Poe2Trade.Navigation\Poe2Trade.Navigation.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ public class SavedSettings
|
|||
public double? WindowWidth { get; set; }
|
||||
public double? WindowHeight { get; set; }
|
||||
public bool Headless { get; set; } = true;
|
||||
public BotMode Mode { get; set; } = BotMode.Trading;
|
||||
public MapType MapType { get; set; } = MapType.TrialOfChaos;
|
||||
}
|
||||
|
||||
public class ConfigStore
|
||||
|
|
|
|||
|
|
@ -68,3 +68,16 @@ public enum PostAction
|
|||
Stash,
|
||||
Salvage
|
||||
}
|
||||
|
||||
public enum BotMode
|
||||
{
|
||||
Trading,
|
||||
Mapping
|
||||
}
|
||||
|
||||
public enum MapType
|
||||
{
|
||||
TrialOfChaos,
|
||||
Temple,
|
||||
Endgame
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,4 +74,5 @@ public class GameController : IGameController
|
|||
public Task CtrlLeftClickAt(int x, int y) => _input.CtrlLeftClick(x, y);
|
||||
public Task HoldCtrl() => _input.KeyDown(InputSender.VK.CONTROL);
|
||||
public Task ReleaseCtrl() => _input.KeyUp(InputSender.VK.CONTROL);
|
||||
public Task ToggleMinimap() => _input.PressKey(InputSender.VK.TAB);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@ public interface IGameController
|
|||
Task OpenInventory();
|
||||
Task HoldCtrl();
|
||||
Task ReleaseCtrl();
|
||||
Task ToggleMinimap();
|
||||
}
|
||||
|
|
|
|||
119
src/Poe2Trade.Navigation/MinimapCapture.cs
Normal file
119
src/Poe2Trade.Navigation/MinimapCapture.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Runtime.InteropServices;
|
||||
using OpenCvSharp;
|
||||
using OpenCvSharp.Extensions;
|
||||
using Serilog;
|
||||
using Region = Poe2Trade.Core.Region;
|
||||
using Point = OpenCvSharp.Point;
|
||||
using Size = OpenCvSharp.Size;
|
||||
|
||||
namespace Poe2Trade.Navigation;
|
||||
|
||||
public class MinimapCapture : IDisposable
|
||||
{
|
||||
private readonly MinimapConfig _config;
|
||||
private Mat? _circularMask;
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int GetSystemMetrics(int nIndex);
|
||||
|
||||
public MinimapCapture(MinimapConfig config)
|
||||
{
|
||||
_config = config;
|
||||
BuildCircularMask();
|
||||
}
|
||||
|
||||
private void BuildCircularMask()
|
||||
{
|
||||
var size = _config.CaptureSize;
|
||||
_circularMask = new Mat(size, size, MatType.CV_8UC1, Scalar.Black);
|
||||
var center = new Point(size / 2, size / 2);
|
||||
Cv2.Circle(_circularMask, center, _config.FogRadius, Scalar.White, -1);
|
||||
}
|
||||
|
||||
public MinimapFrame? CaptureFrame()
|
||||
{
|
||||
var region = _config.CaptureRegion;
|
||||
using var bitmap = CaptureScreen(region);
|
||||
using var bgr = BitmapConverter.ToMat(bitmap);
|
||||
|
||||
if (bgr.Empty())
|
||||
return null;
|
||||
|
||||
// Apply circular mask to ignore area outside fog-of-war circle
|
||||
using var masked = new Mat();
|
||||
Cv2.BitwiseAnd(bgr, bgr, masked, _circularMask!);
|
||||
|
||||
// Convert to HSV
|
||||
using var hsv = new Mat();
|
||||
Cv2.CvtColor(masked, hsv, ColorConversionCodes.BGR2HSV);
|
||||
|
||||
// Classify explored areas
|
||||
using var exploredMask = new Mat();
|
||||
Cv2.InRange(hsv, _config.ExploredLoHSV, _config.ExploredHiHSV, exploredMask);
|
||||
|
||||
// Classify walls: dark pixels within the circular mask
|
||||
using var valueChan = new Mat();
|
||||
Cv2.ExtractChannel(hsv, valueChan, 2); // V channel
|
||||
using var darkMask = new Mat();
|
||||
Cv2.Threshold(valueChan, darkMask, _config.WallMaxValue, 255, ThresholdTypes.BinaryInv);
|
||||
// Apply morphological close to connect wall fragments
|
||||
using var wallKernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3));
|
||||
using var wallMask = new Mat();
|
||||
Cv2.MorphologyEx(darkMask, wallMask, MorphTypes.Close, wallKernel);
|
||||
// Only within circular mask
|
||||
Cv2.BitwiseAnd(wallMask, _circularMask!, wallMask);
|
||||
|
||||
// Build classified mat: Unknown=0, Explored=1, Wall=2
|
||||
var classified = new Mat(_config.CaptureSize, _config.CaptureSize, MatType.CV_8UC1, Scalar.Black);
|
||||
classified.SetTo(new Scalar((byte)MapCell.Explored), exploredMask);
|
||||
classified.SetTo(new Scalar((byte)MapCell.Wall), wallMask);
|
||||
// Ensure only within circular mask
|
||||
Cv2.BitwiseAnd(classified, _circularMask!, classified);
|
||||
|
||||
// Detect player marker (orange X)
|
||||
using var playerMask = new Mat();
|
||||
Cv2.InRange(hsv, _config.PlayerLoHSV, _config.PlayerHiHSV, playerMask);
|
||||
var playerOffset = FindCentroid(playerMask);
|
||||
|
||||
// Convert to grayscale for phase correlation
|
||||
var gray = new Mat();
|
||||
Cv2.CvtColor(masked, gray, ColorConversionCodes.BGR2GRAY);
|
||||
// Apply circular mask to gray too
|
||||
Cv2.BitwiseAnd(gray, _circularMask!, gray);
|
||||
|
||||
return new MinimapFrame(
|
||||
GrayMat: gray,
|
||||
ClassifiedMat: classified,
|
||||
PlayerOffset: playerOffset,
|
||||
Timestamp: DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
);
|
||||
}
|
||||
|
||||
private Point2d FindCentroid(Mat mask)
|
||||
{
|
||||
var moments = Cv2.Moments(mask, true);
|
||||
if (moments.M00 < 10) // not enough pixels
|
||||
return new Point2d(0, 0);
|
||||
|
||||
var cx = moments.M10 / moments.M00 - _config.CaptureSize / 2.0;
|
||||
var cy = moments.M01 / moments.M00 - _config.CaptureSize / 2.0;
|
||||
return new Point2d(cx, cy);
|
||||
}
|
||||
|
||||
private static Bitmap CaptureScreen(Region region)
|
||||
{
|
||||
var bitmap = new Bitmap(region.Width, region.Height, PixelFormat.Format32bppArgb);
|
||||
using var g = Graphics.FromImage(bitmap);
|
||||
g.CopyFromScreen(region.X, region.Y, 0, 0,
|
||||
new System.Drawing.Size(region.Width, region.Height),
|
||||
CopyPixelOperation.SourceCopy);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_circularMask?.Dispose();
|
||||
}
|
||||
}
|
||||
156
src/Poe2Trade.Navigation/NavigationExecutor.cs
Normal file
156
src/Poe2Trade.Navigation/NavigationExecutor.cs
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
using Poe2Trade.Core;
|
||||
using Poe2Trade.Game;
|
||||
using Serilog;
|
||||
|
||||
namespace Poe2Trade.Navigation;
|
||||
|
||||
public class NavigationExecutor : IDisposable
|
||||
{
|
||||
private readonly IGameController _game;
|
||||
private readonly MinimapConfig _config;
|
||||
private readonly MinimapCapture _capture;
|
||||
private readonly PositionTracker _tracker;
|
||||
private readonly WorldMap _worldMap;
|
||||
private NavigationState _state = NavigationState.Idle;
|
||||
private bool _stopped;
|
||||
private static readonly Random Rng = new();
|
||||
|
||||
public event Action<NavigationState>? StateChanged;
|
||||
public NavigationState State => _state;
|
||||
|
||||
public NavigationExecutor(IGameController game, MinimapConfig? config = null)
|
||||
{
|
||||
_game = game;
|
||||
_config = config ?? new MinimapConfig();
|
||||
_capture = new MinimapCapture(_config);
|
||||
_tracker = new PositionTracker(_config);
|
||||
_worldMap = new WorldMap(_config);
|
||||
}
|
||||
|
||||
private void SetState(NavigationState s)
|
||||
{
|
||||
_state = s;
|
||||
StateChanged?.Invoke(s);
|
||||
}
|
||||
|
||||
public Task Stop()
|
||||
{
|
||||
_stopped = true;
|
||||
SetState(NavigationState.Idle);
|
||||
Log.Information("Navigation executor stopped");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_tracker.Reset();
|
||||
_worldMap.Reset();
|
||||
_stopped = false;
|
||||
SetState(NavigationState.Idle);
|
||||
}
|
||||
|
||||
public async Task RunExploreLoop()
|
||||
{
|
||||
_stopped = false;
|
||||
Log.Information("Starting explore loop");
|
||||
|
||||
// Open minimap overlay (Tab)
|
||||
await _game.ToggleMinimap();
|
||||
await Helpers.Sleep(300);
|
||||
|
||||
while (!_stopped)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. Capture frame
|
||||
SetState(NavigationState.Capturing);
|
||||
using var frame = _capture.CaptureFrame();
|
||||
if (frame == null)
|
||||
{
|
||||
Log.Warning("Failed to capture minimap frame");
|
||||
await Helpers.Sleep(200);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Track position via phase correlation
|
||||
SetState(NavigationState.Processing);
|
||||
var pos = _tracker.UpdatePosition(frame.GrayMat);
|
||||
|
||||
// 3. Stitch into world map
|
||||
_worldMap.StitchFrame(frame.ClassifiedMat, pos);
|
||||
|
||||
// 4. Check if stuck
|
||||
if (_tracker.IsStuck)
|
||||
{
|
||||
SetState(NavigationState.Stuck);
|
||||
Log.Information("Stuck detected, clicking random direction");
|
||||
await ClickRandomDirection();
|
||||
await Helpers.Sleep(_config.MovementWaitMs);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5. Find best exploration direction
|
||||
SetState(NavigationState.Planning);
|
||||
var direction = _worldMap.FindNearestUnexplored(pos);
|
||||
|
||||
if (direction == null)
|
||||
{
|
||||
Log.Information("Map fully explored");
|
||||
SetState(NavigationState.Completed);
|
||||
break;
|
||||
}
|
||||
|
||||
// 6. Click to move in that direction
|
||||
SetState(NavigationState.Moving);
|
||||
await ClickToMove(direction.Value.dirX, direction.Value.dirY);
|
||||
|
||||
// 7. Wait for character to walk
|
||||
await Helpers.Sleep(_config.MovementWaitMs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error in explore loop");
|
||||
SetState(NavigationState.Failed);
|
||||
await Helpers.Sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
if (_state != NavigationState.Completed)
|
||||
SetState(NavigationState.Idle);
|
||||
|
||||
Log.Information("Explore loop ended");
|
||||
}
|
||||
|
||||
private async Task ClickToMove(double dirX, double dirY)
|
||||
{
|
||||
// Player is at minimap center on screen; click offset from center
|
||||
var len = Math.Sqrt(dirX * dirX + dirY * dirY);
|
||||
if (len < 0.001) return;
|
||||
|
||||
var nx = dirX / len;
|
||||
var ny = dirY / len;
|
||||
|
||||
var clickX = _config.MinimapCenterX + (int)(nx * _config.ClickRadius);
|
||||
var clickY = _config.MinimapCenterY + (int)(ny * _config.ClickRadius);
|
||||
|
||||
Log.Debug("Click to move: ({X}, {Y}) dir=({Dx:F2}, {Dy:F2})", clickX, clickY, nx, ny);
|
||||
await _game.LeftClickAt(clickX, clickY);
|
||||
}
|
||||
|
||||
private async Task ClickRandomDirection()
|
||||
{
|
||||
var angle = Rng.NextDouble() * 2 * Math.PI;
|
||||
await ClickToMove(Math.Cos(angle), Math.Sin(angle));
|
||||
}
|
||||
|
||||
public MapPosition Position => _tracker.Position;
|
||||
public byte[] GetMapSnapshot() => _worldMap.GetMapSnapshot();
|
||||
public byte[] GetViewportSnapshot(int viewSize = 400) => _worldMap.GetViewportSnapshot(_tracker.Position, viewSize);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_capture.Dispose();
|
||||
_tracker.Dispose();
|
||||
_worldMap.Dispose();
|
||||
}
|
||||
}
|
||||
82
src/Poe2Trade.Navigation/NavigationTypes.cs
Normal file
82
src/Poe2Trade.Navigation/NavigationTypes.cs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
using Poe2Trade.Core;
|
||||
using OpenCvSharp;
|
||||
|
||||
namespace Poe2Trade.Navigation;
|
||||
|
||||
public enum NavigationState
|
||||
{
|
||||
Idle,
|
||||
Capturing,
|
||||
Processing,
|
||||
Planning,
|
||||
Moving,
|
||||
Stuck,
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
|
||||
public record MapPosition(double X, double Y);
|
||||
|
||||
public enum MapCell : byte
|
||||
{
|
||||
Unknown = 0,
|
||||
Explored = 1,
|
||||
Wall = 2
|
||||
}
|
||||
|
||||
public record MinimapFrame(
|
||||
Mat GrayMat,
|
||||
Mat ClassifiedMat,
|
||||
Point2d PlayerOffset,
|
||||
long Timestamp
|
||||
) : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
GrayMat.Dispose();
|
||||
ClassifiedMat.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public class MinimapConfig
|
||||
{
|
||||
// Minimap center on screen (2560x1440)
|
||||
public int MinimapCenterX { get; set; } = 1280;
|
||||
public int MinimapCenterY { get; set; } = 700;
|
||||
|
||||
// Capture region: 300x300 centered at minimap center
|
||||
public Region CaptureRegion => new(
|
||||
MinimapCenterX - CaptureSize / 2,
|
||||
MinimapCenterY - CaptureSize / 2,
|
||||
CaptureSize, CaptureSize);
|
||||
|
||||
public int CaptureSize { get; set; } = 300;
|
||||
|
||||
// Fog-of-war circle radius within the captured frame
|
||||
public int FogRadius { get; set; } = 120;
|
||||
|
||||
// HSV range for explored areas (blue/teal)
|
||||
public Scalar ExploredLoHSV { get; set; } = new(85, 30, 30);
|
||||
public Scalar ExploredHiHSV { get; set; } = new(145, 255, 255);
|
||||
|
||||
// HSV range for player marker (orange X)
|
||||
public Scalar PlayerLoHSV { get; set; } = new(5, 100, 100);
|
||||
public Scalar PlayerHiHSV { get; set; } = new(25, 255, 255);
|
||||
|
||||
// HSV range for walls (dark pixels)
|
||||
public int WallMaxValue { get; set; } = 40;
|
||||
|
||||
// Movement
|
||||
public int ClickRadius { get; set; } = 100;
|
||||
public int MovementWaitMs { get; set; } = 600;
|
||||
|
||||
// World map canvas
|
||||
public int CanvasSize { get; set; } = 4000;
|
||||
|
||||
// Phase correlation confidence threshold
|
||||
public double ConfidenceThreshold { get; set; } = 0.15;
|
||||
|
||||
// Stuck detection
|
||||
public double StuckThreshold { get; set; } = 2.0;
|
||||
public int StuckFrameCount { get; set; } = 5;
|
||||
}
|
||||
19
src/Poe2Trade.Navigation/Poe2Trade.Navigation.csproj
Normal file
19
src/Poe2Trade.Navigation/Poe2Trade.Navigation.csproj
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenCvSharp4" Version="4.11.0.*" />
|
||||
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.11.0.*" />
|
||||
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.*" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.12" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Poe2Trade.Core\Poe2Trade.Core.csproj" />
|
||||
<ProjectReference Include="..\Poe2Trade.Game\Poe2Trade.Game.csproj" />
|
||||
<ProjectReference Include="..\Poe2Trade.Screen\Poe2Trade.Screen.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
93
src/Poe2Trade.Navigation/PositionTracker.cs
Normal file
93
src/Poe2Trade.Navigation/PositionTracker.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using OpenCvSharp;
|
||||
using Serilog;
|
||||
|
||||
namespace Poe2Trade.Navigation;
|
||||
|
||||
public class PositionTracker : IDisposable
|
||||
{
|
||||
private readonly MinimapConfig _config;
|
||||
private Mat? _prevGray;
|
||||
private Mat? _hanningWindow;
|
||||
private double _worldX;
|
||||
private double _worldY;
|
||||
private int _stuckCounter;
|
||||
|
||||
public MapPosition Position => new(_worldX, _worldY);
|
||||
public bool IsStuck => _stuckCounter >= _config.StuckFrameCount;
|
||||
|
||||
public PositionTracker(MinimapConfig config)
|
||||
{
|
||||
_config = config;
|
||||
_worldX = config.CanvasSize / 2.0;
|
||||
_worldY = config.CanvasSize / 2.0;
|
||||
}
|
||||
|
||||
public MapPosition UpdatePosition(Mat currentGray)
|
||||
{
|
||||
if (_prevGray == null || _hanningWindow == null)
|
||||
{
|
||||
_prevGray = currentGray.Clone();
|
||||
_hanningWindow = new Mat();
|
||||
Cv2.CreateHanningWindow(_hanningWindow, currentGray.Size(), MatType.CV_64F);
|
||||
return Position;
|
||||
}
|
||||
|
||||
// Convert to float64 for phase correlation
|
||||
using var prev64 = new Mat();
|
||||
using var curr64 = new Mat();
|
||||
_prevGray.ConvertTo(prev64, MatType.CV_64F);
|
||||
currentGray.ConvertTo(curr64, MatType.CV_64F);
|
||||
|
||||
var shift = Cv2.PhaseCorrelate(prev64, curr64, _hanningWindow, out var confidence);
|
||||
|
||||
if (confidence < _config.ConfidenceThreshold)
|
||||
{
|
||||
Log.Debug("Phase correlation low confidence: {Confidence:F3}", confidence);
|
||||
_stuckCounter++;
|
||||
_prevGray.Dispose();
|
||||
_prevGray = currentGray.Clone();
|
||||
return Position;
|
||||
}
|
||||
|
||||
// Negate: minimap scrolls opposite to player movement
|
||||
var dx = -shift.X;
|
||||
var dy = -shift.Y;
|
||||
var displacement = Math.Sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (displacement < _config.StuckThreshold)
|
||||
{
|
||||
_stuckCounter++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_stuckCounter = 0;
|
||||
_worldX += dx;
|
||||
_worldY += dy;
|
||||
}
|
||||
|
||||
Log.Debug("Position: ({X:F1}, {Y:F1}) dx={Dx:F1} dy={Dy:F1} conf={Conf:F3} stuck={Stuck}",
|
||||
_worldX, _worldY, dx, dy, confidence, _stuckCounter);
|
||||
|
||||
_prevGray.Dispose();
|
||||
_prevGray = currentGray.Clone();
|
||||
return Position;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_prevGray?.Dispose();
|
||||
_prevGray = null;
|
||||
_hanningWindow?.Dispose();
|
||||
_hanningWindow = null;
|
||||
_worldX = _config.CanvasSize / 2.0;
|
||||
_worldY = _config.CanvasSize / 2.0;
|
||||
_stuckCounter = 0;
|
||||
Log.Information("Position tracker reset");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_prevGray?.Dispose();
|
||||
_hanningWindow?.Dispose();
|
||||
}
|
||||
}
|
||||
158
src/Poe2Trade.Navigation/WorldMap.cs
Normal file
158
src/Poe2Trade.Navigation/WorldMap.cs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
using OpenCvSharp;
|
||||
using Serilog;
|
||||
|
||||
namespace Poe2Trade.Navigation;
|
||||
|
||||
public class WorldMap : IDisposable
|
||||
{
|
||||
private readonly MinimapConfig _config;
|
||||
private readonly Mat _canvas;
|
||||
|
||||
public WorldMap(MinimapConfig config)
|
||||
{
|
||||
_config = config;
|
||||
_canvas = new Mat(config.CanvasSize, config.CanvasSize, MatType.CV_8UC1, Scalar.Black);
|
||||
}
|
||||
|
||||
public void StitchFrame(Mat classifiedMat, MapPosition position)
|
||||
{
|
||||
var halfSize = _config.CaptureSize / 2;
|
||||
var canvasX = (int)Math.Round(position.X) - halfSize;
|
||||
var canvasY = (int)Math.Round(position.Y) - halfSize;
|
||||
|
||||
// Clamp to canvas bounds
|
||||
var srcX = Math.Max(0, -canvasX);
|
||||
var srcY = Math.Max(0, -canvasY);
|
||||
var dstX = Math.Max(0, canvasX);
|
||||
var dstY = Math.Max(0, canvasY);
|
||||
var w = Math.Min(_config.CaptureSize - srcX, _config.CanvasSize - dstX);
|
||||
var h = Math.Min(_config.CaptureSize - srcY, _config.CanvasSize - dstY);
|
||||
|
||||
if (w <= 0 || h <= 0) return;
|
||||
|
||||
var srcRect = new Rect(srcX, srcY, w, h);
|
||||
var dstRect = new Rect(dstX, dstY, w, h);
|
||||
|
||||
var srcRoi = new Mat(classifiedMat, srcRect);
|
||||
var dstRoi = new Mat(_canvas, dstRect);
|
||||
|
||||
// Only paste non-Unknown pixels; don't overwrite Explored with Wall
|
||||
for (var row = 0; row < h; row++)
|
||||
{
|
||||
for (var col = 0; col < w; col++)
|
||||
{
|
||||
var srcVal = srcRoi.At<byte>(row, col);
|
||||
if (srcVal == (byte)MapCell.Unknown) continue;
|
||||
|
||||
var dstVal = dstRoi.At<byte>(row, col);
|
||||
// Don't overwrite Explored with Wall (Explored is more reliable)
|
||||
if (dstVal == (byte)MapCell.Explored && srcVal == (byte)MapCell.Wall)
|
||||
continue;
|
||||
|
||||
dstRoi.Set(row, col, srcVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public (double dirX, double dirY)? FindNearestUnexplored(MapPosition pos, int searchRadius = 200)
|
||||
{
|
||||
var cx = (int)Math.Round(pos.X);
|
||||
var cy = (int)Math.Round(pos.Y);
|
||||
|
||||
// Scan in angular sectors to find direction with most Unknown cells at frontier
|
||||
var bestAngle = double.NaN;
|
||||
var bestScore = 0;
|
||||
const int sectorCount = 16;
|
||||
var fogRadius = _config.FogRadius;
|
||||
|
||||
for (var sector = 0; sector < sectorCount; sector++)
|
||||
{
|
||||
var angle = 2 * Math.PI * sector / sectorCount;
|
||||
var score = 0;
|
||||
|
||||
// Sample along a cone in this direction, at the fog boundary and beyond
|
||||
for (var r = fogRadius - 20; r <= fogRadius + searchRadius; r += 5)
|
||||
{
|
||||
for (var spread = -15; spread <= 15; spread += 5)
|
||||
{
|
||||
var sampleAngle = angle + spread * Math.PI / 180;
|
||||
var sx = cx + (int)(r * Math.Cos(sampleAngle));
|
||||
var sy = cy + (int)(r * Math.Sin(sampleAngle));
|
||||
|
||||
if (sx < 0 || sx >= _config.CanvasSize || sy < 0 || sy >= _config.CanvasSize)
|
||||
continue;
|
||||
|
||||
if (_canvas.At<byte>(sy, sx) == (byte)MapCell.Unknown)
|
||||
score++;
|
||||
}
|
||||
}
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestAngle = angle;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestScore == 0 || double.IsNaN(bestAngle))
|
||||
{
|
||||
Log.Information("No unexplored area found within search radius");
|
||||
return null;
|
||||
}
|
||||
|
||||
var dirX = Math.Cos(bestAngle);
|
||||
var dirY = Math.Sin(bestAngle);
|
||||
Log.Debug("Best exploration direction: angle={Angle:F1}deg score={Score}",
|
||||
bestAngle * 180 / Math.PI, bestScore);
|
||||
return (dirX, dirY);
|
||||
}
|
||||
|
||||
public byte[] GetMapSnapshot()
|
||||
{
|
||||
Cv2.ImEncode(".png", _canvas, out var buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
public byte[] GetViewportSnapshot(MapPosition center, int viewSize = 400)
|
||||
{
|
||||
var cx = (int)Math.Round(center.X);
|
||||
var cy = (int)Math.Round(center.Y);
|
||||
var half = viewSize / 2;
|
||||
|
||||
// Clamp viewport to canvas
|
||||
var x0 = Math.Clamp(cx - half, 0, _config.CanvasSize - viewSize);
|
||||
var y0 = Math.Clamp(cy - half, 0, _config.CanvasSize - viewSize);
|
||||
var roi = new Mat(_canvas, new Rect(x0, y0, viewSize, viewSize));
|
||||
|
||||
// Colorize: Unknown=#0d1117, Explored=#1f4068, Wall=#3d2d1a
|
||||
using var colored = new Mat(viewSize, viewSize, MatType.CV_8UC3, new Scalar(23, 17, 13)); // BGR #0d1117
|
||||
for (var r = 0; r < viewSize; r++)
|
||||
for (var c = 0; c < viewSize; c++)
|
||||
{
|
||||
var v = roi.At<byte>(r, c);
|
||||
if (v == (byte)MapCell.Explored)
|
||||
colored.Set(r, c, new Vec3b(104, 64, 31)); // BGR #1f4068
|
||||
else if (v == (byte)MapCell.Wall)
|
||||
colored.Set(r, c, new Vec3b(26, 45, 61)); // BGR #3d2d1a
|
||||
}
|
||||
|
||||
// Draw player dot
|
||||
var px = cx - x0;
|
||||
var py = cy - y0;
|
||||
if (px >= 0 && px < viewSize && py >= 0 && py < viewSize)
|
||||
Cv2.Circle(colored, new Point(px, py), 4, new Scalar(0, 140, 255), -1); // orange
|
||||
|
||||
Cv2.ImEncode(".png", colored, out var buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_canvas.SetTo(Scalar.Black);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_canvas.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -14,5 +14,6 @@
|
|||
<conv:ActiveOpacityConverter x:Key="ActiveOpacity" />
|
||||
<conv:CellBorderConverter x:Key="CellBorderConverter" />
|
||||
<conv:BoolToOverlayBrushConverter x:Key="OccupiedOverlayBrush" />
|
||||
<conv:MapRequirementsConverter x:Key="MapRequirementsText" />
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ public partial class App : Application
|
|||
services.AddSingleton<MainWindowViewModel>();
|
||||
services.AddSingleton<DebugViewModel>();
|
||||
services.AddSingleton<SettingsViewModel>();
|
||||
services.AddSingleton<MappingViewModel>();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
|
|
@ -58,6 +59,7 @@ public partial class App : Application
|
|||
var mainVm = provider.GetRequiredService<MainWindowViewModel>();
|
||||
mainVm.DebugVm = provider.GetRequiredService<DebugViewModel>();
|
||||
mainVm.SettingsVm = provider.GetRequiredService<SettingsViewModel>();
|
||||
mainVm.MappingVm = provider.GetRequiredService<MappingViewModel>();
|
||||
|
||||
var window = new MainWindow { DataContext = mainVm };
|
||||
window.SetConfigStore(store);
|
||||
|
|
|
|||
|
|
@ -94,6 +94,23 @@ public class BoolToOverlayBrushConverter : IValueConverter
|
|||
=> throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public class MapRequirementsConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
MapType.TrialOfChaos => "Trial Token x1",
|
||||
MapType.Temple => "Identity Scroll x20",
|
||||
MapType.Endgame => "Identity Scroll x20",
|
||||
_ => "",
|
||||
};
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public class CellBorderConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||
using CommunityToolkit.Mvvm.Input;
|
||||
using Poe2Trade.Bot;
|
||||
using Poe2Trade.Core;
|
||||
using Poe2Trade.Navigation;
|
||||
using Serilog;
|
||||
|
||||
namespace Poe2Trade.Ui.ViewModels;
|
||||
|
|
@ -42,6 +43,9 @@ public partial class MainWindowViewModel : ObservableObject
|
|||
private bool _isStarted;
|
||||
|
||||
[ObservableProperty] private Bitmap? _inventoryImage;
|
||||
[ObservableProperty] private Bitmap? _minimapImage;
|
||||
[ObservableProperty] private string _navigationStateText = "";
|
||||
private long _lastMinimapUpdate;
|
||||
|
||||
[ObservableProperty] private string _newUrl = "";
|
||||
[ObservableProperty] private string _newLinkName = "";
|
||||
|
|
@ -49,13 +53,16 @@ public partial class MainWindowViewModel : ObservableObject
|
|||
[ObservableProperty] private int _tradesCompleted;
|
||||
[ObservableProperty] private int _tradesFailed;
|
||||
[ObservableProperty] private int _activeLinksCount;
|
||||
[ObservableProperty] private BotMode _botMode;
|
||||
|
||||
public static LinkMode[] LinkModes { get; } = [LinkMode.Live, LinkMode.Scrap];
|
||||
public static BotMode[] BotModes { get; } = [BotMode.Trading, BotMode.Mapping];
|
||||
|
||||
public MainWindowViewModel(BotOrchestrator bot)
|
||||
{
|
||||
_bot = bot;
|
||||
_isPaused = bot.IsPaused;
|
||||
_botMode = bot.Mode;
|
||||
|
||||
for (var i = 0; i < 60; i++)
|
||||
InventoryCells.Add(new CellState());
|
||||
|
|
@ -66,12 +73,14 @@ public partial class MainWindowViewModel : ObservableObject
|
|||
{
|
||||
State = bot.State;
|
||||
IsPaused = bot.IsPaused;
|
||||
BotMode = bot.Mode;
|
||||
var status = bot.GetStatus();
|
||||
TradesCompleted = status.TradesCompleted;
|
||||
TradesFailed = status.TradesFailed;
|
||||
ActiveLinksCount = status.Links.Count(l => l.Active);
|
||||
OnPropertyChanged(nameof(Links));
|
||||
UpdateInventoryGrid();
|
||||
UpdateMinimapImage();
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -99,6 +108,12 @@ public partial class MainWindowViewModel : ObservableObject
|
|||
// Sub-ViewModels for tabs
|
||||
public DebugViewModel? DebugVm { get; set; }
|
||||
public SettingsViewModel? SettingsVm { get; set; }
|
||||
public MappingViewModel? MappingVm { get; set; }
|
||||
|
||||
partial void OnBotModeChanged(BotMode value)
|
||||
{
|
||||
_bot.Mode = value;
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanStart))]
|
||||
private async Task Start()
|
||||
|
|
@ -198,4 +213,39 @@ public partial class MainWindowViewModel : ObservableObject
|
|||
|
||||
OnPropertyChanged(nameof(InventoryFreeCells));
|
||||
}
|
||||
|
||||
private void UpdateMinimapImage()
|
||||
{
|
||||
var nav = _bot.Navigation;
|
||||
var navState = nav.State;
|
||||
NavigationStateText = navState == NavigationState.Idle ? "" : navState.ToString();
|
||||
|
||||
if (navState == NavigationState.Idle)
|
||||
{
|
||||
if (MinimapImage != null)
|
||||
{
|
||||
var old = MinimapImage;
|
||||
MinimapImage = null;
|
||||
old.Dispose();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Throttle: update at most once per second
|
||||
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
if (now - _lastMinimapUpdate < 1000) return;
|
||||
_lastMinimapUpdate = now;
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = nav.GetViewportSnapshot();
|
||||
var old = MinimapImage;
|
||||
MinimapImage = new Bitmap(new MemoryStream(bytes));
|
||||
old?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Failed to update minimap image");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
src/Poe2Trade.Ui/ViewModels/MappingViewModel.cs
Normal file
25
src/Poe2Trade.Ui/ViewModels/MappingViewModel.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Poe2Trade.Bot;
|
||||
using Poe2Trade.Core;
|
||||
|
||||
namespace Poe2Trade.Ui.ViewModels;
|
||||
|
||||
public partial class MappingViewModel : ObservableObject
|
||||
{
|
||||
private readonly BotOrchestrator _bot;
|
||||
|
||||
[ObservableProperty] private MapType _selectedMapType;
|
||||
|
||||
public static MapType[] MapTypes { get; } = [MapType.TrialOfChaos, MapType.Temple, MapType.Endgame];
|
||||
|
||||
public MappingViewModel(BotOrchestrator bot)
|
||||
{
|
||||
_bot = bot;
|
||||
_selectedMapType = bot.Config.MapType;
|
||||
}
|
||||
|
||||
partial void OnSelectedMapTypeChanged(MapType value)
|
||||
{
|
||||
_bot.Store.UpdateSettings(s => s.MapType = value);
|
||||
}
|
||||
}
|
||||
|
|
@ -56,7 +56,10 @@
|
|||
</StackPanel>
|
||||
|
||||
<!-- Controls -->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<ComboBox ItemsSource="{x:Static vm:MainWindowViewModel.BotModes}"
|
||||
SelectedItem="{Binding BotMode}" Width="110" />
|
||||
<Button Content="Start" Command="{Binding StartCommand}" />
|
||||
<Button Content="{Binding PauseButtonText}" Command="{Binding PauseCommand}" />
|
||||
</StackPanel>
|
||||
|
|
@ -70,36 +73,61 @@
|
|||
<TabItem Header="State">
|
||||
<Grid RowDefinitions="Auto,*" Margin="0,6,0,0">
|
||||
|
||||
<!-- Inventory Grid (12x5) -->
|
||||
<Border Grid.Row="0" Background="#161b22" BorderBrush="#30363d"
|
||||
BorderThickness="1" CornerRadius="8" Padding="8" Margin="0,0,0,6">
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,4">
|
||||
<TextBlock Text="INVENTORY" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="#8b949e" />
|
||||
<TextBlock Text="{Binding InventoryFreeCells, StringFormat='{}{0}/60 free'}"
|
||||
FontSize="11" Foreground="#8b949e" Margin="12,0,0,0" />
|
||||
</StackPanel>
|
||||
<Grid MaxHeight="170">
|
||||
<Image Source="{Binding InventoryImage}" Stretch="Uniform" />
|
||||
<ItemsControl ItemsSource="{Binding InventoryCells}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="12" Rows="5" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:CellState">
|
||||
<Border Margin="1" CornerRadius="2"
|
||||
Background="{Binding IsOccupied, Converter={StaticResource OccupiedOverlayBrush}}"
|
||||
BorderBrush="#3fb950"
|
||||
BorderThickness="{Binding Converter={StaticResource CellBorderConverter}}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<!-- Top row: Inventory + Minimap side by side -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,6">
|
||||
|
||||
<!-- Inventory Grid (12x5) -->
|
||||
<Border Grid.Column="0" Background="#161b22" BorderBrush="#30363d"
|
||||
BorderThickness="1" CornerRadius="8" Padding="8" Margin="0,0,6,0">
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,4">
|
||||
<TextBlock Text="INVENTORY" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="#8b949e" />
|
||||
<TextBlock Text="{Binding InventoryFreeCells, StringFormat='{}{0}/60 free'}"
|
||||
FontSize="11" Foreground="#8b949e" Margin="12,0,0,0" />
|
||||
</StackPanel>
|
||||
<Grid MaxHeight="170">
|
||||
<Image Source="{Binding InventoryImage}" Stretch="Uniform" />
|
||||
<ItemsControl ItemsSource="{Binding InventoryCells}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<UniformGrid Columns="12" Rows="5" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:CellState">
|
||||
<Border Margin="1" CornerRadius="2"
|
||||
Background="{Binding IsOccupied, Converter={StaticResource OccupiedOverlayBrush}}"
|
||||
BorderBrush="#3fb950"
|
||||
BorderThickness="{Binding Converter={StaticResource CellBorderConverter}}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Minimap -->
|
||||
<Border Grid.Column="1" Background="#161b22" BorderBrush="#30363d"
|
||||
BorderThickness="1" CornerRadius="8" Padding="8" Width="200">
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,4">
|
||||
<TextBlock Text="MINIMAP" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="#8b949e" />
|
||||
<TextBlock Text="{Binding NavigationStateText}"
|
||||
FontSize="11" Foreground="#8b949e" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
<Grid>
|
||||
<Image Source="{Binding MinimapImage}" Stretch="Uniform"
|
||||
RenderOptions.BitmapInterpolationMode="None" />
|
||||
<TextBlock Text="Idle"
|
||||
IsVisible="{Binding MinimapImage, Converter={x:Static ObjectConverters.IsNull}}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
FontSize="12" Foreground="#484f58" />
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Logs -->
|
||||
<Border Grid.Row="1" Background="#0d1117" BorderBrush="#30363d"
|
||||
|
|
@ -178,6 +206,24 @@
|
|||
</Border>
|
||||
</TabItem>
|
||||
|
||||
<!-- ========== MAPPING TAB ========== -->
|
||||
<TabItem Header="Mapping">
|
||||
<Border DataContext="{Binding MappingVm}" Background="#161b22"
|
||||
BorderBrush="#30363d" BorderThickness="1" CornerRadius="8"
|
||||
Padding="10" Margin="0,6,0,0">
|
||||
<StackPanel Spacing="8" x:DataType="vm:MappingViewModel">
|
||||
<TextBlock Text="MAP TYPE" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="#8b949e" />
|
||||
<ComboBox ItemsSource="{x:Static vm:MappingViewModel.MapTypes}"
|
||||
SelectedItem="{Binding SelectedMapType}" Width="200" />
|
||||
<TextBlock Text="REQUIRED ITEMS" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="#8b949e" Margin="0,8,0,0" />
|
||||
<TextBlock Text="{Binding SelectedMapType, Converter={StaticResource MapRequirementsText}}"
|
||||
FontSize="13" Foreground="#e6edf3" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</TabItem>
|
||||
|
||||
<!-- ========== DEBUG TAB ========== -->
|
||||
<TabItem Header="Debug">
|
||||
<ScrollViewer DataContext="{Binding DebugVm}" Margin="0,6,0,0">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue