Browse Source

Fix minimap issues: dead troops cleanup, camera viewport overlay, and coordinate projection

Co-authored-by: djeada <[email protected]>
copilot-swe-agent[bot] 1 week ago
parent
commit
3286741c3c

+ 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();
     }
   }

+ 54 - 0
app/core/minimap_manager.cpp

@@ -3,14 +3,17 @@
 #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/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 +31,7 @@ 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;
 
     m_unit_layer = std::make_unique<Game::Map::Minimap::UnitLayer>();
     m_unit_layer->init(m_minimap_base_image.width(),
@@ -36,6 +40,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);
@@ -199,6 +209,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 +240,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 =
+      static_cast<float>(camera->get_fov() * M_PI / 180.0);
+  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);
+  }
+}

+ 9 - 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 {
@@ -40,8 +47,10 @@ private:
   QImage m_minimap_base_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

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

@@ -0,0 +1,124 @@
+#include "camera_viewport_layer.h"
+
+#include <QPainter>
+#include <QPen>
+#include <algorithm>
+#include <cmath>
+
+namespace Game::Map::Minimap {
+
+namespace {
+constexpr float k_camera_yaw_cos = -0.70710678118F;
+constexpr float k_camera_yaw_sin = -0.70710678118F;
+} // 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 * 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 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) * 0.15F;
+  const float corner_min = 4.0F;
+  const float actual_corner = std::max(corner_size, corner_min);
+
+  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) + 1.0);
+  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

+ 10 - 1
game/map/minimap/map_preview_generator.cpp

@@ -112,8 +112,17 @@ void MapPreviewGenerator::draw_player_bases(
       continue;
     }
 
+    // Transform grid coordinates to world coordinates (centered at origin)
+    float world_x = spawn.x;
+    float world_z = spawn.z;
+    if (map_def.coordSystem == CoordSystem::Grid) {
+      const float tile = std::max(0.0001F, map_def.grid.tile_size);
+      world_x = (spawn.x - (map_def.grid.width * 0.5F - 0.5F)) * tile;
+      world_z = (spawn.z - (map_def.grid.height * 0.5F - 0.5F)) * tile;
+    }
+
     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;
 

+ 10 - 1
game/map/minimap/minimap_generator.cpp

@@ -459,7 +459,16 @@ void MinimapGenerator::render_structures(QImage &image,
       continue;
     }
 
-    const auto [px, py] = world_to_pixel(spawn.x, spawn.z, map_def.grid);
+    // Transform grid coordinates to world coordinates (centered at origin)
+    float world_x = spawn.x;
+    float world_z = spawn.z;
+    if (map_def.coordSystem == CoordSystem::Grid) {
+      const float tile = std::max(0.0001F, map_def.grid.tile_size);
+      world_x = (spawn.x - (map_def.grid.width * 0.5F - 0.5F)) * tile;
+      world_z = (spawn.z - (map_def.grid.height * 0.5F - 0.5F)) * tile;
+    }
+
+    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;