Bladeren bron

Add minimap generator with terrain, rivers, roads, and structure rendering

Co-authored-by: djeada <[email protected]>
copilot-swe-agent[bot] 1 week geleden
bovenliggende
commit
f1f517cd1c

+ 1 - 0
game/CMakeLists.txt

@@ -64,6 +64,7 @@ add_library(game_systems STATIC
     map/world_bootstrap.cpp
     map/world_bootstrap.cpp
     map/map_catalog.cpp
     map/map_catalog.cpp
     map/skirmish_loader.cpp
     map/skirmish_loader.cpp
+    map/minimap/minimap_generator.cpp
     visuals/visual_catalog.cpp
     visuals/visual_catalog.cpp
     units/unit.cpp
     units/unit.cpp
     units/archer.cpp
     units/archer.cpp

+ 186 - 0
game/map/minimap/minimap_generator.cpp

@@ -0,0 +1,186 @@
+#include "minimap_generator.h"
+#include <QColor>
+#include <QPainter>
+#include <QPen>
+#include <cmath>
+
+namespace Game::Map::Minimap {
+
+MinimapGenerator::MinimapGenerator() : m_config() {}
+
+MinimapGenerator::MinimapGenerator(const Config &config) : m_config(config) {}
+
+auto MinimapGenerator::generate(const MapDefinition &mapDef) -> QImage {
+  // Calculate actual resolution based on map size
+  const int width = static_cast<int>(mapDef.grid.width * m_config.pixels_per_tile);
+  const int height = static_cast<int>(mapDef.grid.height * m_config.pixels_per_tile);
+
+  // Create image with appropriate format
+  QImage image(width, height, QImage::Format_RGBA8888);
+  image.fill(Qt::transparent);
+
+  // Render layers in order (back to front)
+  renderTerrainBase(image, mapDef);
+  renderRoads(image, mapDef);
+  renderRivers(image, mapDef);
+  renderTerrainFeatures(image, mapDef);
+  renderStructures(image, mapDef);
+
+  return image;
+}
+
+void MinimapGenerator::renderTerrainBase(QImage &image,
+                                         const MapDefinition &mapDef) {
+  // Fill with base terrain color from biome
+  const QColor baseColor = biomeToBaseColor(mapDef.biome);
+  image.fill(baseColor);
+}
+
+void MinimapGenerator::renderTerrainFeatures(QImage &image,
+                                             const MapDefinition &mapDef) {
+  QPainter painter(&image);
+  painter.setRenderHint(QPainter::Antialiasing, true);
+
+  for (const auto &feature : mapDef.terrain) {
+    const QColor color = terrainFeatureColor(feature.type);
+    painter.setBrush(color);
+    painter.setPen(Qt::NoPen);
+
+    const auto [px, pz] = worldToPixel(feature.center_x, feature.center_z, mapDef.grid);
+
+    // Calculate pixel dimensions
+    const float pixel_width = feature.width * m_config.pixels_per_tile;
+    const float pixel_depth = feature.depth * m_config.pixels_per_tile;
+
+    // Draw as ellipse
+    painter.drawEllipse(QPointF(px, pz), pixel_width / 2.0F, pixel_depth / 2.0F);
+  }
+}
+
+void MinimapGenerator::renderRivers(QImage &image,
+                                    const MapDefinition &mapDef) {
+  if (mapDef.rivers.empty()) {
+    return;
+  }
+
+  QPainter painter(&image);
+  painter.setRenderHint(QPainter::Antialiasing, true);
+
+  // River color - light blue
+  const QColor riverColor(100, 150, 255, 200);
+
+  for (const auto &river : mapDef.rivers) {
+    const auto [x1, z1] = worldToPixel(river.start.x(), river.start.z(), mapDef.grid);
+    const auto [x2, z2] = worldToPixel(river.end.x(), river.end.z(), mapDef.grid);
+
+    const float pixel_width = river.width * m_config.pixels_per_tile * 0.5F;
+
+    QPen pen(riverColor);
+    pen.setWidthF(pixel_width);
+    pen.setCapStyle(Qt::RoundCap);
+    painter.setPen(pen);
+
+    painter.drawLine(QPointF(x1, z1), QPointF(x2, z2));
+  }
+}
+
+void MinimapGenerator::renderRoads(QImage &image,
+                                   const MapDefinition &mapDef) {
+  if (mapDef.roads.empty()) {
+    return;
+  }
+
+  QPainter painter(&image);
+  painter.setRenderHint(QPainter::Antialiasing, true);
+
+  // Road color - brownish
+  const QColor roadColor(139, 119, 101, 180);
+
+  for (const auto &road : mapDef.roads) {
+    const auto [x1, z1] = worldToPixel(road.start.x(), road.start.z(), mapDef.grid);
+    const auto [x2, z2] = worldToPixel(road.end.x(), road.end.z(), mapDef.grid);
+
+    const float pixel_width = road.width * m_config.pixels_per_tile * 0.4F;
+
+    QPen pen(roadColor);
+    pen.setWidthF(pixel_width);
+    pen.setCapStyle(Qt::RoundCap);
+    painter.setPen(pen);
+
+    painter.drawLine(QPointF(x1, z1), QPointF(x2, z2));
+  }
+}
+
+void MinimapGenerator::renderStructures(QImage &image,
+                                        const MapDefinition &mapDef) {
+  if (mapDef.spawns.empty()) {
+    return;
+  }
+
+  QPainter painter(&image);
+  painter.setRenderHint(QPainter::Antialiasing, true);
+
+  for (const auto &spawn : mapDef.spawns) {
+    // Only render structure spawns (barracks, etc.)
+    if (!Game::Units::isBuildingSpawn(spawn.type)) {
+      continue;
+    }
+
+    const auto [px, pz] = worldToPixel(spawn.x, spawn.z, mapDef.grid);
+
+    // Draw structure as a small square
+    const float size = 3.0F * m_config.pixels_per_tile;
+    
+    // Use team color or neutral
+    QColor structureColor(200, 200, 200, 255);
+    if (spawn.player_id > 0) {
+      // Use different colors for different players
+      if (spawn.player_id == 1) {
+        structureColor = QColor(100, 150, 255, 255); // Blue
+      } else if (spawn.player_id == 2) {
+        structureColor = QColor(255, 100, 100, 255); // Red
+      }
+    }
+
+    painter.setBrush(structureColor);
+    painter.setPen(QPen(Qt::black, 0.5F));
+
+    const QRectF rect(px - size / 2.0F, pz - size / 2.0F, size, size);
+    painter.drawRect(rect);
+  }
+}
+
+auto MinimapGenerator::worldToPixel(float world_x, float world_z,
+                                    const GridDefinition &grid) const
+    -> std::pair<int, int> {
+  // Convert world coordinates to pixel coordinates
+  const int px = static_cast<int>(world_x * m_config.pixels_per_tile);
+  const int pz = static_cast<int>(world_z * m_config.pixels_per_tile);
+  return {px, pz};
+}
+
+auto MinimapGenerator::biomeToBaseColor(const BiomeSettings &biome) -> QColor {
+  // Use grass_primary as the base terrain color
+  const auto &grass = biome.grass_primary;
+  return QColor::fromRgbF(grass.x(), grass.y(), grass.z());
+}
+
+auto MinimapGenerator::terrainFeatureColor(TerrainType type) -> QColor {
+  switch (type) {
+  case TerrainType::Mountain:
+    // Dark gray/brown for mountains
+    return QColor(120, 110, 100, 200);
+  case TerrainType::Hill:
+    // Lighter brown for hills
+    return QColor(150, 140, 120, 150);
+  case TerrainType::River:
+    // Blue for rivers (though rivers are handled separately)
+    return QColor(100, 150, 255, 200);
+  case TerrainType::Flat:
+  default:
+    // Slightly darker grass for flat terrain
+    return QColor(80, 120, 75, 100);
+  }
+}
+
+} // namespace Game::Map::Minimap

+ 69 - 0
game/map/minimap/minimap_generator.h

@@ -0,0 +1,69 @@
+#pragma once
+
+#include "../map_definition.h"
+#include <QImage>
+#include <cstdint>
+#include <memory>
+
+namespace Game::Map::Minimap {
+
+/**
+ * @brief Generates static minimap textures from map JSON definitions.
+ *
+ * This class implements Stage I of the minimap system - generating a static
+ * background texture that includes:
+ * - Terrain colors based on biome settings
+ * - Rivers as blue polylines
+ * - Mountains/hills as shaded symbols
+ * - Roads as paths
+ * - Villages/structures as simplified icons
+ *
+ * The generated texture is meant to be uploaded to OpenGL once during map
+ * load and never recalculated during gameplay.
+ */
+class MinimapGenerator {
+public:
+  /**
+   * @brief Configuration for minimap generation
+   */
+  struct Config {
+    int resolution_width;     // Width of minimap texture in pixels
+    int resolution_height;    // Height of minimap texture in pixels
+    float pixels_per_tile;    // How many pixels per grid tile
+
+    Config()
+        : resolution_width(256), resolution_height(256), pixels_per_tile(2.0F) {}
+  };
+
+  MinimapGenerator();
+  explicit MinimapGenerator(const Config &config);
+
+  /**
+   * @brief Generates a minimap texture from a map definition
+   * @param mapDef The map definition containing terrain, rivers, etc.
+   * @return A QImage containing the generated minimap texture
+   */
+  [[nodiscard]] auto generate(const MapDefinition &mapDef) -> QImage;
+
+private:
+  Config m_config;
+
+  // Helper methods for rendering different map elements
+  void renderTerrainBase(QImage &image, const MapDefinition &mapDef);
+  void renderTerrainFeatures(QImage &image, const MapDefinition &mapDef);
+  void renderRivers(QImage &image, const MapDefinition &mapDef);
+  void renderRoads(QImage &image, const MapDefinition &mapDef);
+  void renderStructures(QImage &image, const MapDefinition &mapDef);
+
+  // Coordinate conversion
+  [[nodiscard]] auto worldToPixel(float world_x, float world_z,
+                                  const GridDefinition &grid) const
+      -> std::pair<int, int>;
+
+  // Color utilities
+  [[nodiscard]] static auto biomeToBaseColor(const BiomeSettings &biome)
+      -> QColor;
+  [[nodiscard]] static auto terrainFeatureColor(TerrainType type) -> QColor;
+};
+
+} // namespace Game::Map::Minimap

+ 1 - 0
tests/CMakeLists.txt

@@ -6,6 +6,7 @@ add_executable(standard_of_iron_tests
     core/serialization_test.cpp
     core/serialization_test.cpp
     core/ground_type_test.cpp
     core/ground_type_test.cpp
     db/save_storage_test.cpp
     db/save_storage_test.cpp
+    map/minimap_generator_test.cpp
     render/pose_controller_test.cpp
     render/pose_controller_test.cpp
     render/pose_controller_compatibility_test.cpp
     render/pose_controller_compatibility_test.cpp
     render/mounted_pose_controller_test.cpp
     render/mounted_pose_controller_test.cpp

+ 127 - 0
tests/map/minimap_generator_test.cpp

@@ -0,0 +1,127 @@
+#include "map/minimap/minimap_generator.h"
+#include "map/map_definition.h"
+#include <gtest/gtest.h>
+#include <QImage>
+
+using namespace Game::Map;
+using namespace Game::Map::Minimap;
+
+class MinimapGeneratorTest : public ::testing::Test {
+protected:
+  void SetUp() override {
+    // Create a simple test map
+    testMap.name = "Test Map";
+    testMap.grid.width = 50;
+    testMap.grid.height = 50;
+    testMap.grid.tile_size = 1.0F;
+
+    // Set up biome with default colors
+    testMap.biome.grass_primary = QVector3D(0.3F, 0.6F, 0.28F);
+    testMap.biome.grass_secondary = QVector3D(0.44F, 0.7F, 0.32F);
+    testMap.biome.soil_color = QVector3D(0.28F, 0.24F, 0.18F);
+  }
+
+  MapDefinition testMap;
+};
+
+TEST_F(MinimapGeneratorTest, GeneratesValidImage) {
+  MinimapGenerator generator;
+  QImage result = generator.generate(testMap);
+
+  // Check that image was created
+  EXPECT_FALSE(result.isNull());
+  EXPECT_GT(result.width(), 0);
+  EXPECT_GT(result.height(), 0);
+}
+
+TEST_F(MinimapGeneratorTest, ImageDimensionsMatchGrid) {
+  MinimapGenerator::Config config;
+  config.pixels_per_tile = 2.0F;
+  MinimapGenerator generator(config);
+
+  QImage result = generator.generate(testMap);
+
+  const int expected_width = testMap.grid.width * config.pixels_per_tile;
+  const int expected_height = testMap.grid.height * config.pixels_per_tile;
+
+  EXPECT_EQ(result.width(), expected_width);
+  EXPECT_EQ(result.height(), expected_height);
+}
+
+TEST_F(MinimapGeneratorTest, RendersRivers) {
+  // Add a river to the map
+  RiverSegment river;
+  river.start = QVector3D(10.0F, 0.0F, 10.0F);
+  river.end = QVector3D(40.0F, 0.0F, 40.0F);
+  river.width = 3.0F;
+  testMap.rivers.push_back(river);
+
+  MinimapGenerator generator;
+  QImage result = generator.generate(testMap);
+
+  EXPECT_FALSE(result.isNull());
+  // River should have been rendered (we can't easily verify pixels, but check image is valid)
+}
+
+TEST_F(MinimapGeneratorTest, RendersTerrainFeatures) {
+  // Add a hill
+  TerrainFeature hill;
+  hill.type = TerrainType::Hill;
+  hill.center_x = 25.0F;
+  hill.center_z = 25.0F;
+  hill.width = 10.0F;
+  hill.depth = 10.0F;
+  hill.height = 3.0F;
+  testMap.terrain.push_back(hill);
+
+  MinimapGenerator generator;
+  QImage result = generator.generate(testMap);
+
+  EXPECT_FALSE(result.isNull());
+}
+
+TEST_F(MinimapGeneratorTest, RendersRoads) {
+  // Add a road
+  RoadSegment road;
+  road.start = QVector3D(5.0F, 0.0F, 5.0F);
+  road.end = QVector3D(45.0F, 0.0F, 45.0F);
+  road.width = 3.0F;
+  road.style = "default";
+  testMap.roads.push_back(road);
+
+  MinimapGenerator generator;
+  QImage result = generator.generate(testMap);
+
+  EXPECT_FALSE(result.isNull());
+}
+
+TEST_F(MinimapGeneratorTest, RendersStructures) {
+  // Add a barracks spawn
+  UnitSpawn barracks;
+  barracks.type = Game::Units::SpawnType::Barracks;
+  barracks.x = 25.0F;
+  barracks.z = 25.0F;
+  barracks.player_id = 1;
+  testMap.spawns.push_back(barracks);
+
+  MinimapGenerator generator;
+  QImage result = generator.generate(testMap);
+
+  EXPECT_FALSE(result.isNull());
+}
+
+TEST_F(MinimapGeneratorTest, HandlesEmptyMap) {
+  // Empty map with no features
+  MapDefinition emptyMap;
+  emptyMap.grid.width = 10;
+  emptyMap.grid.height = 10;
+  emptyMap.grid.tile_size = 1.0F;
+  emptyMap.biome.grass_primary = QVector3D(0.3F, 0.6F, 0.28F);
+
+  MinimapGenerator generator;
+  QImage result = generator.generate(emptyMap);
+
+  EXPECT_FALSE(result.isNull());
+  EXPECT_GT(result.width(), 0);
+  EXPECT_GT(result.height(), 0);
+}