Explorar o código

Merge pull request #526 from djeada/copilot/fix-minimap-issues

Fix minimap: dead troops cleanup, camera viewport overlay, coordinate projection
Adam Djellouli hai 5 días
pai
achega
fe00538ba7

+ 3 - 0
app/core/game_engine.cpp

@@ -623,6 +623,9 @@ void GameEngine::update(float dt) {
       auto *selection_system =
           m_world->get_system<Game::Systems::SelectionSystem>();
       m_minimap_manager->update_units(m_world.get(), selection_system);
+      m_minimap_manager->update_camera_viewport(
+          m_camera.get(), static_cast<float>(m_viewport.width),
+          static_cast<float>(m_viewport.height));
       emit minimap_image_changed();
     }
   }

+ 74 - 9
app/core/minimap_manager.cpp

@@ -3,14 +3,18 @@
 #include "game/core/component.h"
 #include "game/core/world.h"
 #include "game/map/map_loader.h"
+#include "game/map/minimap/camera_viewport_layer.h"
 #include "game/map/minimap/minimap_generator.h"
+#include "game/map/minimap/minimap_utils.h"
 #include "game/map/minimap/unit_layer.h"
 #include "game/map/visibility_service.h"
 #include "game/systems/selection_system.h"
 #include "game/units/troop_type.h"
+#include "render/gl/camera.h"
 #include <QDebug>
 #include <QPainter>
 #include <algorithm>
+#include <cmath>
 #include <unordered_set>
 
 MinimapManager::MinimapManager() = default;
@@ -28,6 +32,11 @@ void MinimapManager::generate_for_map(const Game::Map::MapDefinition &map_def) {
 
     m_world_width = static_cast<float>(map_def.grid.width);
     m_world_height = static_cast<float>(map_def.grid.height);
+    m_tile_size = map_def.grid.tile_size;
+
+    // Initialize fog image with a copy of the base image
+    m_minimap_fog_image = m_minimap_base_image.copy();
+    m_minimap_image = m_minimap_fog_image.copy();
 
     m_unit_layer = std::make_unique<Game::Map::Minimap::UnitLayer>();
     m_unit_layer->init(m_minimap_base_image.width(),
@@ -36,6 +45,12 @@ void MinimapManager::generate_for_map(const Game::Map::MapDefinition &map_def) {
     qDebug() << "MinimapManager: Initialized unit layer for world"
              << m_world_width << "x" << m_world_height;
 
+    m_camera_viewport_layer =
+        std::make_unique<Game::Map::Minimap::CameraViewportLayer>();
+    m_camera_viewport_layer->init(m_minimap_base_image.width(),
+                                  m_minimap_base_image.height(), m_world_width,
+                                  m_world_height);
+
     m_minimap_fog_version = 0;
     m_minimap_update_timer = MINIMAP_UPDATE_INTERVAL;
     update_fog(0.0F, 1);
@@ -57,14 +72,16 @@ void MinimapManager::update_fog(float dt, int local_owner_id) {
 
   auto &visibility_service = Game::Map::VisibilityService::instance();
   if (!visibility_service.is_initialized()) {
-    if (m_minimap_image != m_minimap_base_image) {
-      m_minimap_image = m_minimap_base_image;
+    if (m_minimap_fog_image.isNull() ||
+        m_minimap_fog_image.size() != m_minimap_base_image.size()) {
+      m_minimap_fog_image = m_minimap_base_image.copy();
     }
     return;
   }
 
   const auto current_version = visibility_service.version();
-  if (current_version == m_minimap_fog_version && !m_minimap_image.isNull()) {
+  if (current_version == m_minimap_fog_version &&
+      !m_minimap_fog_image.isNull()) {
     return;
   }
   m_minimap_fog_version = current_version;
@@ -74,14 +91,14 @@ void MinimapManager::update_fog(float dt, int local_owner_id) {
   const auto cells = visibility_service.snapshotCells();
 
   if (cells.empty() || vis_width <= 0 || vis_height <= 0) {
-    m_minimap_image = m_minimap_base_image;
+    m_minimap_fog_image = m_minimap_base_image.copy();
     return;
   }
 
-  m_minimap_image = m_minimap_base_image.copy();
+  m_minimap_fog_image = m_minimap_base_image.copy();
 
-  const int img_width = m_minimap_image.width();
-  const int img_height = m_minimap_image.height();
+  const int img_width = m_minimap_fog_image.width();
+  const int img_height = m_minimap_fog_image.height();
 
   constexpr float k_inv_cos = -0.70710678118F;
   constexpr float k_inv_sin = 0.70710678118F;
@@ -123,7 +140,7 @@ void MinimapManager::update_fog(float dt, int local_owner_id) {
   const float half_vis_h = static_cast<float>(vis_height) * 0.5F;
 
   for (int y = 0; y < img_height; ++y) {
-    auto *scanline = reinterpret_cast<QRgb *>(m_minimap_image.scanLine(y));
+    auto *scanline = reinterpret_cast<QRgb *>(m_minimap_fog_image.scanLine(y));
 
     for (int x = 0; x < img_width; ++x) {
       const float centered_x = static_cast<float>(x) - half_img_w;
@@ -174,10 +191,14 @@ void MinimapManager::update_fog(float dt, int local_owner_id) {
 void MinimapManager::update_units(
     Engine::Core::World *world,
     Game::Systems::SelectionSystem *selection_system) {
-  if (m_minimap_image.isNull() || !m_unit_layer || !world) {
+  if (m_minimap_fog_image.isNull() || !m_unit_layer || !world) {
     return;
   }
 
+  // Always start with a fresh copy of the fog-processed base image
+  // to prevent overlays (units, camera viewport) from accumulating
+  m_minimap_image = m_minimap_fog_image.copy();
+
   std::vector<Game::Map::Minimap::UnitMarker> markers;
 
   constexpr size_t EXPECTED_MAX_UNITS = 128;
@@ -199,6 +220,11 @@ void MinimapManager::update_units(
         continue;
       }
 
+      // Skip dead units - they should not appear on the minimap
+      if (unit->health <= 0) {
+        continue;
+      }
+
       const auto *transform =
           entity->get_component<Engine::Core::TransformComponent>();
       if (!transform) {
@@ -225,3 +251,42 @@ void MinimapManager::update_units(
     painter.drawImage(0, 0, unit_overlay);
   }
 }
+
+void MinimapManager::update_camera_viewport(const Render::GL::Camera *camera,
+                                            float screen_width,
+                                            float screen_height) {
+  if (m_minimap_image.isNull() || !m_camera_viewport_layer || !camera) {
+    return;
+  }
+
+  // Get camera target position (where the camera is looking)
+  const QVector3D &target = camera->get_target();
+
+  // Estimate the visible world area based on camera distance and FOV
+  const float distance = camera->get_distance();
+  const float fov_rad =
+      camera->get_fov() * Game::Map::Minimap::Constants::k_degrees_to_radians;
+  const float aspect = screen_width / std::max(screen_height, 1.0F);
+
+  // Calculate approximate viewport dimensions in world space
+  // Based on the vertical FOV and camera distance
+  const float viewport_half_height = distance * std::tan(fov_rad * 0.5F);
+  const float viewport_half_width = viewport_half_height * aspect;
+
+  // Convert from world units to tile units for the minimap
+  const float viewport_width = viewport_half_width * 2.0F / m_tile_size;
+  const float viewport_height = viewport_half_height * 2.0F / m_tile_size;
+
+  // Update the camera viewport layer
+  m_camera_viewport_layer->update(target.x() / m_tile_size,
+                                  target.z() / m_tile_size, viewport_width,
+                                  viewport_height);
+
+  // Draw the camera viewport overlay on the minimap
+  const QImage &viewport_overlay = m_camera_viewport_layer->get_image();
+  if (!viewport_overlay.isNull()) {
+    QPainter painter(&m_minimap_image);
+    painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
+    painter.drawImage(0, 0, viewport_overlay);
+  }
+}

+ 10 - 0
app/core/minimap_manager.h

@@ -8,6 +8,7 @@ namespace Game::Map {
 struct MapDefinition;
 namespace Minimap {
 class UnitLayer;
+class CameraViewportLayer;
 }
 } // namespace Game::Map
 
@@ -20,6 +21,10 @@ namespace Game::Systems {
 class SelectionSystem;
 }
 
+namespace Render::GL {
+class Camera;
+}
+
 class MinimapManager {
 public:
   MinimapManager();
@@ -29,6 +34,8 @@ public:
   void update_fog(float dt, int local_owner_id);
   void update_units(Engine::Core::World *world,
                     Game::Systems::SelectionSystem *selection_system);
+  void update_camera_viewport(const Render::GL::Camera *camera,
+                              float screen_width, float screen_height);
 
   [[nodiscard]] const QImage &get_image() const { return m_minimap_image; }
   [[nodiscard]] bool has_minimap() const {
@@ -38,10 +45,13 @@ public:
 private:
   QImage m_minimap_image;
   QImage m_minimap_base_image;
+  QImage m_minimap_fog_image;
   std::uint64_t m_minimap_fog_version = 0;
   std::unique_ptr<Game::Map::Minimap::UnitLayer> m_unit_layer;
+  std::unique_ptr<Game::Map::Minimap::CameraViewportLayer> m_camera_viewport_layer;
   float m_world_width = 0.0F;
   float m_world_height = 0.0F;
+  float m_tile_size = 1.0F;
   float m_minimap_update_timer = 0.0F;
   static constexpr float MINIMAP_UPDATE_INTERVAL = 0.1F;
 };

+ 1 - 0
game/CMakeLists.txt

@@ -79,6 +79,7 @@ add_library(game_systems STATIC
     map/minimap/fog_of_war_mask.cpp
     map/minimap/minimap_manager.cpp
     map/minimap/unit_layer.cpp
+    map/minimap/camera_viewport_layer.cpp
     visuals/visual_catalog.cpp
     units/unit.cpp
     units/archer.cpp

+ 126 - 0
game/map/minimap/camera_viewport_layer.cpp

@@ -0,0 +1,126 @@
+#include "camera_viewport_layer.h"
+#include "minimap_utils.h"
+
+#include <QPainter>
+#include <QPen>
+#include <algorithm>
+#include <cmath>
+
+namespace Game::Map::Minimap {
+
+namespace {
+constexpr float k_corner_size_ratio = 0.15F;
+constexpr float k_min_corner_size = 4.0F;
+constexpr float k_corner_pen_offset = 1.0F;
+} // namespace
+
+void CameraViewportLayer::init(int width, int height, float world_width,
+                               float world_height) {
+  m_width = width;
+  m_height = height;
+  m_world_width = world_width;
+  m_world_height = world_height;
+
+  m_scale_x = static_cast<float>(width - 1) / world_width;
+  m_scale_y = static_cast<float>(height - 1) / world_height;
+  m_offset_x = world_width * 0.5F;
+  m_offset_y = world_height * 0.5F;
+
+  m_image = QImage(width, height, QImage::Format_ARGB32);
+  m_image.fill(Qt::transparent);
+}
+
+auto CameraViewportLayer::world_to_pixel(float world_x, float world_z) const
+    -> std::pair<float, float> {
+
+  const float rotated_x = world_x * Constants::k_camera_yaw_cos -
+                          world_z * Constants::k_camera_yaw_sin;
+  const float rotated_z = world_x * Constants::k_camera_yaw_sin +
+                          world_z * Constants::k_camera_yaw_cos;
+
+  const float px = (rotated_x + m_offset_x) * m_scale_x;
+  const float py = (rotated_z + m_offset_y) * m_scale_y;
+
+  return {px, py};
+}
+
+void CameraViewportLayer::update(float camera_x, float camera_z,
+                                 float viewport_width, float viewport_height) {
+  if (m_image.isNull()) {
+    return;
+  }
+
+  m_image.fill(Qt::transparent);
+
+  if (viewport_width <= 0.0F || viewport_height <= 0.0F) {
+    return;
+  }
+
+  QPainter painter(&m_image);
+  painter.setRenderHint(QPainter::Antialiasing, true);
+
+  // Convert camera center position to pixel coordinates
+  const auto [px, py] = world_to_pixel(camera_x, camera_z);
+
+  // Convert viewport dimensions to pixel space
+  const float pixel_width = viewport_width * m_scale_x;
+  const float pixel_height = viewport_height * m_scale_y;
+
+  draw_viewport_rect(painter, px, py, pixel_width, pixel_height);
+}
+
+void CameraViewportLayer::draw_viewport_rect(QPainter &painter, float px,
+                                             float py, float pixel_width,
+                                             float pixel_height) {
+  // Calculate the rectangle bounds centered at the camera position
+  const float half_w = pixel_width * 0.5F;
+  const float half_h = pixel_height * 0.5F;
+
+  QRectF rect(static_cast<qreal>(px - half_w), static_cast<qreal>(py - half_h),
+              static_cast<qreal>(pixel_width),
+              static_cast<qreal>(pixel_height));
+
+  // Draw a semi-transparent border rectangle
+  QColor border_color(m_border_r, m_border_g, m_border_b, m_border_a);
+  QPen pen(border_color);
+  pen.setWidthF(static_cast<qreal>(m_border_width));
+  painter.setPen(pen);
+  painter.setBrush(Qt::NoBrush);
+  painter.drawRect(rect);
+
+  // Draw corner markers for better visibility
+  const float corner_size =
+      std::min(pixel_width, pixel_height) * k_corner_size_ratio;
+  const float actual_corner = std::max(corner_size, k_min_corner_size);
+
+  QColor corner_color(m_border_r, m_border_g, m_border_b, 255);
+  QPen corner_pen(corner_color);
+  corner_pen.setWidthF(static_cast<qreal>(m_border_width) + k_corner_pen_offset);
+  painter.setPen(corner_pen);
+
+  // Top-left corner
+  painter.drawLine(QPointF(rect.left(), rect.top()),
+                   QPointF(rect.left() + actual_corner, rect.top()));
+  painter.drawLine(QPointF(rect.left(), rect.top()),
+                   QPointF(rect.left(), rect.top() + actual_corner));
+
+  // Top-right corner
+  painter.drawLine(QPointF(rect.right(), rect.top()),
+                   QPointF(rect.right() - actual_corner, rect.top()));
+  painter.drawLine(QPointF(rect.right(), rect.top()),
+                   QPointF(rect.right(), rect.top() + actual_corner));
+
+  // Bottom-left corner
+  painter.drawLine(QPointF(rect.left(), rect.bottom()),
+                   QPointF(rect.left() + actual_corner, rect.bottom()));
+  painter.drawLine(QPointF(rect.left(), rect.bottom()),
+                   QPointF(rect.left(), rect.bottom() - actual_corner));
+
+  // Bottom-right corner
+  painter.drawLine(QPointF(rect.right(), rect.bottom()),
+                   QPointF(rect.right() - actual_corner, rect.bottom()));
+  painter.drawLine(QPointF(rect.right(), rect.bottom()),
+                   QPointF(rect.right(), rect.bottom() - actual_corner));
+}
+
+} // namespace Game::Map::Minimap

+ 60 - 0
game/map/minimap/camera_viewport_layer.h

@@ -0,0 +1,60 @@
+#pragma once
+
+#include <QImage>
+#include <QRectF>
+#include <cstdint>
+
+class QPainter;
+
+namespace Game::Map::Minimap {
+
+class CameraViewportLayer {
+public:
+  CameraViewportLayer() = default;
+
+  void init(int width, int height, float world_width, float world_height);
+
+  [[nodiscard]] auto is_initialized() const -> bool {
+    return !m_image.isNull();
+  }
+
+  void update(float camera_x, float camera_z, float viewport_width,
+              float viewport_height);
+
+  [[nodiscard]] auto get_image() const -> const QImage & { return m_image; }
+
+  void set_border_width(float width) { m_border_width = width; }
+  void set_border_color(std::uint8_t r, std::uint8_t g, std::uint8_t b,
+                        std::uint8_t a = 200) {
+    m_border_r = r;
+    m_border_g = g;
+    m_border_b = b;
+    m_border_a = a;
+  }
+
+private:
+  [[nodiscard]] auto
+  world_to_pixel(float world_x, float world_z) const -> std::pair<float, float>;
+
+  void draw_viewport_rect(QPainter &painter, float px, float py,
+                          float pixel_width, float pixel_height);
+
+  QImage m_image;
+  int m_width = 0;
+  int m_height = 0;
+  float m_world_width = 0.0F;
+  float m_world_height = 0.0F;
+
+  float m_scale_x = 1.0F;
+  float m_scale_y = 1.0F;
+  float m_offset_x = 0.0F;
+  float m_offset_y = 0.0F;
+
+  float m_border_width = 2.0F;
+  std::uint8_t m_border_r = 255;
+  std::uint8_t m_border_g = 255;
+  std::uint8_t m_border_b = 255;
+  std::uint8_t m_border_a = 200;
+};
+
+} // namespace Game::Map::Minimap

+ 8 - 8
game/map/minimap/map_preview_generator.cpp

@@ -2,6 +2,7 @@
 #include "../../units/spawn_type.h"
 #include "../map_loader.h"
 #include "minimap_generator.h"
+#include "minimap_utils.h"
 #include <QColor>
 #include <QFile>
 #include <QJsonDocument>
@@ -15,9 +16,6 @@ namespace Game::Map::Minimap {
 
 namespace {
 
-constexpr float k_camera_yaw_cos = -0.70710678118F;
-constexpr float k_camera_yaw_sin = -0.70710678118F;
-
 constexpr float BASE_SIZE = 16.0F;
 constexpr float INNER_SIZE_RATIO = 0.35F;
 constexpr float INNER_OFFSET_RATIO = 0.3F;
@@ -112,8 +110,10 @@ void MapPreviewGenerator::draw_player_bases(
       continue;
     }
 
+    const auto [world_x, world_z] =
+        grid_to_world_coords(spawn.x, spawn.z, map_def);
     const auto [px, py] =
-        world_to_pixel(spawn.x, spawn.z, map_def.grid, pixels_per_tile);
+        world_to_pixel(world_x, world_z, map_def.grid, pixels_per_tile);
 
     constexpr float HALF = BASE_SIZE * 0.5F;
 
@@ -136,10 +136,10 @@ auto MapPreviewGenerator::world_to_pixel(
     float world_x, float world_z, const GridDefinition &grid,
     float pixels_per_tile) const -> std::pair<float, float> {
 
-  const float rotated_x =
-      world_x * k_camera_yaw_cos - world_z * k_camera_yaw_sin;
-  const float rotated_z =
-      world_x * k_camera_yaw_sin + world_z * k_camera_yaw_cos;
+  const float rotated_x = world_x * Constants::k_camera_yaw_cos -
+                          world_z * Constants::k_camera_yaw_sin;
+  const float rotated_z = world_x * Constants::k_camera_yaw_sin +
+                          world_z * Constants::k_camera_yaw_cos;
 
   const float world_width = grid.width * grid.tile_size;
   const float world_height = grid.height * grid.tile_size;

+ 8 - 8
game/map/minimap/minimap_generator.cpp

@@ -1,4 +1,5 @@
 #include "minimap_generator.h"
+#include "minimap_utils.h"
 #include <QColor>
 #include <QLinearGradient>
 #include <QPainter>
@@ -13,9 +14,6 @@ namespace Game::Map::Minimap {
 
 namespace {
 
-constexpr float k_camera_yaw_cos = -0.70710678118F;
-constexpr float k_camera_yaw_sin = -0.70710678118F;
-
 namespace Palette {
 
 constexpr QColor PARCHMENT_BASE{235, 220, 190};
@@ -91,10 +89,10 @@ auto MinimapGenerator::world_to_pixel(float world_x, float world_z,
                                       const GridDefinition &grid) const
     -> std::pair<float, float> {
 
-  const float rotated_x =
-      world_x * k_camera_yaw_cos - world_z * k_camera_yaw_sin;
-  const float rotated_z =
-      world_x * k_camera_yaw_sin + world_z * k_camera_yaw_cos;
+  const float rotated_x = world_x * Constants::k_camera_yaw_cos -
+                          world_z * Constants::k_camera_yaw_sin;
+  const float rotated_z = world_x * Constants::k_camera_yaw_sin +
+                          world_z * Constants::k_camera_yaw_cos;
 
   const float world_width = grid.width * grid.tile_size;
   const float world_height = grid.height * grid.tile_size;
@@ -459,7 +457,9 @@ void MinimapGenerator::render_structures(QImage &image,
       continue;
     }
 
-    const auto [px, py] = world_to_pixel(spawn.x, spawn.z, map_def.grid);
+    const auto [world_x, world_z] =
+        grid_to_world_coords(spawn.x, spawn.z, map_def);
+    const auto [px, py] = world_to_pixel(world_x, world_z, map_def.grid);
 
     QColor fill_color = Palette::STRUCTURE_STONE;
     QColor border_color = Palette::STRUCTURE_SHADOW;

+ 34 - 0
game/map/minimap/minimap_utils.h

@@ -0,0 +1,34 @@
+#pragma once
+
+#include "../map_definition.h"
+#include <algorithm>
+#include <cmath>
+#include <utility>
+
+namespace Game::Map::Minimap {
+
+namespace Constants {
+
+constexpr float k_camera_yaw_cos = -0.70710678118F;
+constexpr float k_camera_yaw_sin = -0.70710678118F;
+constexpr float k_min_tile_size = 0.0001F;
+constexpr float k_degrees_to_radians = 3.14159265358979323846F / 180.0F;
+
+} // namespace Constants
+
+inline auto grid_to_world_coords(float grid_x, float grid_z,
+                                 const MapDefinition &map_def)
+    -> std::pair<float, float> {
+  float world_x = grid_x;
+  float world_z = grid_z;
+
+  if (map_def.coordSystem == CoordSystem::Grid) {
+    const float tile = std::max(Constants::k_min_tile_size, map_def.grid.tile_size);
+    world_x = (grid_x - (map_def.grid.width * 0.5F - 0.5F)) * tile;
+    world_z = (grid_z - (map_def.grid.height * 0.5F - 0.5F)) * tile;
+  }
+
+  return {world_x, world_z};
+}
+
+} // namespace Game::Map::Minimap

+ 5 - 9
game/map/minimap/unit_layer.cpp

@@ -1,4 +1,5 @@
 #include "unit_layer.h"
+#include "minimap_utils.h"
 
 #include <QPainter>
 #include <algorithm>
@@ -6,11 +7,6 @@
 
 namespace Game::Map::Minimap {
 
-namespace {
-constexpr float k_camera_yaw_cos = -0.70710678118F;
-constexpr float k_camera_yaw_sin = -0.70710678118F;
-} // namespace
-
 void UnitLayer::init(int width, int height, float world_width,
                      float world_height) {
   m_width = width;
@@ -30,10 +26,10 @@ void UnitLayer::init(int width, int height, float world_width,
 auto UnitLayer::world_to_pixel(float world_x,
                                float world_z) const -> std::pair<float, float> {
 
-  const float rotated_x =
-      world_x * k_camera_yaw_cos - world_z * k_camera_yaw_sin;
-  const float rotated_z =
-      world_x * k_camera_yaw_sin + world_z * k_camera_yaw_cos;
+  const float rotated_x = world_x * Constants::k_camera_yaw_cos -
+                          world_z * Constants::k_camera_yaw_sin;
+  const float rotated_z = world_x * Constants::k_camera_yaw_sin +
+                          world_z * Constants::k_camera_yaw_cos;
 
   const float px = (rotated_x + m_offset_x) * m_scale_x;
   const float py = (rotated_z + m_offset_y) * m_scale_y;