diff --git a/src/Poe2Trade.Navigation/MinimapCapture.cs b/src/Poe2Trade.Navigation/MinimapCapture.cs index 07685f0..d756c61 100644 --- a/src/Poe2Trade.Navigation/MinimapCapture.cs +++ b/src/Poe2Trade.Navigation/MinimapCapture.cs @@ -66,8 +66,12 @@ public class MinimapCapture : IDisposable // Wall mask: target #A2AEE5 blue-lavender structure lines (range adapts per-map) var wallMask = BuildWallMask(hsv, playerMask, sample: true); - // Build classified mat (walls only — for stitching) + // Fog of war: broad blue range minus walls minus player + using var fogMask = BuildFogMask(hsv, wallMask, playerMask); + + // Build classified mat (fog first, walls override) var classified = new Mat(_config.CaptureSize, _config.CaptureSize, MatType.CV_8UC1, Scalar.Black); + classified.SetTo(new Scalar((byte)MapCell.Fog), fogMask); classified.SetTo(new Scalar((byte)MapCell.Wall), wallMask); // Gray for correlation tracking (player zeroed) @@ -110,6 +114,26 @@ public class MinimapCapture : IDisposable return wallMask; } + private Mat BuildFogMask(Mat hsv, Mat wallMask, Mat playerMask) + { + // Broad blue detection (captures walls + fog + any blue) + using var allBlue = new Mat(); + Cv2.InRange(hsv, _config.FogLoHSV, _config.FogHiHSV, allBlue); + + // Subtract player and walls → remaining blue is fog + using var notPlayer = new Mat(); + Cv2.BitwiseNot(playerMask, notPlayer); + Cv2.BitwiseAnd(allBlue, notPlayer, allBlue); + + var fogMask = new Mat(); + using var notWalls = new Mat(); + Cv2.BitwiseNot(wallMask, notWalls); + Cv2.BitwiseAnd(allBlue, notWalls, fogMask); + + FilterSmallComponents(fogMask, _config.FogMinArea); + return fogMask; + } + private static void FilterSmallComponents(Mat mask, int minArea) { if (minArea <= 0) return; @@ -164,10 +188,14 @@ public class MinimapCapture : IDisposable using var wallMask = BuildWallMask(hsv, playerMask); if (stage == MinimapDebugStage.Walls) return EncodePng(wallMask); - // Classified (walls + player only — explored is tracked by WorldMap) + using var fogMask = BuildFogMask(hsv, wallMask, playerMask); + if (stage == MinimapDebugStage.Fog) return EncodePng(fogMask); + + // Classified (walls + fog + player — explored is tracked by WorldMap) using var classified = new Mat(_config.CaptureSize, _config.CaptureSize, MatType.CV_8UC3, Scalar.Black); - classified.SetTo(new Scalar(26, 45, 61), wallMask); - classified.SetTo(new Scalar(0, 165, 255), playerMask); + classified.SetTo(new Scalar(180, 140, 70), fogMask); // light blue for fog + classified.SetTo(new Scalar(26, 45, 61), wallMask); // brown for walls + classified.SetTo(new Scalar(0, 165, 255), playerMask); // orange for player return EncodePng(classified); } diff --git a/src/Poe2Trade.Navigation/NavigationTypes.cs b/src/Poe2Trade.Navigation/NavigationTypes.cs index f6d2cba..5b1fcf5 100644 --- a/src/Poe2Trade.Navigation/NavigationTypes.cs +++ b/src/Poe2Trade.Navigation/NavigationTypes.cs @@ -21,7 +21,8 @@ public enum MapCell : byte { Unknown = 0, Explored = 1, - Wall = 2 + Wall = 2, + Fog = 3 } public enum MinimapDebugStage @@ -29,6 +30,7 @@ public enum MinimapDebugStage WorldMap, Raw, Walls, + Fog, Player, Classified, Hue, @@ -78,8 +80,13 @@ public class MinimapConfig // Connected components: minimum area to keep (kills speckle) public int WallMinArea { get; set; } = 30; + // Fog of war detection: broad blue range, fog = allBlue minus walls + public Scalar FogLoHSV { get; set; } = new(85, 10, 130); + public Scalar FogHiHSV { get; set; } = new(140, 255, 255); + public int FogMinArea { get; set; } = 100; + // Explored radius: pixels around player position to mark as explored on world map - public int ExploredRadius { get; set; } = 120; + public int ExploredRadius { get; set; } = 75; // Temporal smoothing: majority vote over ring buffer (walls only) public int TemporalFrameCount { get; set; } = 5; diff --git a/src/Poe2Trade.Navigation/WorldMap.cs b/src/Poe2Trade.Navigation/WorldMap.cs index a6dff7b..bfb8271 100644 --- a/src/Poe2Trade.Navigation/WorldMap.cs +++ b/src/Poe2Trade.Navigation/WorldMap.cs @@ -241,7 +241,16 @@ public class WorldMap : IDisposable dstRoi.Set(row, col, (byte)MapCell.Explored); // lost confidence → demote } - // Mark explored area: circle around player, only overwrite Unknown + // Mark fog on canvas: only overwrite Unknown (fog is soft data) + for (var row = 0; row < h; row++) + for (var col = 0; col < w; col++) + { + if (srcRoi.At(row, col) != (byte)MapCell.Fog) continue; + if (dstRoi.At(row, col) == (byte)MapCell.Unknown) + dstRoi.Set(row, col, (byte)MapCell.Fog); + } + + // Mark explored area: circle around player, overwrite Unknown and Fog var pcx = (int)Math.Round(position.X); var pcy = (int)Math.Round(position.Y); var r = _config.ExploredRadius; @@ -258,7 +267,8 @@ public class WorldMap : IDisposable var dx = x - pcx; var dy = y - pcy; if (dx * dx + dy * dy > r2) continue; - if (_canvas.At(y, x) == (byte)MapCell.Unknown) + var cell = _canvas.At(y, x); + if (cell == (byte)MapCell.Unknown || cell == (byte)MapCell.Fog) _canvas.Set(y, x, (byte)MapCell.Explored); } } @@ -329,7 +339,10 @@ public class WorldMap : IDisposable if (sx < 0 || sx >= _config.CanvasSize || sy < 0 || sy >= _config.CanvasSize) continue; - if (_canvas.At(sy, sx) == (byte)MapCell.Unknown) + var cell = _canvas.At(sy, sx); + if (cell == (byte)MapCell.Fog) + score += 2; // prefer visible fog (we know there's unexplored area) + else if (cell == (byte)MapCell.Unknown) score++; } } @@ -379,6 +392,8 @@ public class WorldMap : IDisposable colored.Set(r, c, new Vec3b(104, 64, 31)); else if (v == (byte)MapCell.Wall) colored.Set(r, c, new Vec3b(26, 45, 61)); + else if (v == (byte)MapCell.Fog) + colored.Set(r, c, new Vec3b(180, 140, 70)); // light blue } var px = cx - x0;