瀏覽代碼

Drastically improve minimap readability: larger structures, visible roads/mountains, better contrast

djeada 20 小時之前
父節點
當前提交
ee9ae161be

+ 2 - 0
CMakeLists.txt

@@ -130,6 +130,7 @@ if(QT_VERSION_MAJOR EQUAL 6)
         app/models/cursor_manager.cpp
         app/models/cursor_manager.cpp
         app/models/hover_tracker.cpp
         app/models/hover_tracker.cpp
         app/models/selected_units_model.cpp
         app/models/selected_units_model.cpp
+        app/models/minimap_image_provider.cpp
         app/controllers/command_controller.cpp
         app/controllers/command_controller.cpp
         app/controllers/action_vfx.cpp
         app/controllers/action_vfx.cpp
         app/utils/json_vec_utils.cpp
         app/utils/json_vec_utils.cpp
@@ -146,6 +147,7 @@ else()
         app/models/cursor_manager.cpp
         app/models/cursor_manager.cpp
         app/models/hover_tracker.cpp
         app/models/hover_tracker.cpp
         app/models/selected_units_model.cpp
         app/models/selected_units_model.cpp
+        app/models/minimap_image_provider.cpp
         app/controllers/command_controller.cpp
         app/controllers/command_controller.cpp
         app/controllers/action_vfx.cpp
         app/controllers/action_vfx.cpp
         app/utils/json_vec_utils.cpp
         app/utils/json_vec_utils.cpp

+ 238 - 0
app/core/game_engine.cpp

@@ -20,6 +20,7 @@
 #include <QDebug>
 #include <QDebug>
 #include <QImage>
 #include <QImage>
 #include <QOpenGLContext>
 #include <QOpenGLContext>
+#include <QPainter>
 #include <QQuickWindow>
 #include <QQuickWindow>
 #include <QSize>
 #include <QSize>
 #include <QVariant>
 #include <QVariant>
@@ -42,6 +43,7 @@
 #include <qstringview.h>
 #include <qstringview.h>
 #include <qtmetamacros.h>
 #include <qtmetamacros.h>
 #include <qvectornd.h>
 #include <qvectornd.h>
+#include <unordered_set>
 
 
 #include "../models/selected_units_model.h"
 #include "../models/selected_units_model.h"
 #include "game/core/component.h"
 #include "game/core/component.h"
@@ -53,6 +55,8 @@
 #include "game/map/map_catalog.h"
 #include "game/map/map_catalog.h"
 #include "game/map/map_loader.h"
 #include "game/map/map_loader.h"
 #include "game/map/map_transformer.h"
 #include "game/map/map_transformer.h"
+#include "game/map/minimap/minimap_generator.h"
+#include "game/map/minimap/unit_layer.h"
 #include "game/map/skirmish_loader.h"
 #include "game/map/skirmish_loader.h"
 #include "game/map/terrain_service.h"
 #include "game/map/terrain_service.h"
 #include "game/map/visibility_service.h"
 #include "game/map/visibility_service.h"
@@ -710,6 +714,8 @@ void GameEngine::update(float dt) {
         m_runtime.visibilityVersion = new_version;
         m_runtime.visibilityVersion = new_version;
       }
       }
     }
     }
+
+    update_minimap_fog(dt);
   }
   }
 
 
   if (m_victoryService && m_world) {
   if (m_victoryService && m_world) {
@@ -1316,6 +1322,15 @@ void GameEngine::start_skirmish(const QString &map_path,
                            cam_config.defaultPitch, cam_config.defaultYaw);
                            cam_config.defaultPitch, cam_config.defaultYaw);
     }
     }
 
 
+    Game::Map::MapDefinition map_def;
+    QString map_error;
+    if (Game::Map::MapLoader::loadFromJsonFile(map_path, map_def, &map_error)) {
+      generate_minimap_for_map(map_def);
+    } else {
+      qWarning() << "GameEngine: Failed to load map for minimap generation:"
+                 << map_error;
+    }
+
     m_runtime.loading = false;
     m_runtime.loading = false;
 
 
     if (auto *ai_system = m_world->get_system<Game::Systems::AISystem>()) {
     if (auto *ai_system = m_world->get_system<Game::Systems::AISystem>()) {
@@ -1826,6 +1841,8 @@ void GameEngine::restore_environment_from_metadata(
   if (m_renderer && m_camera) {
   if (m_renderer && m_camera) {
     if (loaded_definition) {
     if (loaded_definition) {
       Game::Map::Environment::apply(def, *m_renderer, *m_camera);
       Game::Map::Environment::apply(def, *m_renderer, *m_camera);
+
+      generate_minimap_for_map(def);
     } else {
     } else {
       Game::Map::Environment::applyDefault(*m_renderer, *m_camera);
       Game::Map::Environment::applyDefault(*m_renderer, *m_camera);
     }
     }
@@ -2121,3 +2138,224 @@ void GameEngine::load_audio_resources() {
 
 
   qInfo() << "Audio resources loading complete";
   qInfo() << "Audio resources loading complete";
 }
 }
+
+auto GameEngine::minimap_image() const -> QImage { return m_minimap_image; }
+
+void GameEngine::generate_minimap_for_map(
+    const Game::Map::MapDefinition &map_def) {
+  Game::Map::Minimap::MinimapGenerator generator;
+  m_minimap_base_image = generator.generate(map_def);
+
+  if (!m_minimap_base_image.isNull()) {
+    qDebug() << "GameEngine: Generated minimap of size"
+             << m_minimap_base_image.width() << "x"
+             << m_minimap_base_image.height();
+
+    m_world_width = static_cast<float>(map_def.grid.width);
+    m_world_height = static_cast<float>(map_def.grid.height);
+
+    m_unit_layer = std::make_unique<Game::Map::Minimap::UnitLayer>();
+    m_unit_layer->init(m_minimap_base_image.width(),
+                       m_minimap_base_image.height(), m_world_width,
+                       m_world_height);
+    qDebug() << "GameEngine: Initialized unit layer for world" << m_world_width
+             << "x" << m_world_height;
+
+    m_minimap_fog_version = 0;
+    m_minimap_update_timer = MINIMAP_UPDATE_INTERVAL;
+    update_minimap_fog(0.0F);
+  } else {
+    qWarning() << "GameEngine: Failed to generate minimap";
+  }
+}
+
+void GameEngine::update_minimap_fog(float dt) {
+  if (m_minimap_base_image.isNull()) {
+    return;
+  }
+
+  m_minimap_update_timer += dt;
+  if (m_minimap_update_timer < MINIMAP_UPDATE_INTERVAL) {
+    return;
+  }
+  m_minimap_update_timer = 0.0F;
+
+  auto &visibility_service = Game::Map::VisibilityService::instance();
+  if (!visibility_service.isInitialized()) {
+
+    if (m_minimap_image != m_minimap_base_image) {
+      m_minimap_image = m_minimap_base_image;
+      emit minimap_image_changed();
+    }
+    return;
+  }
+
+  const auto current_version = visibility_service.version();
+  if (current_version == m_minimap_fog_version && !m_minimap_image.isNull()) {
+
+    update_minimap_units();
+    return;
+  }
+  m_minimap_fog_version = current_version;
+
+  const int vis_width = visibility_service.getWidth();
+  const int vis_height = visibility_service.getHeight();
+  const auto cells = visibility_service.snapshotCells();
+
+  if (cells.empty() || vis_width <= 0 || vis_height <= 0) {
+    m_minimap_image = m_minimap_base_image;
+    emit minimap_image_changed();
+    return;
+  }
+
+  m_minimap_image = m_minimap_base_image.copy();
+
+  const int img_width = m_minimap_image.width();
+  const int img_height = m_minimap_image.height();
+
+  constexpr float k_inv_cos = -0.70710678118F;
+  constexpr float k_inv_sin = 0.70710678118F;
+
+  const float scale_x =
+      static_cast<float>(vis_width) / static_cast<float>(img_width);
+  const float scale_y =
+      static_cast<float>(vis_height) / static_cast<float>(img_height);
+
+  constexpr int FOG_R = 45;
+  constexpr int FOG_G = 38;
+  constexpr int FOG_B = 30;
+  constexpr int ALPHA_UNSEEN = 180;
+  constexpr int ALPHA_EXPLORED = 60;
+  constexpr int ALPHA_VISIBLE = 0;
+  constexpr float ALPHA_THRESHOLD = 0.5F;
+  constexpr float ALPHA_SCALE = 1.0F / 255.0F;
+
+  auto get_alpha = [&cells, vis_width, ALPHA_VISIBLE, ALPHA_EXPLORED,
+                    ALPHA_UNSEEN](int vx, int vy) -> float {
+    const size_t idx = static_cast<size_t>(vy * vis_width + vx);
+    if (idx >= cells.size()) {
+      return static_cast<float>(ALPHA_UNSEEN);
+    }
+    const auto state = static_cast<Game::Map::VisibilityState>(cells[idx]);
+    switch (state) {
+    case Game::Map::VisibilityState::Visible:
+      return static_cast<float>(ALPHA_VISIBLE);
+    case Game::Map::VisibilityState::Explored:
+      return static_cast<float>(ALPHA_EXPLORED);
+    default:
+      return static_cast<float>(ALPHA_UNSEEN);
+    }
+  };
+
+  const float half_img_w = static_cast<float>(img_width) * 0.5F;
+  const float half_img_h = static_cast<float>(img_height) * 0.5F;
+  const float half_vis_w = static_cast<float>(vis_width) * 0.5F;
+  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));
+
+    for (int x = 0; x < img_width; ++x) {
+
+      const float centered_x = static_cast<float>(x) - half_img_w;
+      const float centered_y = static_cast<float>(y) - half_img_h;
+
+      const float world_x = centered_x * k_inv_cos - centered_y * k_inv_sin;
+      const float world_y = centered_x * k_inv_sin + centered_y * k_inv_cos;
+
+      const float vis_x = (world_x * scale_x) + half_vis_w;
+      const float vis_y = (world_y * scale_y) + half_vis_h;
+
+      const int vx0 = std::clamp(static_cast<int>(vis_x), 0, vis_width - 1);
+      const int vx1 = std::clamp(vx0 + 1, 0, vis_width - 1);
+      const float fx = vis_x - static_cast<float>(vx0);
+
+      const int vy0 = std::clamp(static_cast<int>(vis_y), 0, vis_height - 1);
+      const int vy1 = std::clamp(vy0 + 1, 0, vis_height - 1);
+      const float fy = vis_y - static_cast<float>(vy0);
+
+      const float a00 = get_alpha(vx0, vy0);
+      const float a10 = get_alpha(vx1, vy0);
+      const float a01 = get_alpha(vx0, vy1);
+      const float a11 = get_alpha(vx1, vy1);
+
+      const float alpha_top = a00 + (a10 - a00) * fx;
+      const float alpha_bot = a01 + (a11 - a01) * fx;
+      const float fog_alpha = alpha_top + (alpha_bot - alpha_top) * fy;
+
+      if (fog_alpha > ALPHA_THRESHOLD) {
+        const QRgb original = scanline[x];
+        const int orig_r = qRed(original);
+        const int orig_g = qGreen(original);
+        const int orig_b = qBlue(original);
+
+        const float blend = fog_alpha * ALPHA_SCALE;
+        const float inv_blend = 1.0F - blend;
+
+        const int new_r = static_cast<int>(orig_r * inv_blend + FOG_R * blend);
+        const int new_g = static_cast<int>(orig_g * inv_blend + FOG_G * blend);
+        const int new_b = static_cast<int>(orig_b * inv_blend + FOG_B * blend);
+
+        scanline[x] = qRgba(new_r, new_g, new_b, 255);
+      }
+    }
+  }
+
+  update_minimap_units();
+}
+
+void GameEngine::update_minimap_units() {
+  if (m_minimap_image.isNull() || !m_unit_layer || !m_world) {
+    emit minimap_image_changed();
+    return;
+  }
+
+  std::vector<Game::Map::Minimap::UnitMarker> markers;
+  markers.reserve(128);
+
+  std::unordered_set<Engine::Core::EntityID> selected_ids;
+  if (auto *selection_system =
+          m_world->get_system<Game::Systems::SelectionSystem>()) {
+    const auto &sel = selection_system->get_selected_units();
+    selected_ids.insert(sel.begin(), sel.end());
+  }
+
+  {
+    const std::lock_guard<std::recursive_mutex> lock(
+        m_world->get_entity_mutex());
+    const auto &entities = m_world->get_entities();
+
+    for (const auto &[entity_id, entity] : entities) {
+      const auto *unit = entity->get_component<Engine::Core::UnitComponent>();
+      if (!unit) {
+        continue;
+      }
+
+      const auto *transform =
+          entity->get_component<Engine::Core::TransformComponent>();
+      if (!transform) {
+        continue;
+      }
+
+      Game::Map::Minimap::UnitMarker marker;
+      marker.world_x = transform->position.x;
+      marker.world_z = transform->position.z;
+      marker.owner_id = unit->owner_id;
+      marker.is_selected = selected_ids.count(entity_id) > 0;
+      marker.is_building = Game::Units::isBuildingSpawn(unit->spawn_type);
+
+      markers.push_back(marker);
+    }
+  }
+
+  m_unit_layer->update(markers);
+
+  const QImage &unit_overlay = m_unit_layer->get_image();
+  if (!unit_overlay.isNull()) {
+    QPainter painter(&m_minimap_image);
+    painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
+    painter.drawImage(0, 0, unit_overlay);
+  }
+
+  emit minimap_image_changed();
+}

+ 21 - 1
app/core/game_engine.h

@@ -52,6 +52,9 @@ struct IRenderPass;
 } // namespace Render::GL
 } // namespace Render::GL
 
 
 namespace Game {
 namespace Game {
+namespace Map::Minimap {
+class UnitLayer;
+}
 namespace Systems {
 namespace Systems {
 class SelectionSystem;
 class SelectionSystem;
 class SelectionController;
 class SelectionController;
@@ -63,7 +66,8 @@ class SaveLoadService;
 } // namespace Systems
 } // namespace Systems
 namespace Map {
 namespace Map {
 class MapCatalog;
 class MapCatalog;
-}
+struct MapDefinition;
+} // namespace Map
 } // namespace Game
 } // namespace Game
 
 
 namespace App {
 namespace App {
@@ -117,6 +121,8 @@ public:
                  set_selected_player_id NOTIFY selected_player_id_changed)
                  set_selected_player_id NOTIFY selected_player_id_changed)
   Q_PROPERTY(QString last_error READ last_error NOTIFY last_error_changed)
   Q_PROPERTY(QString last_error READ last_error NOTIFY last_error_changed)
   Q_PROPERTY(QObject *audio_system READ audio_system CONSTANT)
   Q_PROPERTY(QObject *audio_system READ audio_system CONSTANT)
+  Q_PROPERTY(
+      QImage minimap_image READ minimap_image NOTIFY minimap_image_changed)
 
 
   Q_INVOKABLE void on_map_clicked(qreal sx, qreal sy);
   Q_INVOKABLE void on_map_clicked(qreal sx, qreal sy);
   Q_INVOKABLE void on_right_click(qreal sx, qreal sy);
   Q_INVOKABLE void on_right_click(qreal sx, qreal sy);
@@ -209,6 +215,8 @@ public:
   Q_INVOKABLE void exit_game();
   Q_INVOKABLE void exit_game();
   Q_INVOKABLE [[nodiscard]] QVariantList get_owner_info() const;
   Q_INVOKABLE [[nodiscard]] QVariantList get_owner_info() const;
 
 
+  [[nodiscard]] QImage minimap_image() const;
+
   QObject *audio_system();
   QObject *audio_system();
 
 
   void setWindow(QQuickWindow *w) { m_window = w; }
   void setWindow(QQuickWindow *w) { m_window = w; }
@@ -309,6 +317,14 @@ private:
   std::unique_ptr<Game::Map::MapCatalog> m_mapCatalog;
   std::unique_ptr<Game::Map::MapCatalog> m_mapCatalog;
   std::unique_ptr<Game::Audio::AudioEventHandler> m_audioEventHandler;
   std::unique_ptr<Game::Audio::AudioEventHandler> m_audioEventHandler;
   std::unique_ptr<App::Models::AudioSystemProxy> m_audio_systemProxy;
   std::unique_ptr<App::Models::AudioSystemProxy> m_audio_systemProxy;
+  QImage m_minimap_image;
+  QImage m_minimap_base_image;
+  std::uint64_t m_minimap_fog_version = 0;
+  std::unique_ptr<Game::Map::Minimap::UnitLayer> m_unit_layer;
+  float m_world_width = 0.0F;
+  float m_world_height = 0.0F;
+  float m_minimap_update_timer = 0.0F;
+  static constexpr float MINIMAP_UPDATE_INTERVAL = 0.1F;
   QQuickWindow *m_window = nullptr;
   QQuickWindow *m_window = nullptr;
   RuntimeState m_runtime;
   RuntimeState m_runtime;
   ViewportState m_viewport;
   ViewportState m_viewport;
@@ -334,6 +350,9 @@ private:
   [[nodiscard]] bool is_player_in_combat() const;
   [[nodiscard]] bool is_player_in_combat() const;
   static void load_audio_resources();
   static void load_audio_resources();
   void load_campaigns();
   void load_campaigns();
+  void generate_minimap_for_map(const Game::Map::MapDefinition &map_def);
+  void update_minimap_fog(float dt);
+  void update_minimap_units();
 signals:
 signals:
   void selected_units_changed();
   void selected_units_changed();
   void selected_units_data_changed();
   void selected_units_data_changed();
@@ -348,6 +367,7 @@ signals:
   void selected_player_id_changed();
   void selected_player_id_changed();
   void last_error_changed();
   void last_error_changed();
   void maps_loading_changed();
   void maps_loading_changed();
+  void minimap_image_changed();
   void save_slots_changed();
   void save_slots_changed();
   void hold_mode_changed(bool active);
   void hold_mode_changed(bool active);
 };
 };

+ 34 - 0
app/models/minimap_image_provider.cpp

@@ -0,0 +1,34 @@
+#include "minimap_image_provider.h"
+
+MinimapImageProvider::MinimapImageProvider()
+    : QQuickImageProvider(QQuickImageProvider::Image) {}
+
+QImage MinimapImageProvider::requestImage(const QString &id, QSize *size,
+                                          const QSize &requested_size) {
+  Q_UNUSED(id);
+
+  if (m_minimap_image.isNull()) {
+
+    QImage placeholder(64, 64, QImage::Format_RGBA8888);
+    placeholder.fill(QColor(15, 26, 34));
+    if (size) {
+      *size = placeholder.size();
+    }
+    return placeholder;
+  }
+
+  if (size) {
+    *size = m_minimap_image.size();
+  }
+
+  if (requested_size.isValid() && !requested_size.isEmpty()) {
+    return m_minimap_image.scaled(requested_size, Qt::KeepAspectRatio,
+                                  Qt::SmoothTransformation);
+  }
+
+  return m_minimap_image;
+}
+
+void MinimapImageProvider::set_minimap_image(const QImage &image) {
+  m_minimap_image = image;
+}

+ 17 - 0
app/models/minimap_image_provider.h

@@ -0,0 +1,17 @@
+#pragma once
+
+#include <QImage>
+#include <QQuickImageProvider>
+
+class MinimapImageProvider : public QQuickImageProvider {
+public:
+  MinimapImageProvider();
+
+  QImage requestImage(const QString &id, QSize *size,
+                      const QSize &requested_size) override;
+
+  void set_minimap_image(const QImage &image);
+
+private:
+  QImage m_minimap_image;
+};

+ 3 - 0
game/CMakeLists.txt

@@ -66,6 +66,9 @@ add_library(game_systems STATIC
     map/skirmish_loader.cpp
     map/skirmish_loader.cpp
     map/minimap/minimap_generator.cpp
     map/minimap/minimap_generator.cpp
     map/minimap/minimap_texture_manager.cpp
     map/minimap/minimap_texture_manager.cpp
+    map/minimap/fog_of_war_mask.cpp
+    map/minimap/minimap_manager.cpp
+    map/minimap/unit_layer.cpp
     visuals/visual_catalog.cpp
     visuals/visual_catalog.cpp
     units/unit.cpp
     units/unit.cpp
     units/archer.cpp
     units/archer.cpp

+ 0 - 229
game/map/minimap/README.md

@@ -1,229 +0,0 @@
-# Minimap Static Layer Implementation (Stage I)
-
-This directory contains the implementation of the **static layer** of the minimap system, which generates a single background texture from map JSON data.
-
-## Overview
-
-The minimap system has three conceptual layers:
-
-1. **Static Layer (Implemented Here)** - Map background texture generated once per map
-2. **Semi-Dynamic Layer (Future)** - Fog of war mask updated less frequently
-3. **Dynamic Layer (Future)** - Interactive entities and UI updated frequently
-
-This implementation focuses on **Stage I: Static Layer**.
-
-## Components
-
-### MinimapGenerator
-
-**File:** `minimap_generator.h`, `minimap_generator.cpp`
-
-The core class that generates a minimap texture from a `MapDefinition`. It converts JSON map data into a rasterized image suitable for OpenGL upload.
-
-**Features:**
-- Terrain base color rendering from biome settings
-- Terrain features (hills, mountains) as shaded symbols
-- Rivers as blue polylines
-- Roads as brown paths
-- Structures (barracks) as colored squares
-
-**Usage:**
-```cpp
-#include "map/minimap/minimap_generator.h"
-
-Game::Map::Minimap::MinimapGenerator generator;
-QImage minimap = generator.generate(mapDefinition);
-```
-
-**Configuration:**
-```cpp
-Game::Map::Minimap::MinimapGenerator::Config config;
-config.pixels_per_tile = 2.0F;  // Resolution scale
-Game::Map::Minimap::MinimapGenerator generator(config);
-```
-
-### MinimapTextureManager
-
-**File:** `minimap_texture_manager.h`, `minimap_texture_manager.cpp`
-
-A higher-level manager class that handles the complete lifecycle of minimap textures, including OpenGL texture management.
-
-**Features:**
-- Automatic minimap generation from map definition
-- OpenGL texture management (ready for integration)
-- Image caching for debugging/saving
-
-**Usage:**
-```cpp
-#include "map/minimap/minimap_texture_manager.h"
-
-Game::Map::Minimap::MinimapTextureManager manager;
-if (manager.generateForMap(mapDefinition)) {
-    auto* texture = manager.getTexture();
-    // Use texture for rendering
-}
-```
-
-## Pipeline
-
-The minimap generation follows this pipeline:
-
-```
-JSON Map File
-     ↓
-MapDefinition (parsed)
-     ↓
-MinimapGenerator
-     ↓
-QImage (RGBA)
-     ↓
-OpenGL Texture
-```
-
-### Rendering Order
-
-Layers are rendered back-to-front:
-1. Terrain base (biome color)
-2. Roads
-3. Rivers
-4. Terrain features (hills, mountains)
-5. Structures (barracks)
-
-## Color Mapping
-
-### Terrain Base
-- Uses `biome.grass_primary` from map definition
-- Fills the entire background
-
-### Terrain Features
-- **Mountains**: Dark gray/brown `(120, 110, 100)`
-- **Hills**: Light brown `(150, 140, 120)`
-- **Flat**: Slightly darker grass `(80, 120, 75)`
-
-### Rivers
-- Light blue `(100, 150, 255)` with alpha
-- Width scales with `pixels_per_tile`
-
-### Roads
-- Brownish `(139, 119, 101)` with alpha
-- Width scales with `pixels_per_tile`
-
-### Structures
-- **Player 1**: Blue `(100, 150, 255)`
-- **Player 2**: Red `(255, 100, 100)`
-- **Neutral**: Gray `(200, 200, 200)`
-
-## Integration Guide
-
-### During Map Load
-
-Integrate minimap generation during map initialization:
-
-```cpp
-// In your map loading code (e.g., world_bootstrap.cpp)
-#include "map/minimap/minimap_texture_manager.h"
-
-// After loading map definition
-Game::Map::Minimap::MinimapTextureManager minimap;
-if (!minimap.generateForMap(mapDefinition)) {
-    qWarning() << "Failed to generate minimap";
-}
-
-// Store minimap manager in your game state
-// Use minimap.getTexture() for rendering in UI
-```
-
-### In Renderer
-
-```cpp
-// In your UI rendering code
-auto* minimapTexture = gameState->getMinimapTexture();
-if (minimapTexture) {
-    minimapTexture->bind();
-    // Render to screen
-    minimapTexture->unbind();
-}
-```
-
-## Testing
-
-Comprehensive unit tests are provided in `tests/map/minimap_generator_test.cpp`:
-
-```bash
-# Run tests
-cd build
-./bin/standard_of_iron_tests --gtest_filter="MinimapGeneratorTest.*"
-```
-
-## Example
-
-See `minimap_example.cpp` for a complete standalone example showing:
-1. Loading a map from JSON
-2. Generating a minimap
-3. Saving the minimap to disk
-4. Using the texture manager
-
-## Performance
-
-- **Generation Time**: Milliseconds for typical maps (< 100ms for 120x120 grid)
-- **Memory**: Depends on resolution. Default 2 pixels/tile = ~100KB for 100x100 grid
-- **Runtime Cost**: Zero - texture is generated once during map load
-
-## Configuration
-
-### Resolution Scaling
-
-Adjust `pixels_per_tile` to control minimap resolution:
-
-```cpp
-config.pixels_per_tile = 1.0F;  // Lower resolution (faster, smaller)
-config.pixels_per_tile = 2.0F;  // Default
-config.pixels_per_tile = 4.0F;  // Higher resolution (slower, larger)
-```
-
-### Custom Colors
-
-To customize colors, modify the color constants in `minimap_generator.cpp`:
-- `biomeToBaseColor()` - Terrain base color
-- `terrainFeatureColor()` - Feature colors
-- `renderRivers()` - River color
-- `renderRoads()` - Road color
-- `renderStructures()` - Structure colors
-
-## Future Enhancements (Not in This Stage)
-
-- **Stage II**: Fog of war mask generation
-- **Stage III**: Dynamic entity rendering
-- Historical styling (parchment texture, hand-drawn icons)
-- Borders and faction territories
-- Elevation shading
-- Mini-icons for villages and landmarks
-
-## Technical Notes
-
-### Coordinate System
-
-- Input: World coordinates from `MapDefinition`
-- Output: Pixel coordinates in texture
-- Conversion: `pixel = world_coord * pixels_per_tile`
-
-### Image Format
-
-- **QImage Format**: RGBA8888
-- **OpenGL Format**: GL_RGBA, GL_UNSIGNED_BYTE
-- **Alpha Channel**: Used for semi-transparent features
-
-### Anti-aliasing
-
-QPainter anti-aliasing is enabled for smooth lines and curves.
-
-## Dependencies
-
-- Qt6 Core (QImage, QPainter, QColor)
-- Qt6 Gui (QVector3D)
-- Game map system (MapDefinition, terrain types)
-- Render GL (Texture class for OpenGL integration)
-
-## License
-
-Part of the Standard of Iron project. See main project LICENSE.

+ 359 - 0
game/map/minimap/fog_of_war_mask.cpp

@@ -0,0 +1,359 @@
+#include "fog_of_war_mask.h"
+#include <algorithm>
+#include <cmath>
+#include <cstring>
+
+namespace Game::Map::Minimap {
+
+FogOfWarMask::FogOfWarMask(int map_width, int map_height, float tile_size,
+                           const FogOfWarConfig &config)
+    : m_config(config), m_map_width(map_width), m_map_height(map_height),
+      m_tile_size(tile_size) {
+
+  m_fog_width = std::max(1, map_width / m_config.resolution_divisor);
+  m_fog_height = std::max(1, map_height / m_config.resolution_divisor);
+
+  m_fog_cell_size = tile_size * static_cast<float>(m_config.resolution_divisor);
+
+  const size_t total_cells =
+      static_cast<size_t>(m_fog_width) * static_cast<size_t>(m_fog_height);
+  const size_t bytes_needed = (total_cells + 3) / 4;
+
+  m_visibility_data.resize(bytes_needed, 0);
+}
+
+FogOfWarMask::~FogOfWarMask() = default;
+
+FogOfWarMask::FogOfWarMask(FogOfWarMask &&) noexcept = default;
+auto FogOfWarMask::operator=(FogOfWarMask &&) noexcept -> FogOfWarMask & =
+                                                              default;
+
+void FogOfWarMask::set_cell(int fog_x, int fog_y, VisibilityState state) {
+  if (fog_x < 0 || fog_x >= m_fog_width || fog_y < 0 || fog_y >= m_fog_height) {
+    return;
+  }
+
+  const size_t cell_index =
+      static_cast<size_t>(fog_y) * static_cast<size_t>(m_fog_width) +
+      static_cast<size_t>(fog_x);
+  const size_t byte_index = cell_index / 4;
+  const int bit_offset = static_cast<int>((cell_index % 4) * 2);
+
+  const uint8_t mask = static_cast<uint8_t>(~(0x03 << bit_offset));
+  const uint8_t value =
+      static_cast<uint8_t>(static_cast<uint8_t>(state) << bit_offset);
+
+  m_visibility_data[byte_index] =
+      static_cast<uint8_t>((m_visibility_data[byte_index] & mask) | value);
+}
+
+auto FogOfWarMask::get_cell(int fog_x, int fog_y) const -> VisibilityState {
+  if (fog_x < 0 || fog_x >= m_fog_width || fog_y < 0 || fog_y >= m_fog_height) {
+    return VisibilityState::Unseen;
+  }
+
+  const size_t cell_index =
+      static_cast<size_t>(fog_y) * static_cast<size_t>(m_fog_width) +
+      static_cast<size_t>(fog_x);
+  const size_t byte_index = cell_index / 4;
+  const int bit_offset = static_cast<int>((cell_index % 4) * 2);
+
+  const uint8_t value = static_cast<uint8_t>(
+      (m_visibility_data[byte_index] >> bit_offset) & 0x03);
+  return static_cast<VisibilityState>(value);
+}
+
+auto FogOfWarMask::world_to_fog(float world_x,
+                                float world_z) const -> std::pair<int, int> {
+
+  const float world_width = static_cast<float>(m_map_width) * m_tile_size;
+  const float world_height = static_cast<float>(m_map_height) * m_tile_size;
+
+  const float norm_x = (world_x + world_width * 0.5F) / world_width;
+  const float norm_z = (world_z + world_height * 0.5F) / world_height;
+
+  const int fog_x =
+      std::clamp(static_cast<int>(norm_x * static_cast<float>(m_fog_width)), 0,
+                 m_fog_width - 1);
+  const int fog_y =
+      std::clamp(static_cast<int>(norm_z * static_cast<float>(m_fog_height)), 0,
+                 m_fog_height - 1);
+
+  return {fog_x, fog_y};
+}
+
+void FogOfWarMask::clear_current_visibility() {
+
+  const size_t total_cells =
+      static_cast<size_t>(m_fog_width) * static_cast<size_t>(m_fog_height);
+
+  for (size_t i = 0; i < total_cells; ++i) {
+    const size_t byte_index = i / 4;
+    const int bit_offset = static_cast<int>((i % 4) * 2);
+
+    const uint8_t current = static_cast<uint8_t>(
+        (m_visibility_data[byte_index] >> bit_offset) & 0x03);
+
+    if (current == static_cast<uint8_t>(VisibilityState::Visible)) {
+
+      const uint8_t mask = static_cast<uint8_t>(~(0x03 << bit_offset));
+      const uint8_t value = static_cast<uint8_t>(
+          static_cast<uint8_t>(VisibilityState::Revealed) << bit_offset);
+      m_visibility_data[byte_index] =
+          static_cast<uint8_t>((m_visibility_data[byte_index] & mask) | value);
+    }
+  }
+}
+
+void FogOfWarMask::reveal_circle(int center_x, int center_y,
+                                 float radius_cells) {
+
+  const int radius_int = static_cast<int>(std::ceil(radius_cells));
+  const float radius_sq = radius_cells * radius_cells;
+
+  const int min_y = std::max(0, center_y - radius_int);
+  const int max_y = std::min(m_fog_height - 1, center_y + radius_int);
+  const int min_x = std::max(0, center_x - radius_int);
+  const int max_x = std::min(m_fog_width - 1, center_x + radius_int);
+
+  for (int y = min_y; y <= max_y; ++y) {
+    const float dy = static_cast<float>(y - center_y);
+    const float dy_sq = dy * dy;
+
+    for (int x = min_x; x <= max_x; ++x) {
+      const float dx = static_cast<float>(x - center_x);
+      const float dist_sq = dx * dx + dy_sq;
+
+      if (dist_sq <= radius_sq) {
+        set_cell(x, y, VisibilityState::Visible);
+      }
+    }
+  }
+}
+
+void FogOfWarMask::update_vision(const std::vector<VisionSource> &sources,
+                                 int player_id) {
+
+  clear_current_visibility();
+
+  for (const auto &source : sources) {
+    if (source.player_id != player_id) {
+      continue;
+    }
+
+    const auto [fog_x, fog_y] = world_to_fog(source.world_x, source.world_z);
+
+    const float radius_cells = source.vision_radius / m_fog_cell_size;
+
+    reveal_circle(fog_x, fog_y, radius_cells);
+  }
+
+  m_dirty = true;
+}
+
+auto FogOfWarMask::tick(const std::vector<VisionSource> &sources,
+                        int player_id) -> bool {
+  ++m_frame_counter;
+
+  if (m_frame_counter >= m_config.update_interval) {
+    m_frame_counter = 0;
+    update_vision(sources, player_id);
+    return true;
+  }
+
+  return false;
+}
+
+auto FogOfWarMask::get_visibility(int fog_x,
+                                  int fog_y) const -> VisibilityState {
+  return get_cell(fog_x, fog_y);
+}
+
+auto FogOfWarMask::is_revealed(float world_x, float world_z) const -> bool {
+  const auto [fog_x, fog_y] = world_to_fog(world_x, world_z);
+  const auto state = get_cell(fog_x, fog_y);
+  return state != VisibilityState::Unseen;
+}
+
+auto FogOfWarMask::is_visible(float world_x, float world_z) const -> bool {
+  const auto [fog_x, fog_y] = world_to_fog(world_x, world_z);
+  return get_cell(fog_x, fog_y) == VisibilityState::Visible;
+}
+
+void FogOfWarMask::reset() {
+  std::memset(m_visibility_data.data(), 0, m_visibility_data.size());
+  m_dirty = true;
+}
+
+void FogOfWarMask::reveal_all() {
+
+  const size_t total_cells =
+      static_cast<size_t>(m_fog_width) * static_cast<size_t>(m_fog_height);
+
+  for (size_t i = 0; i < total_cells; ++i) {
+    const size_t byte_index = i / 4;
+    const int bit_offset = static_cast<int>((i % 4) * 2);
+
+    const uint8_t mask = static_cast<uint8_t>(~(0x03 << bit_offset));
+    const uint8_t value = static_cast<uint8_t>(
+        static_cast<uint8_t>(VisibilityState::Revealed) << bit_offset);
+    m_visibility_data[byte_index] =
+        static_cast<uint8_t>((m_visibility_data[byte_index] & mask) | value);
+  }
+
+  m_dirty = true;
+}
+
+auto FogOfWarMask::memory_usage() const -> size_t {
+  size_t total = sizeof(*this);
+  total += m_visibility_data.capacity();
+  total += static_cast<size_t>(m_cached_mask.sizeInBytes());
+  return total;
+}
+
+void FogOfWarMask::apply_gaussian_blur(std::vector<uint8_t> &alpha_buffer,
+                                       int width, int height) const {
+  if (m_config.blur_radius <= 0) {
+    return;
+  }
+
+  const int radius = m_config.blur_radius;
+
+  const int kernel_size = radius * 2 + 1;
+  std::vector<float> kernel(static_cast<size_t>(kernel_size));
+  float kernel_sum = 0.0F;
+
+  const float sigma = static_cast<float>(radius) / 2.0F;
+  const float sigma_sq_2 = 2.0F * sigma * sigma;
+
+  for (int i = 0; i < kernel_size; ++i) {
+    const float x = static_cast<float>(i - radius);
+    kernel[static_cast<size_t>(i)] = std::exp(-(x * x) / sigma_sq_2);
+    kernel_sum += kernel[static_cast<size_t>(i)];
+  }
+
+  for (auto &k : kernel) {
+    k /= kernel_sum;
+  }
+
+  std::vector<float> temp(static_cast<size_t>(width * height));
+
+  for (int y = 0; y < height; ++y) {
+    for (int x = 0; x < width; ++x) {
+      float sum = 0.0F;
+
+      for (int k = -radius; k <= radius; ++k) {
+        const int sample_x = std::clamp(x + k, 0, width - 1);
+        const size_t src_idx = static_cast<size_t>(y * width + sample_x);
+        sum += static_cast<float>(alpha_buffer[src_idx]) *
+               kernel[static_cast<size_t>(k + radius)];
+      }
+
+      temp[static_cast<size_t>(y * width + x)] = sum;
+    }
+  }
+
+  for (int y = 0; y < height; ++y) {
+    for (int x = 0; x < width; ++x) {
+      float sum = 0.0F;
+
+      for (int k = -radius; k <= radius; ++k) {
+        const int sample_y = std::clamp(y + k, 0, height - 1);
+        const size_t src_idx = static_cast<size_t>(sample_y * width + x);
+        sum += temp[src_idx] * kernel[static_cast<size_t>(k + radius)];
+      }
+
+      alpha_buffer[static_cast<size_t>(y * width + x)] =
+          static_cast<uint8_t>(std::clamp(sum, 0.0F, 255.0F));
+    }
+  }
+}
+
+auto FogOfWarMask::generate_mask(int target_width,
+                                 int target_height) const -> QImage {
+
+  if (!m_dirty && m_cached_width == target_width &&
+      m_cached_height == target_height && !m_cached_mask.isNull()) {
+    return m_cached_mask;
+  }
+
+  std::vector<uint8_t> fog_alpha(static_cast<size_t>(m_fog_width) *
+                                 static_cast<size_t>(m_fog_height));
+
+  for (int y = 0; y < m_fog_height; ++y) {
+    for (int x = 0; x < m_fog_width; ++x) {
+      const auto state = get_cell(x, y);
+      uint8_t alpha = m_config.alpha_unseen;
+
+      switch (state) {
+      case VisibilityState::Unseen:
+        alpha = m_config.alpha_unseen;
+        break;
+      case VisibilityState::Revealed:
+        alpha = m_config.alpha_revealed;
+        break;
+      case VisibilityState::Visible:
+        alpha = m_config.alpha_visible;
+        break;
+      }
+
+      fog_alpha[static_cast<size_t>(y * m_fog_width + x)] = alpha;
+    }
+  }
+
+  apply_gaussian_blur(fog_alpha, m_fog_width, m_fog_height);
+
+  QImage mask(target_width, target_height, QImage::Format_RGBA8888);
+
+  const float scale_x =
+      static_cast<float>(m_fog_width) / static_cast<float>(target_width);
+  const float scale_y =
+      static_cast<float>(m_fog_height) / static_cast<float>(target_height);
+
+  for (int y = 0; y < target_height; ++y) {
+    auto *scanline = reinterpret_cast<uint32_t *>(mask.scanLine(y));
+
+    for (int x = 0; x < target_width; ++x) {
+
+      const float fog_x = static_cast<float>(x) * scale_x;
+      const float fog_y = static_cast<float>(y) * scale_y;
+
+      const int x0 = std::min(static_cast<int>(fog_x), m_fog_width - 1);
+      const int y0 = std::min(static_cast<int>(fog_y), m_fog_height - 1);
+      const int x1 = std::min(x0 + 1, m_fog_width - 1);
+      const int y1 = std::min(y0 + 1, m_fog_height - 1);
+
+      const float fx = fog_x - static_cast<float>(x0);
+      const float fy = fog_y - static_cast<float>(y0);
+
+      const float a00 = static_cast<float>(
+          fog_alpha[static_cast<size_t>(y0 * m_fog_width + x0)]);
+      const float a10 = static_cast<float>(
+          fog_alpha[static_cast<size_t>(y0 * m_fog_width + x1)]);
+      const float a01 = static_cast<float>(
+          fog_alpha[static_cast<size_t>(y1 * m_fog_width + x0)]);
+      const float a11 = static_cast<float>(
+          fog_alpha[static_cast<size_t>(y1 * m_fog_width + x1)]);
+
+      const float alpha_top = a00 + (a10 - a00) * fx;
+      const float alpha_bot = a01 + (a11 - a01) * fx;
+      const float alpha = alpha_top + (alpha_bot - alpha_top) * fy;
+
+      const uint8_t final_alpha =
+          static_cast<uint8_t>(std::clamp(alpha, 0.0F, 255.0F));
+
+      scanline[x] = (static_cast<uint32_t>(final_alpha) << 24) |
+                    (static_cast<uint32_t>(m_config.fog_color_b) << 16) |
+                    (static_cast<uint32_t>(m_config.fog_color_g) << 8) |
+                    static_cast<uint32_t>(m_config.fog_color_r);
+    }
+  }
+
+  m_cached_mask = mask;
+  m_cached_width = target_width;
+  m_cached_height = target_height;
+
+  return mask;
+}
+
+} // namespace Game::Map::Minimap

+ 112 - 0
game/map/minimap/fog_of_war_mask.h

@@ -0,0 +1,112 @@
+#pragma once
+
+#include <QImage>
+#include <QVector2D>
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+namespace Game::Map::Minimap {
+
+enum class VisibilityState : uint8_t { Unseen = 0, Revealed = 1, Visible = 2 };
+
+struct FogOfWarConfig {
+
+  int update_interval = 15;
+
+  int resolution_divisor = 2;
+
+  int blur_radius = 2;
+
+  uint8_t alpha_unseen = 220;
+  uint8_t alpha_revealed = 120;
+  uint8_t alpha_visible = 0;
+
+  uint8_t fog_color_r = 30;
+  uint8_t fog_color_g = 25;
+  uint8_t fog_color_b = 20;
+
+  FogOfWarConfig() = default;
+};
+
+struct VisionSource {
+  float world_x = 0.0F;
+  float world_z = 0.0F;
+  float vision_radius = 10.0F;
+  int player_id = 0;
+};
+
+class FogOfWarMask {
+public:
+  FogOfWarMask(int map_width, int map_height, float tile_size,
+               const FogOfWarConfig &config = FogOfWarConfig());
+
+  ~FogOfWarMask();
+
+  FogOfWarMask(const FogOfWarMask &) = delete;
+  auto operator=(const FogOfWarMask &) -> FogOfWarMask & = delete;
+  FogOfWarMask(FogOfWarMask &&) noexcept;
+  auto operator=(FogOfWarMask &&) noexcept -> FogOfWarMask &;
+
+  void update_vision(const std::vector<VisionSource> &sources, int player_id);
+
+  auto tick(const std::vector<VisionSource> &sources, int player_id) -> bool;
+
+  [[nodiscard]] auto generate_mask(int target_width,
+                                   int target_height) const -> QImage;
+
+  [[nodiscard]] auto get_visibility(int fog_x,
+                                    int fog_y) const -> VisibilityState;
+
+  [[nodiscard]] auto is_revealed(float world_x, float world_z) const -> bool;
+
+  [[nodiscard]] auto is_visible(float world_x, float world_z) const -> bool;
+
+  void reset();
+
+  void reveal_all();
+
+  [[nodiscard]] auto fog_width() const -> int { return m_fog_width; }
+  [[nodiscard]] auto fog_height() const -> int { return m_fog_height; }
+
+  [[nodiscard]] auto memory_usage() const -> size_t;
+
+  [[nodiscard]] auto is_dirty() const -> bool { return m_dirty; }
+
+  void clear_dirty() { m_dirty = false; }
+
+private:
+  void set_cell(int fog_x, int fog_y, VisibilityState state);
+  [[nodiscard]] auto get_cell(int fog_x, int fog_y) const -> VisibilityState;
+
+  [[nodiscard]] auto world_to_fog(float world_x,
+                                  float world_z) const -> std::pair<int, int>;
+
+  void reveal_circle(int center_x, int center_y, float radius_cells);
+  void clear_current_visibility();
+
+  void apply_gaussian_blur(std::vector<uint8_t> &alpha_buffer, int width,
+                           int height) const;
+
+  FogOfWarConfig m_config;
+
+  int m_map_width;
+  int m_map_height;
+  float m_tile_size;
+
+  int m_fog_width;
+  int m_fog_height;
+  float m_fog_cell_size;
+
+  std::vector<uint8_t> m_visibility_data;
+
+  int m_frame_counter = 0;
+
+  bool m_dirty = true;
+
+  mutable QImage m_cached_mask;
+  mutable int m_cached_width = 0;
+  mutable int m_cached_height = 0;
+};
+
+} // namespace Game::Map::Minimap

+ 6 - 16
game/map/minimap/minimap_example.cpp

@@ -1,11 +1,4 @@
-/**
- * @file minimap_example.cpp
- * @brief Example demonstrating how to use the minimap generation system
- *
- * This example shows how to integrate the minimap generator with the map
- * loading system. In a real application, this would typically be done during
- * map initialization in the game bootstrap or level loader.
- */
+
 
 
 #include "map/map_loader.h"
 #include "map/map_loader.h"
 #include "map/minimap/minimap_generator.h"
 #include "map/minimap/minimap_generator.h"
@@ -19,7 +12,6 @@ using namespace Game::Map::Minimap;
 auto main(int argc, char *argv[]) -> int {
 auto main(int argc, char *argv[]) -> int {
   QCoreApplication app(argc, argv);
   QCoreApplication app(argc, argv);
 
 
-  // Step 1: Load a map from JSON
   qDebug() << "=== Minimap Generation Example ===";
   qDebug() << "=== Minimap Generation Example ===";
   qDebug() << "";
   qDebug() << "";
   qDebug() << "Step 1: Loading map from JSON...";
   qDebug() << "Step 1: Loading map from JSON...";
@@ -34,14 +26,14 @@ auto main(int argc, char *argv[]) -> int {
   }
   }
 
 
   qDebug() << "  ✓ Loaded map:" << mapDef.name;
   qDebug() << "  ✓ Loaded map:" << mapDef.name;
-  qDebug() << "  ✓ Grid size:" << mapDef.grid.width << "x" << mapDef.grid.height;
+  qDebug() << "  ✓ Grid size:" << mapDef.grid.width << "x"
+           << mapDef.grid.height;
   qDebug() << "  ✓ Terrain features:" << mapDef.terrain.size();
   qDebug() << "  ✓ Terrain features:" << mapDef.terrain.size();
   qDebug() << "  ✓ Rivers:" << mapDef.rivers.size();
   qDebug() << "  ✓ Rivers:" << mapDef.rivers.size();
   qDebug() << "  ✓ Roads:" << mapDef.roads.size();
   qDebug() << "  ✓ Roads:" << mapDef.roads.size();
   qDebug() << "  ✓ Spawns:" << mapDef.spawns.size();
   qDebug() << "  ✓ Spawns:" << mapDef.spawns.size();
   qDebug() << "";
   qDebug() << "";
 
 
-  // Step 2: Generate minimap using the generator directly
   qDebug() << "Step 2: Generating minimap texture...";
   qDebug() << "Step 2: Generating minimap texture...";
 
 
   MinimapGenerator generator;
   MinimapGenerator generator;
@@ -53,11 +45,11 @@ auto main(int argc, char *argv[]) -> int {
   }
   }
 
 
   qDebug() << "  ✓ Generated minimap texture";
   qDebug() << "  ✓ Generated minimap texture";
-  qDebug() << "  ✓ Size:" << minimapImage.width() << "x" << minimapImage.height();
+  qDebug() << "  ✓ Size:" << minimapImage.width() << "x"
+           << minimapImage.height();
   qDebug() << "  ✓ Format:" << minimapImage.format();
   qDebug() << "  ✓ Format:" << minimapImage.format();
   qDebug() << "";
   qDebug() << "";
 
 
-  // Step 3: Optionally save the minimap for inspection
   const QString outputPath = "/tmp/minimap_example.png";
   const QString outputPath = "/tmp/minimap_example.png";
   if (minimapImage.save(outputPath)) {
   if (minimapImage.save(outputPath)) {
     qDebug() << "  ✓ Saved minimap to:" << outputPath;
     qDebug() << "  ✓ Saved minimap to:" << outputPath;
@@ -66,8 +58,7 @@ auto main(int argc, char *argv[]) -> int {
   }
   }
   qDebug() << "";
   qDebug() << "";
 
 
-  // Step 4: Demonstrate the texture manager (higher-level API)
-  qDebug() << "Step 3: Using MinimapTextureManager (recommended approach)...";
+  qDebug() << "Step 4: Using MinimapTextureManager (recommended approach)...";
 
 
   MinimapTextureManager manager;
   MinimapTextureManager manager;
   if (!manager.generateForMap(mapDef)) {
   if (!manager.generateForMap(mapDef)) {
@@ -79,7 +70,6 @@ auto main(int argc, char *argv[]) -> int {
   qDebug() << "  ✓ Texture ready for OpenGL upload";
   qDebug() << "  ✓ Texture ready for OpenGL upload";
   qDebug() << "";
   qDebug() << "";
 
 
-  // Step 5: Show integration points
   qDebug() << "=== Integration Guide ===";
   qDebug() << "=== Integration Guide ===";
   qDebug() << "";
   qDebug() << "";
   qDebug() << "In your game initialization code:";
   qDebug() << "In your game initialization code:";

+ 560 - 99
game/map/minimap/minimap_generator.cpp

@@ -1,185 +1,646 @@
 #include "minimap_generator.h"
 #include "minimap_generator.h"
 #include <QColor>
 #include <QColor>
+#include <QLinearGradient>
 #include <QPainter>
 #include <QPainter>
+#include <QPainterPath>
 #include <QPen>
 #include <QPen>
+#include <QRadialGradient>
+#include <algorithm>
 #include <cmath>
 #include <cmath>
+#include <random>
 
 
 namespace Game::Map::Minimap {
 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};
+constexpr QColor PARCHMENT_LIGHT{245, 235, 215};
+constexpr QColor PARCHMENT_DARK{200, 180, 150};
+constexpr QColor PARCHMENT_STAIN{180, 160, 130, 40};
+
+constexpr QColor INK_DARK{45, 35, 25};
+constexpr QColor INK_MEDIUM{80, 65, 50};
+constexpr QColor INK_LIGHT{120, 100, 80};
+
+constexpr QColor MOUNTAIN_SHADOW{95, 80, 65};
+constexpr QColor MOUNTAIN_FACE{140, 125, 105};
+constexpr QColor MOUNTAIN_HIGHLIGHT{180, 165, 145};
+constexpr QColor HILL_BASE{160, 145, 120};
+
+constexpr QColor WATER_DARK{55, 95, 130};
+constexpr QColor WATER_MAIN{75, 120, 160};
+constexpr QColor WATER_LIGHT{100, 145, 180};
+
+constexpr QColor ROAD_MAIN{130, 105, 75};
+constexpr QColor ROAD_HIGHLIGHT{165, 140, 110};
+
+constexpr QColor STRUCTURE_STONE{160, 150, 135};
+constexpr QColor STRUCTURE_SHADOW{100, 85, 70};
+
+constexpr QColor TEAM_BLUE{65, 105, 165};
+constexpr QColor TEAM_BLUE_DARK{40, 65, 100};
+constexpr QColor TEAM_RED{175, 65, 55};
+constexpr QColor TEAM_RED_DARK{110, 40, 35};
+
+} // namespace Palette
+
+auto hash_coords(int x, int y, int seed = 0) -> float {
+  const int n = x + y * 57 + seed * 131;
+  const int shifted = (n << 13) ^ n;
+  return 1.0F -
+         static_cast<float>(
+             (shifted * (shifted * shifted * 15731 + 789221) + 1376312589) &
+             0x7fffffff) /
+             1073741824.0F;
+}
+
+} // namespace
+
 MinimapGenerator::MinimapGenerator() : m_config() {}
 MinimapGenerator::MinimapGenerator() : m_config() {}
 
 
 MinimapGenerator::MinimapGenerator(const Config &config) : m_config(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);
+auto MinimapGenerator::generate(const MapDefinition &map_def) -> QImage {
 
 
-  // Create image with appropriate format
-  QImage image(width, height, QImage::Format_RGBA8888);
-  image.fill(Qt::transparent);
+  const int img_width =
+      static_cast<int>(map_def.grid.width * m_config.pixels_per_tile);
+  const int img_height =
+      static_cast<int>(map_def.grid.height * m_config.pixels_per_tile);
 
 
-  // Render layers in order (back to front)
-  renderTerrainBase(image, mapDef);
-  renderRoads(image, mapDef);
-  renderRivers(image, mapDef);
-  renderTerrainFeatures(image, mapDef);
-  renderStructures(image, mapDef);
+  QImage image(img_width, img_height, QImage::Format_RGBA8888);
+  image.fill(Palette::PARCHMENT_BASE);
+
+  render_parchment_background(image);
+  render_terrain_base(image, map_def);
+  render_terrain_features(image, map_def);
+  render_rivers(image, map_def);
+  render_roads(image, map_def);
+  render_bridges(image, map_def);
+  render_structures(image, map_def);
+  apply_historical_styling(image);
 
 
   return image;
   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);
+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 world_width = grid.width * grid.tile_size;
+  const float world_height = grid.height * grid.tile_size;
+  const float img_width = grid.width * m_config.pixels_per_tile;
+  const float img_height = grid.height * m_config.pixels_per_tile;
+
+  const float px = (rotated_x + world_width * 0.5F) * (img_width / world_width);
+  const float py =
+      (rotated_z + world_height * 0.5F) * (img_height / world_height);
+
+  return {px, py};
+}
+
+auto MinimapGenerator::world_to_pixel_size(
+    float world_size, const GridDefinition &grid) const -> float {
+
+  return (world_size / grid.tile_size) * m_config.pixels_per_tile;
 }
 }
 
 
-void MinimapGenerator::renderTerrainFeatures(QImage &image,
-                                             const MapDefinition &mapDef) {
+void MinimapGenerator::render_parchment_background(QImage &image) {
   QPainter painter(&image);
   QPainter painter(&image);
+
+  for (int y = 0; y < image.height(); ++y) {
+    for (int x = 0; x < image.width(); ++x) {
+
+      const float noise = hash_coords(x / 3, y / 3, 42) * 0.08F;
+
+      QColor pixel = Palette::PARCHMENT_BASE;
+      int r = pixel.red() + static_cast<int>(noise * 20);
+      int g = pixel.green() + static_cast<int>(noise * 18);
+      int b = pixel.blue() + static_cast<int>(noise * 15);
+
+      pixel.setRgb(std::clamp(r, 0, 255), std::clamp(g, 0, 255),
+                   std::clamp(b, 0, 255));
+      image.setPixelColor(x, y, pixel);
+    }
+  }
+
   painter.setRenderHint(QPainter::Antialiasing, true);
   painter.setRenderHint(QPainter::Antialiasing, true);
 
 
-  for (const auto &feature : mapDef.terrain) {
-    const QColor color = terrainFeatureColor(feature.type);
-    painter.setBrush(color);
+  std::mt19937 rng(12345);
+  std::uniform_real_distribution<float> dist_x(
+      0.0F, static_cast<float>(image.width()));
+  std::uniform_real_distribution<float> dist_y(
+      0.0F, static_cast<float>(image.height()));
+  std::uniform_real_distribution<float> dist_size(5.0F, 25.0F);
+  std::uniform_real_distribution<float> dist_alpha(0.02F, 0.06F);
+
+  const int num_stains = (image.width() * image.height()) / 8000;
+  for (int i = 0; i < num_stains; ++i) {
+    const float cx = dist_x(rng);
+    const float cy = dist_y(rng);
+    const float radius = dist_size(rng);
+    const float alpha = dist_alpha(rng);
+
+    QRadialGradient stain(cx, cy, radius);
+    QColor stain_color = Palette::PARCHMENT_STAIN;
+    stain_color.setAlphaF(static_cast<double>(alpha));
+    stain.setColorAt(0, stain_color);
+    stain.setColorAt(1, Qt::transparent);
+
+    painter.setBrush(stain);
     painter.setPen(Qt::NoPen);
     painter.setPen(Qt::NoPen);
+    painter.drawEllipse(QPointF(cx, cy), radius, radius);
+  }
+}
 
 
-    const auto [px, pz] = worldToPixel(feature.center_x, feature.center_z, mapDef.grid);
+void MinimapGenerator::render_terrain_base(QImage &image,
+                                           const MapDefinition &map_def) {
+  QPainter painter(&image);
+  painter.setRenderHint(QPainter::Antialiasing, true);
+
+  const QColor biome_color = biome_to_base_color(map_def.biome);
+
+  painter.setCompositionMode(QPainter::CompositionMode_Multiply);
+  painter.setOpacity(0.15);
+  painter.fillRect(image.rect(), biome_color);
+  painter.setOpacity(1.0);
+  painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
+}
+
+void MinimapGenerator::render_terrain_features(QImage &image,
+                                               const MapDefinition &map_def) {
+  QPainter painter(&image);
+  painter.setRenderHint(QPainter::Antialiasing, true);
 
 
-    // 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;
+  for (const auto &feature : map_def.terrain) {
+    const auto [px, py] =
+        world_to_pixel(feature.center_x, feature.center_z, map_def.grid);
 
 
-    // Draw as ellipse
-    painter.drawEllipse(QPointF(px, pz), pixel_width / 2.0F, pixel_depth / 2.0F);
+    float pixel_width = world_to_pixel_size(feature.width, map_def.grid);
+    float pixel_depth = world_to_pixel_size(feature.depth, map_def.grid);
+
+    constexpr float MIN_FEATURE_SIZE = 4.0F;
+    pixel_width = std::max(pixel_width, MIN_FEATURE_SIZE);
+    pixel_depth = std::max(pixel_depth, MIN_FEATURE_SIZE);
+
+    if (feature.type == TerrainType::Mountain) {
+      draw_mountain_symbol(painter, px, py, pixel_width, pixel_depth);
+    } else if (feature.type == TerrainType::Hill) {
+      draw_hill_symbol(painter, px, py, pixel_width, pixel_depth);
+    }
   }
   }
 }
 }
 
 
-void MinimapGenerator::renderRivers(QImage &image,
-                                    const MapDefinition &mapDef) {
-  if (mapDef.rivers.empty()) {
+void MinimapGenerator::draw_mountain_symbol(QPainter &painter, float cx,
+                                            float cy, float width,
+                                            float height) {
+
+  const float peak_height = height * 0.6F;
+  const float base_width = width * 0.5F;
+
+  QPainterPath shadow_path;
+  shadow_path.moveTo(cx, cy - peak_height);
+  shadow_path.lineTo(cx - base_width, cy + height * 0.3F);
+  shadow_path.lineTo(cx, cy + height * 0.1F);
+  shadow_path.closeSubpath();
+
+  painter.setBrush(Palette::MOUNTAIN_SHADOW);
+  painter.setPen(Qt::NoPen);
+  painter.drawPath(shadow_path);
+
+  QPainterPath lit_path;
+  lit_path.moveTo(cx, cy - peak_height);
+  lit_path.lineTo(cx + base_width, cy + height * 0.3F);
+  lit_path.lineTo(cx, cy + height * 0.1F);
+  lit_path.closeSubpath();
+
+  painter.setBrush(Palette::MOUNTAIN_FACE);
+  painter.drawPath(lit_path);
+
+  QPainterPath snow_path;
+  snow_path.moveTo(cx, cy - peak_height);
+  snow_path.lineTo(cx - base_width * 0.3F, cy - peak_height * 0.5F);
+  snow_path.lineTo(cx + base_width * 0.2F, cy - peak_height * 0.6F);
+  snow_path.closeSubpath();
+
+  painter.setBrush(Palette::MOUNTAIN_HIGHLIGHT);
+  painter.drawPath(snow_path);
+
+  painter.setBrush(Qt::NoBrush);
+  painter.setPen(QPen(Palette::INK_MEDIUM, 0.8));
+
+  QPainterPath outline;
+  outline.moveTo(cx - base_width, cy + height * 0.3F);
+  outline.lineTo(cx, cy - peak_height);
+  outline.lineTo(cx + base_width, cy + height * 0.3F);
+  painter.drawPath(outline);
+}
+
+void MinimapGenerator::draw_hill_symbol(QPainter &painter, float cx, float cy,
+                                        float width, float height) {
+
+  const float hill_height = height * 0.35F;
+  const float base_width = width * 0.6F;
+
+  QPainterPath hill_path;
+  hill_path.moveTo(cx - base_width, cy + hill_height * 0.2F);
+  hill_path.quadTo(cx - base_width * 0.3F, cy - hill_height, cx,
+                   cy - hill_height);
+  hill_path.quadTo(cx + base_width * 0.3F, cy - hill_height, cx + base_width,
+                   cy + hill_height * 0.2F);
+  hill_path.closeSubpath();
+
+  QLinearGradient gradient(cx - base_width, cy, cx + base_width, cy);
+  gradient.setColorAt(0.0, Palette::MOUNTAIN_SHADOW);
+  gradient.setColorAt(0.4, Palette::HILL_BASE);
+  gradient.setColorAt(1.0, Palette::MOUNTAIN_FACE);
+
+  painter.setBrush(gradient);
+  painter.setPen(QPen(Palette::INK_LIGHT, 0.6));
+  painter.drawPath(hill_path);
+}
+
+void MinimapGenerator::render_rivers(QImage &image,
+                                     const MapDefinition &map_def) {
+  if (map_def.rivers.empty()) {
     return;
     return;
   }
   }
 
 
   QPainter painter(&image);
   QPainter painter(&image);
   painter.setRenderHint(QPainter::Antialiasing, true);
   painter.setRenderHint(QPainter::Antialiasing, true);
 
 
-  // River color - light blue
-  const QColor riverColor(100, 150, 255, 200);
+  for (const auto &river : map_def.rivers) {
+    const auto [x1, y1] =
+        world_to_pixel(river.start.x(), river.start.z(), map_def.grid);
+    const auto [x2, y2] =
+        world_to_pixel(river.end.x(), river.end.z(), map_def.grid);
+
+    float pixel_width = world_to_pixel_size(river.width, map_def.grid);
+    pixel_width = std::max(pixel_width, 1.5F);
+
+    draw_river_segment(painter, x1, y1, x2, y2, pixel_width);
+  }
+}
 
 
-  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);
+void MinimapGenerator::draw_river_segment(QPainter &painter, float x1, float y1,
+                                          float x2, float y2, float width) {
 
 
-    const float pixel_width = river.width * m_config.pixels_per_tile * 0.5F;
+  QPainterPath river_path;
+  river_path.moveTo(x1, y1);
 
 
-    QPen pen(riverColor);
-    pen.setWidthF(pixel_width);
-    pen.setCapStyle(Qt::RoundCap);
-    painter.setPen(pen);
+  const float dx = x2 - x1;
+  const float dy = y2 - y1;
+  const float length = std::sqrt(dx * dx + dy * dy);
 
 
-    painter.drawLine(QPointF(x1, z1), QPointF(x2, z2));
+  if (length > 10.0F) {
+
+    const float mid_x = (x1 + x2) * 0.5F;
+    const float mid_y = (y1 + y2) * 0.5F;
+
+    const float perp_x = -dy / length;
+    const float perp_y = dx / length;
+    const float wave_amount =
+        hash_coords(static_cast<int>(x1), static_cast<int>(y1)) * width * 0.5F;
+
+    river_path.quadTo(mid_x + perp_x * wave_amount,
+                      mid_y + perp_y * wave_amount, x2, y2);
+  } else {
+    river_path.lineTo(x2, y2);
+  }
+
+  QPen outline_pen(Palette::WATER_DARK);
+  outline_pen.setWidthF(width * 1.4F);
+  outline_pen.setCapStyle(Qt::RoundCap);
+  outline_pen.setJoinStyle(Qt::RoundJoin);
+  painter.setPen(outline_pen);
+  painter.setBrush(Qt::NoBrush);
+  painter.drawPath(river_path);
+
+  QPen main_pen(Palette::WATER_MAIN);
+  main_pen.setWidthF(width);
+  main_pen.setCapStyle(Qt::RoundCap);
+  main_pen.setJoinStyle(Qt::RoundJoin);
+  painter.setPen(main_pen);
+  painter.drawPath(river_path);
+
+  if (width > 2.0F) {
+    QPen highlight_pen(Palette::WATER_LIGHT);
+    highlight_pen.setWidthF(width * 0.4F);
+    highlight_pen.setCapStyle(Qt::RoundCap);
+    painter.setPen(highlight_pen);
+    painter.drawPath(river_path);
   }
   }
 }
 }
 
 
-void MinimapGenerator::renderRoads(QImage &image,
-                                   const MapDefinition &mapDef) {
-  if (mapDef.roads.empty()) {
+void MinimapGenerator::render_roads(QImage &image,
+                                    const MapDefinition &map_def) {
+  if (map_def.roads.empty()) {
     return;
     return;
   }
   }
 
 
   QPainter painter(&image);
   QPainter painter(&image);
   painter.setRenderHint(QPainter::Antialiasing, true);
   painter.setRenderHint(QPainter::Antialiasing, true);
 
 
-  // Road color - brownish
-  const QColor roadColor(139, 119, 101, 180);
+  for (const auto &road : map_def.roads) {
+    const auto [x1, y1] =
+        world_to_pixel(road.start.x(), road.start.z(), map_def.grid);
+    const auto [x2, y2] =
+        world_to_pixel(road.end.x(), road.end.z(), map_def.grid);
+
+    float pixel_width = world_to_pixel_size(road.width, map_def.grid);
+    pixel_width = std::max(pixel_width, 1.5F);
+
+    draw_road_segment(painter, x1, y1, x2, y2, pixel_width);
+  }
+}
+
+void MinimapGenerator::draw_road_segment(QPainter &painter, float x1, float y1,
+                                         float x2, float y2, float width) {
+
+  QPen road_pen(Palette::ROAD_MAIN);
+  road_pen.setWidthF(width);
+  road_pen.setCapStyle(Qt::RoundCap);
 
 
-  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);
+  QVector<qreal> dash_pattern;
+  dash_pattern << 3.0 << 2.0;
+  road_pen.setDashPattern(dash_pattern);
 
 
-    const float pixel_width = road.width * m_config.pixels_per_tile * 0.4F;
+  painter.setPen(road_pen);
+  painter.drawLine(QPointF(x1, y1), QPointF(x2, y2));
 
 
-    QPen pen(roadColor);
-    pen.setWidthF(pixel_width);
-    pen.setCapStyle(Qt::RoundCap);
-    painter.setPen(pen);
+  const float dx = x2 - x1;
+  const float dy = y2 - y1;
+  const float length = std::sqrt(dx * dx + dy * dy);
 
 
-    painter.drawLine(QPointF(x1, z1), QPointF(x2, z2));
+  if (length > 8.0F) {
+    painter.setPen(Qt::NoPen);
+    painter.setBrush(Palette::ROAD_HIGHLIGHT);
+
+    const int num_dots = static_cast<int>(length / 6.0F);
+    for (int i = 1; i < num_dots; ++i) {
+      const float t = static_cast<float>(i) / static_cast<float>(num_dots);
+      const float dot_x = x1 + dx * t;
+      const float dot_y = y1 + dy * t;
+      painter.drawEllipse(QPointF(dot_x, dot_y), width * 0.25F, width * 0.25F);
+    }
   }
   }
 }
 }
 
 
-void MinimapGenerator::renderStructures(QImage &image,
-                                        const MapDefinition &mapDef) {
-  if (mapDef.spawns.empty()) {
+void MinimapGenerator::render_bridges(QImage &image,
+                                      const MapDefinition &map_def) {
+  if (map_def.bridges.empty()) {
     return;
     return;
   }
   }
 
 
   QPainter painter(&image);
   QPainter painter(&image);
   painter.setRenderHint(QPainter::Antialiasing, true);
   painter.setRenderHint(QPainter::Antialiasing, true);
 
 
-  for (const auto &spawn : mapDef.spawns) {
-    // Only render structure spawns (barracks, etc.)
+  for (const auto &bridge : map_def.bridges) {
+    const auto [x1, y1] =
+        world_to_pixel(bridge.start.x(), bridge.start.z(), map_def.grid);
+    const auto [x2, y2] =
+        world_to_pixel(bridge.end.x(), bridge.end.z(), map_def.grid);
+
+    float pixel_width = world_to_pixel_size(bridge.width, map_def.grid);
+    pixel_width = std::max(pixel_width, 2.0F);
+
+    painter.setPen(QPen(Palette::INK_DARK, 1.0));
+    painter.setBrush(Palette::STRUCTURE_STONE);
+
+    const float dx = x2 - x1;
+    const float dy = y2 - y1;
+    const float length = std::sqrt(dx * dx + dy * dy);
+
+    if (length > 0.01F) {
+
+      const float perp_x = -dy / length * pixel_width * 0.5F;
+      const float perp_y = dx / length * pixel_width * 0.5F;
+
+      QPolygonF bridge_poly;
+      bridge_poly << QPointF(x1 - perp_x, y1 - perp_y)
+                  << QPointF(x1 + perp_x, y1 + perp_y)
+                  << QPointF(x2 + perp_x, y2 + perp_y)
+                  << QPointF(x2 - perp_x, y2 - perp_y);
+      painter.drawPolygon(bridge_poly);
+
+      painter.setPen(QPen(Palette::INK_LIGHT, 0.5));
+      const int num_planks = static_cast<int>(length / 3.0F);
+      for (int i = 1; i < num_planks; ++i) {
+        const float t = static_cast<float>(i) / static_cast<float>(num_planks);
+        const float plank_x = x1 + dx * t;
+        const float plank_y = y1 + dy * t;
+        painter.drawLine(QPointF(plank_x - perp_x, plank_y - perp_y),
+                         QPointF(plank_x + perp_x, plank_y + perp_y));
+      }
+    }
+  }
+}
+
+void MinimapGenerator::render_structures(QImage &image,
+                                         const MapDefinition &map_def) {
+  if (map_def.spawns.empty()) {
+    return;
+  }
+
+  QPainter painter(&image);
+  painter.setRenderHint(QPainter::Antialiasing, true);
+
+  for (const auto &spawn : map_def.spawns) {
     if (!Game::Units::isBuildingSpawn(spawn.type)) {
     if (!Game::Units::isBuildingSpawn(spawn.type)) {
       continue;
       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
-      }
+    const auto [px, py] = world_to_pixel(spawn.x, spawn.z, map_def.grid);
+
+    QColor fill_color = Palette::STRUCTURE_STONE;
+    QColor border_color = Palette::STRUCTURE_SHADOW;
+
+    if (spawn.player_id == 1) {
+      fill_color = Palette::TEAM_BLUE;
+      border_color = Palette::TEAM_BLUE_DARK;
+    } else if (spawn.player_id == 2) {
+      fill_color = Palette::TEAM_RED;
+      border_color = Palette::TEAM_RED_DARK;
+    } else if (spawn.player_id > 0) {
+
+      const int hue = (spawn.player_id * 47 + 30) % 360;
+      fill_color.setHsv(hue, 140, 180);
+      border_color.setHsv(hue, 180, 100);
     }
     }
 
 
-    painter.setBrush(structureColor);
-    painter.setPen(QPen(Qt::black, 0.5F));
+    draw_fortress_icon(painter, px, py, fill_color, border_color);
+  }
+}
+
+void MinimapGenerator::draw_fortress_icon(QPainter &painter, float cx, float cy,
+                                          const QColor &fill,
+                                          const QColor &border) {
 
 
-    const QRectF rect(px - size / 2.0F, pz - size / 2.0F, size, size);
-    painter.drawRect(rect);
+  constexpr float SIZE = 10.0F;
+  constexpr float HALF = SIZE * 0.5F;
+
+  painter.setBrush(fill);
+  painter.setPen(QPen(border, 1.5));
+  painter.drawRect(
+      QRectF(cx - HALF * 0.7F, cy - HALF * 0.7F, SIZE * 0.7F, SIZE * 0.7F));
+
+  constexpr float TOWER_SIZE = SIZE * 0.35F;
+  constexpr float TOWER_OFFSET = HALF * 0.85F;
+
+  painter.setBrush(fill);
+  painter.setPen(QPen(border, 1.0));
+
+  for (int i = 0; i < 4; ++i) {
+    const float tx = cx + ((i & 1) != 0 ? TOWER_OFFSET : -TOWER_OFFSET);
+    const float ty = cy + ((i & 2) != 0 ? TOWER_OFFSET : -TOWER_OFFSET);
+    painter.drawRect(QRectF(tx - TOWER_SIZE * 0.5F, ty - TOWER_SIZE * 0.5F,
+                            TOWER_SIZE, TOWER_SIZE));
   }
   }
+
+  painter.setBrush(border);
+  painter.setPen(Qt::NoPen);
+  painter.drawRect(
+      QRectF(cx - SIZE * 0.12F, cy + SIZE * 0.15F, SIZE * 0.24F, SIZE * 0.25F));
+
+  constexpr float MERLON_W = SIZE * 0.15F;
+  constexpr float MERLON_H = SIZE * 0.12F;
+  painter.setBrush(fill);
+  painter.setPen(QPen(border, 0.8));
+
+  for (int i = 0; i < 3; ++i) {
+    const float mx = cx - SIZE * 0.25F + static_cast<float>(i) * SIZE * 0.25F;
+    const float my = cy - HALF * 0.7F - MERLON_H;
+    painter.drawRect(QRectF(mx, my, MERLON_W, MERLON_H));
+  }
+}
+
+void MinimapGenerator::apply_historical_styling(QImage &image) {
+  QPainter painter(&image);
+  painter.setRenderHint(QPainter::Antialiasing, true);
+
+  draw_map_border(painter, image.width(), image.height());
+
+  apply_vignette(painter, image.width(), image.height());
+
+  draw_compass_rose(painter, image.width(), image.height());
+}
+
+void MinimapGenerator::draw_map_border(QPainter &painter, int width,
+                                       int height) {
+
+  constexpr float OUTER_MARGIN = 2.0F;
+  constexpr float INNER_MARGIN = 5.0F;
+
+  painter.setPen(QPen(Palette::INK_MEDIUM, 1.5));
+  painter.setBrush(Qt::NoBrush);
+  painter.drawRect(QRectF(OUTER_MARGIN, OUTER_MARGIN,
+                          static_cast<float>(width) - OUTER_MARGIN * 2,
+                          static_cast<float>(height) - OUTER_MARGIN * 2));
+
+  painter.setPen(QPen(Palette::INK_LIGHT, 0.8));
+  painter.drawRect(QRectF(INNER_MARGIN, INNER_MARGIN,
+                          static_cast<float>(width) - INNER_MARGIN * 2,
+                          static_cast<float>(height) - INNER_MARGIN * 2));
+}
+
+void MinimapGenerator::apply_vignette(QPainter &painter, int width,
+                                      int height) {
+
+  const float radius = static_cast<float>(std::max(width, height)) * 0.75F;
+  QRadialGradient vignette(static_cast<float>(width) * 0.5F,
+                           static_cast<float>(height) * 0.5F, radius);
+  vignette.setColorAt(0.0, Qt::transparent);
+  vignette.setColorAt(0.7, Qt::transparent);
+  vignette.setColorAt(1.0, QColor(60, 45, 30, 35));
+
+  painter.setCompositionMode(QPainter::CompositionMode_Multiply);
+  painter.fillRect(0, 0, width, height, vignette);
+  painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
 }
 }
 
 
-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};
+void MinimapGenerator::draw_compass_rose(QPainter &painter, int width,
+                                         int height) {
+
+  const float cx = static_cast<float>(width) - 18.0F;
+  const float cy = static_cast<float>(height) - 18.0F;
+  constexpr float SIZE = 10.0F;
+
+  painter.setPen(QPen(Palette::INK_MEDIUM, 1.2));
+  painter.setBrush(Qt::NoBrush);
+
+  QPainterPath north_arrow;
+  north_arrow.moveTo(cx, cy - SIZE);
+  north_arrow.lineTo(cx - SIZE * 0.3F, cy);
+  north_arrow.lineTo(cx + SIZE * 0.3F, cy);
+  north_arrow.closeSubpath();
+
+  painter.setBrush(Palette::INK_DARK);
+  painter.drawPath(north_arrow);
+
+  QPainterPath south_arrow;
+  south_arrow.moveTo(cx, cy + SIZE);
+  south_arrow.lineTo(cx - SIZE * 0.3F, cy);
+  south_arrow.lineTo(cx + SIZE * 0.3F, cy);
+  south_arrow.closeSubpath();
+
+  painter.setBrush(Palette::PARCHMENT_LIGHT);
+  painter.drawPath(south_arrow);
+
+  painter.drawLine(QPointF(cx - SIZE * 0.7F, cy),
+                   QPointF(cx + SIZE * 0.7F, cy));
+
+  painter.setBrush(Palette::INK_MEDIUM);
+  painter.drawEllipse(QPointF(cx, cy), 2.0, 2.0);
+
+  painter.setPen(QPen(Palette::INK_DARK, 1.2F));
+  const float n_left = cx - 3.5F;
+  const float n_right = cx + 3.5F;
+  const float n_top = cy - SIZE - 7.0F;
+  const float n_bottom = cy - SIZE - 1.5F;
+
+  QPainterPath n_path;
+  n_path.moveTo(n_left, n_bottom);
+  n_path.lineTo(n_left, n_top);
+  n_path.lineTo(n_right, n_bottom);
+  n_path.lineTo(n_right, n_top);
+  painter.drawPath(n_path);
 }
 }
 
 
-auto MinimapGenerator::biomeToBaseColor(const BiomeSettings &biome) -> QColor {
-  // Use grass_primary as the base terrain color
+auto MinimapGenerator::biome_to_base_color(const BiomeSettings &biome)
+    -> QColor {
+
   const auto &grass = biome.grass_primary;
   const auto &grass = biome.grass_primary;
-  return QColor::fromRgbF(grass.x(), grass.y(), grass.z());
+  QColor base = QColor::fromRgbF(static_cast<double>(grass.x()),
+                                 static_cast<double>(grass.y()),
+                                 static_cast<double>(grass.z()));
+
+  int h, s, v;
+  base.getHsv(&h, &s, &v);
+  base.setHsv(h, static_cast<int>(s * 0.4), static_cast<int>(v * 0.85));
+
+  return base;
 }
 }
 
 
-auto MinimapGenerator::terrainFeatureColor(TerrainType type) -> QColor {
+auto MinimapGenerator::terrain_feature_color(TerrainType type) -> QColor {
   switch (type) {
   switch (type) {
   case TerrainType::Mountain:
   case TerrainType::Mountain:
-    // Dark gray/brown for mountains
-    return QColor(120, 110, 100, 200);
+    return Palette::MOUNTAIN_SHADOW;
   case TerrainType::Hill:
   case TerrainType::Hill:
-    // Lighter brown for hills
-    return QColor(150, 140, 120, 150);
+    return Palette::HILL_BASE;
   case TerrainType::River:
   case TerrainType::River:
-    // Blue for rivers (though rivers are handled separately)
-    return QColor(100, 150, 255, 200);
+    return Palette::WATER_MAIN;
   case TerrainType::Flat:
   case TerrainType::Flat:
   default:
   default:
-    // Slightly darker grass for flat terrain
-    return QColor(80, 120, 75, 100);
+    return Palette::PARCHMENT_DARK;
   }
   }
 }
 }
 
 

+ 39 - 42
game/map/minimap/minimap_generator.h

@@ -4,66 +4,63 @@
 #include <QImage>
 #include <QImage>
 #include <cstdint>
 #include <cstdint>
 #include <memory>
 #include <memory>
+#include <utility>
+
+class QPainter;
 
 
 namespace Game::Map::Minimap {
 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 {
 class MinimapGenerator {
 public:
 public:
-  /**
-   * @brief Configuration for minimap generation
-   */
   struct Config {
   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
+    float pixels_per_tile = 2.0F;
 
 
-    Config()
-        : resolution_width(256), resolution_height(256), pixels_per_tile(2.0F) {}
+    Config() = default;
   };
   };
 
 
   MinimapGenerator();
   MinimapGenerator();
   explicit MinimapGenerator(const Config &config);
   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;
+  [[nodiscard]] auto generate(const MapDefinition &map_def) -> QImage;
 
 
 private:
 private:
   Config m_config;
   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);
+  void render_parchment_background(QImage &image);
+  void render_terrain_base(QImage &image, const MapDefinition &map_def);
+  void render_terrain_features(QImage &image, const MapDefinition &map_def);
+  void render_rivers(QImage &image, const MapDefinition &map_def);
+  void render_roads(QImage &image, const MapDefinition &map_def);
+  void render_bridges(QImage &image, const MapDefinition &map_def);
+  void render_structures(QImage &image, const MapDefinition &map_def);
+  void apply_historical_styling(QImage &image);
+
+  static void draw_mountain_symbol(QPainter &painter, float cx, float cy,
+                                   float width, float height);
+  static void draw_hill_symbol(QPainter &painter, float cx, float cy,
+                               float width, float height);
+  static void draw_river_segment(QPainter &painter, float x1, float y1,
+                                 float x2, float y2, float width);
+  static void draw_road_segment(QPainter &painter, float x1, float y1, float x2,
+                                float y2, float width);
+  static void draw_fortress_icon(QPainter &painter, float cx, float cy,
+                                 const QColor &fill, const QColor &border);
+
+  static void draw_map_border(QPainter &painter, int width, int height);
+  static void apply_vignette(QPainter &painter, int width, int height);
+  static void draw_compass_rose(QPainter &painter, int width, int height);
+
+  [[nodiscard]] auto
+  world_to_pixel(float world_x, float world_z,
+                 const GridDefinition &grid) const -> std::pair<float, float>;
 
 
-  // Coordinate conversion
-  [[nodiscard]] auto worldToPixel(float world_x, float world_z,
-                                  const GridDefinition &grid) const
-      -> std::pair<int, int>;
+  [[nodiscard]] auto
+  world_to_pixel_size(float world_size,
+                      const GridDefinition &grid) const -> float;
 
 
-  // Color utilities
-  [[nodiscard]] static auto biomeToBaseColor(const BiomeSettings &biome)
-      -> QColor;
-  [[nodiscard]] static auto terrainFeatureColor(TerrainType type) -> QColor;
+  [[nodiscard]] static auto
+  biome_to_base_color(const BiomeSettings &biome) -> QColor;
+  [[nodiscard]] static auto terrain_feature_color(TerrainType type) -> QColor;
 };
 };
 
 
 } // namespace Game::Map::Minimap
 } // namespace Game::Map::Minimap

+ 179 - 0
game/map/minimap/minimap_manager.cpp

@@ -0,0 +1,179 @@
+#include "minimap_manager.h"
+#include <QPainter>
+
+namespace Game::Map::Minimap {
+
+MinimapManager::MinimapManager(const MapDefinition &map_def,
+                               const MinimapManagerConfig &config)
+    : m_config(config), m_grid(map_def.grid) {
+
+  m_generator = std::make_unique<MinimapGenerator>(config.generator_config);
+  m_base_image = m_generator->generate(map_def);
+
+  if (config.fog_enabled) {
+    m_fog_mask = std::make_unique<FogOfWarMask>(
+        map_def.grid.width, map_def.grid.height, map_def.grid.tile_size,
+        config.fog_config);
+  }
+
+  m_composite_dirty = true;
+}
+
+MinimapManager::~MinimapManager() = default;
+
+MinimapManager::MinimapManager(MinimapManager &&) noexcept = default;
+auto MinimapManager::operator=(MinimapManager &&) noexcept -> MinimapManager & =
+                                                                  default;
+
+void MinimapManager::tick(const std::vector<VisionSource> &vision_sources,
+                          int player_id) {
+  if (m_fog_mask && m_config.fog_enabled) {
+    if (m_fog_mask->tick(vision_sources, player_id)) {
+      m_composite_dirty = true;
+    }
+  }
+}
+
+void MinimapManager::force_fog_update(
+    const std::vector<VisionSource> &vision_sources, int player_id) {
+  if (m_fog_mask && m_config.fog_enabled) {
+    m_fog_mask->update_vision(vision_sources, player_id);
+    m_composite_dirty = true;
+  }
+}
+
+auto MinimapManager::get_base_image() const -> const QImage & {
+  return m_base_image;
+}
+
+auto MinimapManager::get_fog_mask() const -> QImage {
+  if (!m_fog_mask || !m_config.fog_enabled) {
+
+    QImage empty(m_base_image.size(), QImage::Format_RGBA8888);
+    empty.fill(Qt::transparent);
+    return empty;
+  }
+
+  return m_fog_mask->generate_mask(m_base_image.width(), m_base_image.height());
+}
+
+auto MinimapManager::get_composite_image() const -> QImage {
+  if (m_composite_dirty) {
+    regenerate_composite();
+  }
+  return m_composite_image;
+}
+
+auto MinimapManager::get_composite_image(int width,
+                                         int height) const -> QImage {
+  QImage composite = get_composite_image();
+
+  if (composite.width() != width || composite.height() != height) {
+    return composite.scaled(width, height, Qt::KeepAspectRatio,
+                            Qt::SmoothTransformation);
+  }
+
+  return composite;
+}
+
+void MinimapManager::regenerate_composite() const {
+
+  m_composite_image = m_base_image.copy();
+
+  if (m_fog_mask && m_config.fog_enabled) {
+    QImage fog = m_fog_mask->generate_mask(m_composite_image.width(),
+                                           m_composite_image.height());
+
+    QPainter painter(&m_composite_image);
+
+    if (m_config.fog_multiply_blend) {
+
+      painter.setCompositionMode(QPainter::CompositionMode_Multiply);
+    } else {
+
+      painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
+    }
+
+    painter.drawImage(0, 0, fog);
+    painter.end();
+  }
+
+  m_composite_dirty = false;
+
+  if (m_fog_mask) {
+    m_fog_mask->clear_dirty();
+  }
+}
+
+auto MinimapManager::is_position_visible(float world_x,
+                                         float world_z) const -> bool {
+  if (!m_fog_mask || !m_config.fog_enabled) {
+    return true;
+  }
+  return m_fog_mask->is_visible(world_x, world_z);
+}
+
+auto MinimapManager::is_position_revealed(float world_x,
+                                          float world_z) const -> bool {
+  if (!m_fog_mask || !m_config.fog_enabled) {
+    return true;
+  }
+  return m_fog_mask->is_revealed(world_x, world_z);
+}
+
+void MinimapManager::reset_fog() {
+  if (m_fog_mask) {
+    m_fog_mask->reset();
+    m_composite_dirty = true;
+  }
+}
+
+void MinimapManager::reveal_all() {
+  if (m_fog_mask) {
+    m_fog_mask->reveal_all();
+    m_composite_dirty = true;
+  }
+}
+
+void MinimapManager::set_fog_enabled(bool enabled) {
+  if (m_config.fog_enabled != enabled) {
+    m_config.fog_enabled = enabled;
+    m_composite_dirty = true;
+
+    if (enabled && !m_fog_mask) {
+      m_fog_mask = std::make_unique<FogOfWarMask>(
+          m_grid.width, m_grid.height, m_grid.tile_size, m_config.fog_config);
+    }
+  }
+}
+
+auto MinimapManager::is_fog_enabled() const -> bool {
+  return m_config.fog_enabled;
+}
+
+auto MinimapManager::memory_usage() const -> size_t {
+  size_t total = sizeof(*this);
+  total += static_cast<size_t>(m_base_image.sizeInBytes());
+  total += static_cast<size_t>(m_composite_image.sizeInBytes());
+
+  if (m_fog_mask) {
+    total += m_fog_mask->memory_usage();
+  }
+
+  return total;
+}
+
+void MinimapManager::regenerate_base(const MapDefinition &map_def) {
+  m_grid = map_def.grid;
+  m_base_image = m_generator->generate(map_def);
+
+  if (m_fog_mask) {
+    m_fog_mask = std::make_unique<FogOfWarMask>(
+        map_def.grid.width, map_def.grid.height, map_def.grid.tile_size,
+        m_config.fog_config);
+  }
+
+  m_composite_dirty = true;
+}
+
+} // namespace Game::Map::Minimap

+ 82 - 0
game/map/minimap/minimap_manager.h

@@ -0,0 +1,82 @@
+#pragma once
+
+#include "fog_of_war_mask.h"
+#include "minimap_generator.h"
+#include <QImage>
+#include <memory>
+#include <vector>
+
+namespace Game::Map::Minimap {
+
+struct MinimapManagerConfig {
+  MinimapGenerator::Config generator_config;
+  FogOfWarConfig fog_config;
+
+  bool fog_enabled = true;
+
+  bool fog_multiply_blend = true;
+
+  MinimapManagerConfig() = default;
+};
+
+class MinimapManager {
+public:
+  MinimapManager(const MapDefinition &map_def,
+                 const MinimapManagerConfig &config = MinimapManagerConfig());
+
+  ~MinimapManager();
+
+  MinimapManager(const MinimapManager &) = delete;
+  auto operator=(const MinimapManager &) -> MinimapManager & = delete;
+  MinimapManager(MinimapManager &&) noexcept;
+  auto operator=(MinimapManager &&) noexcept -> MinimapManager &;
+
+  void tick(const std::vector<VisionSource> &vision_sources, int player_id);
+
+  void force_fog_update(const std::vector<VisionSource> &vision_sources,
+                        int player_id);
+
+  [[nodiscard]] auto get_base_image() const -> const QImage &;
+
+  [[nodiscard]] auto get_fog_mask() const -> QImage;
+
+  [[nodiscard]] auto get_composite_image() const -> QImage;
+
+  [[nodiscard]] auto get_composite_image(int width, int height) const -> QImage;
+
+  [[nodiscard]] auto is_position_visible(float world_x,
+                                         float world_z) const -> bool;
+
+  [[nodiscard]] auto is_position_revealed(float world_x,
+                                          float world_z) const -> bool;
+
+  void reset_fog();
+
+  void reveal_all();
+
+  void set_fog_enabled(bool enabled);
+
+  [[nodiscard]] auto is_fog_enabled() const -> bool;
+
+  [[nodiscard]] auto memory_usage() const -> size_t;
+
+  void regenerate_base(const MapDefinition &map_def);
+
+  [[nodiscard]] auto grid() const -> const GridDefinition & { return m_grid; }
+
+private:
+  void regenerate_composite() const;
+
+  MinimapManagerConfig m_config;
+  GridDefinition m_grid;
+
+  std::unique_ptr<MinimapGenerator> m_generator;
+  QImage m_base_image;
+
+  std::unique_ptr<FogOfWarMask> m_fog_mask;
+
+  mutable QImage m_composite_image;
+  mutable bool m_composite_dirty = true;
+};
+
+} // namespace Game::Map::Minimap

+ 6 - 15
game/map/minimap/minimap_texture_manager.cpp

@@ -10,10 +10,10 @@ MinimapTextureManager::MinimapTextureManager()
 
 
 MinimapTextureManager::~MinimapTextureManager() = default;
 MinimapTextureManager::~MinimapTextureManager() = default;
 
 
-auto MinimapTextureManager::generateForMap(const MapDefinition &mapDef)
+auto MinimapTextureManager::generate_for_map(const MapDefinition &map_def)
     -> bool {
     -> bool {
-  // Generate the minimap image
-  m_image = m_generator->generate(mapDef);
+
+  m_image = m_generator->generate(map_def);
 
 
   if (m_image.isNull()) {
   if (m_image.isNull()) {
     qWarning() << "MinimapTextureManager: Failed to generate minimap image";
     qWarning() << "MinimapTextureManager: Failed to generate minimap image";
@@ -23,26 +23,17 @@ auto MinimapTextureManager::generateForMap(const MapDefinition &mapDef)
   qDebug() << "MinimapTextureManager: Generated minimap of size"
   qDebug() << "MinimapTextureManager: Generated minimap of size"
            << m_image.width() << "x" << m_image.height();
            << m_image.width() << "x" << m_image.height();
 
 
-  // Note: OpenGL texture upload would happen here when an OpenGL context is available
-  // For now, we just store the image. The texture upload would be:
-  // 1. Convert QImage to appropriate format (RGBA8888)
-  // 2. Upload to OpenGL via m_texture->loadFromData() or similar
-  // This is left as a TODO because it requires an active OpenGL context
-
   return true;
   return true;
 }
 }
 
 
-auto MinimapTextureManager::getTexture() const -> Render::GL::Texture * {
+auto MinimapTextureManager::get_texture() const -> Render::GL::Texture * {
   return m_texture.get();
   return m_texture.get();
 }
 }
 
 
-auto MinimapTextureManager::getImage() const -> const QImage & {
+auto MinimapTextureManager::get_image() const -> const QImage & {
   return m_image;
   return m_image;
 }
 }
 
 
-void MinimapTextureManager::clear() {
-  m_image = QImage();
-  // Clear OpenGL texture if needed
-}
+void MinimapTextureManager::clear() { m_image = QImage(); }
 
 
 } // namespace Game::Map::Minimap
 } // namespace Game::Map::Minimap

+ 6 - 29
game/map/minimap/minimap_texture_manager.h

@@ -9,40 +9,17 @@ namespace Game::Map::Minimap {
 
 
 class MinimapGenerator;
 class MinimapGenerator;
 
 
-/**
- * @brief Manages minimap texture lifecycle and integration with the map system.
- *
- * This class provides the glue between the map loading system and the minimap
- * generator. It generates the static minimap texture when a map is loaded and
- * manages the OpenGL texture for rendering.
- */
 class MinimapTextureManager {
 class MinimapTextureManager {
 public:
 public:
   MinimapTextureManager();
   MinimapTextureManager();
   ~MinimapTextureManager();
   ~MinimapTextureManager();
 
 
-  /**
-   * @brief Generates and uploads a minimap texture for the given map definition
-   * @param mapDef The map definition to generate the minimap from
-   * @return true if successful, false otherwise
-   */
-  auto generateForMap(const MapDefinition &mapDef) -> bool;
-
-  /**
-   * @brief Gets the OpenGL texture for rendering
-   * @return Pointer to the texture, or nullptr if not generated
-   */
-  [[nodiscard]] auto getTexture() const -> Render::GL::Texture *;
-
-  /**
-   * @brief Gets the generated minimap image (for debugging/saving)
-   * @return The minimap image
-   */
-  [[nodiscard]] auto getImage() const -> const QImage &;
-
-  /**
-   * @brief Clears the current minimap texture
-   */
+  auto generate_for_map(const MapDefinition &map_def) -> bool;
+
+  [[nodiscard]] auto get_texture() const -> Render::GL::Texture *;
+
+  [[nodiscard]] auto get_image() const -> const QImage &;
+
   void clear();
   void clear();
 
 
 private:
 private:

+ 145 - 0
game/map/minimap/unit_layer.cpp

@@ -0,0 +1,145 @@
+#include "unit_layer.h"
+
+#include <QPainter>
+#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 UnitLayer::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 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 px = (rotated_x + m_offset_x) * m_scale_x;
+  const float py = (rotated_z + m_offset_y) * m_scale_y;
+
+  return {px, py};
+}
+
+void UnitLayer::update(const std::vector<UnitMarker> &markers) {
+  if (m_image.isNull()) {
+    return;
+  }
+
+  m_image.fill(Qt::transparent);
+
+  if (markers.empty()) {
+    return;
+  }
+
+  QPainter painter(&m_image);
+  painter.setRenderHint(QPainter::Antialiasing, true);
+
+  std::vector<const UnitMarker *> buildings;
+  std::vector<const UnitMarker *> units;
+  std::vector<const UnitMarker *> selected;
+
+  for (const auto &marker : markers) {
+    if (marker.is_selected) {
+      selected.push_back(&marker);
+    } else if (marker.is_building) {
+      buildings.push_back(&marker);
+    } else {
+      units.push_back(&marker);
+    }
+  }
+
+  for (const auto *marker : buildings) {
+    const auto [px, py] = world_to_pixel(marker->world_x, marker->world_z);
+    const auto colors = TeamColors::get_color(marker->owner_id);
+    draw_building_marker(painter, px, py, colors, false);
+  }
+
+  for (const auto *marker : units) {
+    const auto [px, py] = world_to_pixel(marker->world_x, marker->world_z);
+    const auto colors = TeamColors::get_color(marker->owner_id);
+    draw_unit_marker(painter, px, py, colors, false);
+  }
+
+  for (const auto *marker : selected) {
+    const auto [px, py] = world_to_pixel(marker->world_x, marker->world_z);
+    const auto colors = TeamColors::get_color(marker->owner_id);
+    if (marker->is_building) {
+      draw_building_marker(painter, px, py, colors, true);
+    } else {
+      draw_unit_marker(painter, px, py, colors, true);
+    }
+  }
+}
+
+void UnitLayer::draw_unit_marker(QPainter &painter, float px, float py,
+                                 const TeamColors::ColorSet &colors,
+                                 bool is_selected) {
+  const QPointF center(static_cast<qreal>(px), static_cast<qreal>(py));
+
+  if (is_selected) {
+    painter.setBrush(Qt::NoBrush);
+    QPen glow_pen(QColor(TeamColors::SELECT_R, TeamColors::SELECT_G,
+                         TeamColors::SELECT_B, 200));
+    glow_pen.setWidthF(2.0);
+    painter.setPen(glow_pen);
+    painter.drawEllipse(center, m_unit_radius + 2.0, m_unit_radius + 2.0);
+  }
+
+  QColor fill_color(colors.r, colors.g, colors.b);
+  QColor border_color(colors.border_r, colors.border_g, colors.border_b);
+
+  painter.setBrush(fill_color);
+  painter.setPen(QPen(border_color, 1.2));
+  painter.drawEllipse(center, m_unit_radius, m_unit_radius);
+}
+
+void UnitLayer::draw_building_marker(QPainter &painter, float px, float py,
+                                     const TeamColors::ColorSet &colors,
+                                     bool is_selected) {
+  const qreal half = static_cast<qreal>(m_building_half_size);
+  const QRectF rect(px - half, py - half, half * 2.0, half * 2.0);
+
+  if (is_selected) {
+    painter.setBrush(Qt::NoBrush);
+    QPen glow_pen(QColor(TeamColors::SELECT_R, TeamColors::SELECT_G,
+                         TeamColors::SELECT_B, 200));
+    glow_pen.setWidthF(2.5);
+    painter.setPen(glow_pen);
+    painter.drawRect(rect.adjusted(-2.5, -2.5, 2.5, 2.5));
+  }
+
+  QColor fill_color(colors.r, colors.g, colors.b);
+  QColor border_color(colors.border_r, colors.border_g, colors.border_b);
+
+  painter.setBrush(fill_color);
+  painter.setPen(QPen(border_color, 1.5));
+  painter.drawRect(rect);
+
+  const qreal inner = half * 0.4;
+  painter.setBrush(border_color);
+  painter.setPen(Qt::NoPen);
+  painter.drawRect(QRectF(px - inner, py - inner, inner * 2.0, inner * 2.0));
+}
+
+} // namespace Game::Map::Minimap

+ 106 - 0
game/map/minimap/unit_layer.h

@@ -0,0 +1,106 @@
+#pragma once
+
+#include <QImage>
+#include <cstdint>
+#include <vector>
+
+class QPainter;
+
+namespace Game::Map::Minimap {
+
+struct UnitMarker {
+  float world_x = 0.0F;
+  float world_z = 0.0F;
+  int owner_id = 0;
+  bool is_selected = false;
+  bool is_building = false;
+};
+
+struct TeamColors {
+  struct ColorSet {
+    std::uint8_t r, g, b;
+    std::uint8_t border_r, border_g, border_b;
+  };
+
+  static constexpr ColorSet PLAYER_1 = {70, 100, 160, 35, 50, 80};
+
+  static constexpr ColorSet PLAYER_2 = {180, 60, 50, 90, 30, 25};
+
+  static constexpr ColorSet PLAYER_3 = {60, 130, 70, 30, 65, 35};
+
+  static constexpr ColorSet PLAYER_4 = {190, 160, 60, 95, 80, 30};
+
+  static constexpr ColorSet PLAYER_5 = {120, 60, 140, 60, 30, 70};
+
+  static constexpr ColorSet PLAYER_6 = {60, 140, 140, 30, 70, 70};
+
+  static constexpr ColorSet NEUTRAL = {100, 95, 85, 50, 48, 43};
+
+  static constexpr std::uint8_t SELECT_R = 255;
+  static constexpr std::uint8_t SELECT_G = 215;
+  static constexpr std::uint8_t SELECT_B = 0;
+
+  static constexpr auto get_color(int owner_id) -> ColorSet {
+    switch (owner_id) {
+    case 1:
+      return PLAYER_1;
+    case 2:
+      return PLAYER_2;
+    case 3:
+      return PLAYER_3;
+    case 4:
+      return PLAYER_4;
+    case 5:
+      return PLAYER_5;
+    case 6:
+      return PLAYER_6;
+    default:
+      return NEUTRAL;
+    }
+  }
+};
+
+class UnitLayer {
+public:
+  UnitLayer() = 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(const std::vector<UnitMarker> &markers);
+
+  [[nodiscard]] auto get_image() const -> const QImage & { return m_image; }
+
+  void set_unit_radius(float radius) { m_unit_radius = radius; }
+
+  void set_building_size(float size) { m_building_half_size = size; }
+
+private:
+  [[nodiscard]] auto
+  world_to_pixel(float world_x, float world_z) const -> std::pair<float, float>;
+
+  void draw_unit_marker(QPainter &painter, float px, float py,
+                        const TeamColors::ColorSet &colors, bool is_selected);
+
+  void draw_building_marker(QPainter &painter, float px, float py,
+                            const TeamColors::ColorSet &colors,
+                            bool is_selected);
+
+  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_unit_radius = 3.0F;
+  float m_building_half_size = 5.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;
+};
+
+} // namespace Game::Map::Minimap

+ 24 - 0
main.cpp

@@ -38,6 +38,7 @@
 #include "app/core/game_engine.h"
 #include "app/core/game_engine.h"
 #include "app/core/language_manager.h"
 #include "app/core/language_manager.h"
 #include "app/models/graphics_settings_proxy.h"
 #include "app/models/graphics_settings_proxy.h"
+#include "app/models/minimap_image_provider.h"
 #include "ui/gl_view.h"
 #include "ui/gl_view.h"
 #include "ui/theme.h"
 #include "ui/theme.h"
 
 
@@ -288,12 +289,35 @@ auto main(int argc, char *argv[]) -> int {
 
 
   qInfo() << "Setting up QML engine...";
   qInfo() << "Setting up QML engine...";
   engine = std::make_unique<QQmlApplicationEngine>();
   engine = std::make_unique<QQmlApplicationEngine>();
+
+  // Register minimap image provider
+  qInfo() << "Registering minimap image provider...";
+  auto *minimap_provider = new MinimapImageProvider();
+  engine->addImageProvider("minimap", minimap_provider);
+
   qInfo() << "Adding context properties...";
   qInfo() << "Adding context properties...";
   engine->rootContext()->setContextProperty("languageManager",
   engine->rootContext()->setContextProperty("languageManager",
                                             language_manager.get());
                                             language_manager.get());
   engine->rootContext()->setContextProperty("game", game_engine.get());
   engine->rootContext()->setContextProperty("game", game_engine.get());
   engine->rootContext()->setContextProperty("graphicsSettings",
   engine->rootContext()->setContextProperty("graphicsSettings",
                                             graphics_settings.get());
                                             graphics_settings.get());
+
+  // Connect minimap image updates to the provider with DirectConnection
+  // This ensures the image is set in the provider BEFORE QML reacts to the
+  // signal
+  QObject::connect(
+      game_engine.get(), &GameEngine::minimap_image_changed, &app,
+      [minimap_provider, game_engine_ptr = game_engine.get()]() {
+        minimap_provider->set_minimap_image(game_engine_ptr->minimap_image());
+      },
+      Qt::DirectConnection);
+
+  // Set initial minimap image if available
+  if (!game_engine->minimap_image().isNull()) {
+    qInfo() << "Setting initial minimap image";
+    minimap_provider->set_minimap_image(game_engine->minimap_image());
+  }
+
   qInfo() << "Adding import path...";
   qInfo() << "Adding import path...";
   engine->addImportPath("qrc:/StandardOfIron/ui/qml");
   engine->addImportPath("qrc:/StandardOfIron/ui/qml");
   engine->addImportPath("qrc:/");
   engine->addImportPath("qrc:/");

+ 26 - 26
render/draw_queue.h

@@ -300,7 +300,7 @@ private:
       SelectionRing = 15
       SelectionRing = 15
     };
     };
 
 
-    static constexpr uint8_t kTypeOrder[] = {
+    static constexpr uint8_t k_type_order[] = {
         static_cast<uint8_t>(RenderOrder::Grid),
         static_cast<uint8_t>(RenderOrder::Grid),
         static_cast<uint8_t>(RenderOrder::SelectionRing),
         static_cast<uint8_t>(RenderOrder::SelectionRing),
         static_cast<uint8_t>(RenderOrder::SelectionSmoke),
         static_cast<uint8_t>(RenderOrder::SelectionSmoke),
@@ -316,65 +316,65 @@ private:
         static_cast<uint8_t>(RenderOrder::TerrainChunk),
         static_cast<uint8_t>(RenderOrder::TerrainChunk),
         static_cast<uint8_t>(RenderOrder::PrimitiveBatch)};
         static_cast<uint8_t>(RenderOrder::PrimitiveBatch)};
 
 
-    const std::size_t typeIndex = cmd.index();
-    constexpr std::size_t typeCount =
-        sizeof(kTypeOrder) / sizeof(kTypeOrder[0]);
-    const uint8_t typeOrder = typeIndex < typeCount
-                                  ? kTypeOrder[typeIndex]
-                                  : static_cast<uint8_t>(typeIndex);
+    const std::size_t type_index = cmd.index();
+    constexpr std::size_t type_count =
+        sizeof(k_type_order) / sizeof(k_type_order[0]);
+    const uint8_t type_order = type_index < type_count
+                                   ? k_type_order[type_index]
+                                   : static_cast<uint8_t>(type_index);
 
 
-    uint64_t key = static_cast<uint64_t>(typeOrder) << 56;
+    uint64_t key = static_cast<uint64_t>(type_order) << 56;
 
 
     if (cmd.index() == MeshCmdIndex) {
     if (cmd.index() == MeshCmdIndex) {
       const auto &mesh = std::get<MeshCmdIndex>(cmd);
       const auto &mesh = std::get<MeshCmdIndex>(cmd);
 
 
-      uint64_t const texPtr =
+      uint64_t const tex_ptr =
           reinterpret_cast<uintptr_t>(mesh.texture) & 0x0000FFFFFFFFFFFF;
           reinterpret_cast<uintptr_t>(mesh.texture) & 0x0000FFFFFFFFFFFF;
-      key |= texPtr;
+      key |= tex_ptr;
     } else if (cmd.index() == GrassBatchCmdIndex) {
     } else if (cmd.index() == GrassBatchCmdIndex) {
       const auto &grass = std::get<GrassBatchCmdIndex>(cmd);
       const auto &grass = std::get<GrassBatchCmdIndex>(cmd);
-      uint64_t const bufferPtr =
+      uint64_t const buffer_ptr =
           reinterpret_cast<uintptr_t>(grass.instance_buffer) &
           reinterpret_cast<uintptr_t>(grass.instance_buffer) &
           0x0000FFFFFFFFFFFF;
           0x0000FFFFFFFFFFFF;
-      key |= bufferPtr;
+      key |= buffer_ptr;
     } else if (cmd.index() == StoneBatchCmdIndex) {
     } else if (cmd.index() == StoneBatchCmdIndex) {
       const auto &stone = std::get<StoneBatchCmdIndex>(cmd);
       const auto &stone = std::get<StoneBatchCmdIndex>(cmd);
-      uint64_t const bufferPtr =
+      uint64_t const buffer_ptr =
           reinterpret_cast<uintptr_t>(stone.instance_buffer) &
           reinterpret_cast<uintptr_t>(stone.instance_buffer) &
           0x0000FFFFFFFFFFFF;
           0x0000FFFFFFFFFFFF;
-      key |= bufferPtr;
+      key |= buffer_ptr;
     } else if (cmd.index() == PlantBatchCmdIndex) {
     } else if (cmd.index() == PlantBatchCmdIndex) {
       const auto &plant = std::get<PlantBatchCmdIndex>(cmd);
       const auto &plant = std::get<PlantBatchCmdIndex>(cmd);
-      uint64_t const bufferPtr =
+      uint64_t const buffer_ptr =
           reinterpret_cast<uintptr_t>(plant.instance_buffer) &
           reinterpret_cast<uintptr_t>(plant.instance_buffer) &
           0x0000FFFFFFFFFFFF;
           0x0000FFFFFFFFFFFF;
-      key |= bufferPtr;
+      key |= buffer_ptr;
     } else if (cmd.index() == PineBatchCmdIndex) {
     } else if (cmd.index() == PineBatchCmdIndex) {
       const auto &pine = std::get<PineBatchCmdIndex>(cmd);
       const auto &pine = std::get<PineBatchCmdIndex>(cmd);
-      uint64_t const bufferPtr =
+      uint64_t const buffer_ptr =
           reinterpret_cast<uintptr_t>(pine.instance_buffer) &
           reinterpret_cast<uintptr_t>(pine.instance_buffer) &
           0x0000FFFFFFFFFFFF;
           0x0000FFFFFFFFFFFF;
-      key |= bufferPtr;
+      key |= buffer_ptr;
     } else if (cmd.index() == OliveBatchCmdIndex) {
     } else if (cmd.index() == OliveBatchCmdIndex) {
       const auto &olive = std::get<OliveBatchCmdIndex>(cmd);
       const auto &olive = std::get<OliveBatchCmdIndex>(cmd);
-      uint64_t const bufferPtr =
+      uint64_t const buffer_ptr =
           reinterpret_cast<uintptr_t>(olive.instance_buffer) &
           reinterpret_cast<uintptr_t>(olive.instance_buffer) &
           0x0000FFFFFFFFFFFF;
           0x0000FFFFFFFFFFFF;
-      key |= bufferPtr;
+      key |= buffer_ptr;
     } else if (cmd.index() == FireCampBatchCmdIndex) {
     } else if (cmd.index() == FireCampBatchCmdIndex) {
       const auto &firecamp = std::get<FireCampBatchCmdIndex>(cmd);
       const auto &firecamp = std::get<FireCampBatchCmdIndex>(cmd);
-      uint64_t const bufferPtr =
+      uint64_t const buffer_ptr =
           reinterpret_cast<uintptr_t>(firecamp.instance_buffer) &
           reinterpret_cast<uintptr_t>(firecamp.instance_buffer) &
           0x0000FFFFFFFFFFFF;
           0x0000FFFFFFFFFFFF;
-      key |= bufferPtr;
+      key |= buffer_ptr;
     } else if (cmd.index() == TerrainChunkCmdIndex) {
     } else if (cmd.index() == TerrainChunkCmdIndex) {
       const auto &terrain = std::get<TerrainChunkCmdIndex>(cmd);
       const auto &terrain = std::get<TerrainChunkCmdIndex>(cmd);
-      auto const sortByte =
+      auto const sort_byte =
           static_cast<uint64_t>((terrain.sort_key >> 8) & 0xFFU);
           static_cast<uint64_t>((terrain.sort_key >> 8) & 0xFFU);
-      key |= sortByte << 48;
-      uint64_t const meshPtr =
+      key |= sort_byte << 48;
+      uint64_t const mesh_ptr =
           reinterpret_cast<uintptr_t>(terrain.mesh) & 0x0000FFFFFFFFFFFFU;
           reinterpret_cast<uintptr_t>(terrain.mesh) & 0x0000FFFFFFFFFFFFU;
-      key |= meshPtr;
+      key |= mesh_ptr;
     } else if (cmd.index() == PrimitiveBatchCmdIndex) {
     } else if (cmd.index() == PrimitiveBatchCmdIndex) {
       const auto &prim = std::get<PrimitiveBatchCmdIndex>(cmd);
       const auto &prim = std::get<PrimitiveBatchCmdIndex>(cmd);
 
 

+ 75 - 73
render/draw_queue_soa.h

@@ -7,15 +7,15 @@
 #include <QMatrix4x4>
 #include <QMatrix4x4>
 #include <QVector3D>
 #include <QVector3D>
 #include <algorithm>
 #include <algorithm>
-<parameter name = "cstddef">
+#include <cstddef>
 #include <cstdint>
 #include <cstdint>
 #include <vector>
 #include <vector>
 
 
-    namespace Render::GL {
-  class Mesh;
-  class Texture;
-  class Buffer;
-}
+namespace Render::GL {
+class Mesh;
+class Texture;
+class Buffer;
+} // namespace Render::GL
 
 
 namespace Render::GL {
 namespace Render::GL {
 
 
@@ -49,19 +49,19 @@ struct FogBatchCmd {
 };
 };
 
 
 struct GrassBatchCmd {
 struct GrassBatchCmd {
-  Buffer *instanceBuffer = nullptr;
+  Buffer *instance_buffer = nullptr;
   std::size_t instance_count = 0;
   std::size_t instance_count = 0;
   GrassBatchParams params;
   GrassBatchParams params;
 };
 };
 
 
 struct StoneBatchCmd {
 struct StoneBatchCmd {
-  Buffer *instanceBuffer = nullptr;
+  Buffer *instance_buffer = nullptr;
   std::size_t instance_count = 0;
   std::size_t instance_count = 0;
   StoneBatchParams params;
   StoneBatchParams params;
 };
 };
 
 
 struct PlantBatchCmd {
 struct PlantBatchCmd {
-  Buffer *instanceBuffer = nullptr;
+  Buffer *instance_buffer = nullptr;
   std::size_t instance_count = 0;
   std::size_t instance_count = 0;
   PlantBatchParams params;
   PlantBatchParams params;
 };
 };
@@ -70,16 +70,16 @@ struct TerrainChunkCmd {
   Mesh *mesh = nullptr;
   Mesh *mesh = nullptr;
   QMatrix4x4 model;
   QMatrix4x4 model;
   TerrainChunkParams params;
   TerrainChunkParams params;
-  std::uint16_t sortKey = 0x8000u;
-  bool depthWrite = true;
-  float depthBias = 0.0F;
+  std::uint16_t sort_key = 0x8000u;
+  bool depth_write = true;
+  float depth_bias = 0.0F;
 };
 };
 
 
 struct GridCmd {
 struct GridCmd {
   QMatrix4x4 model;
   QMatrix4x4 model;
   QMatrix4x4 mvp;
   QMatrix4x4 mvp;
   QVector3D color{0.2F, 0.25F, 0.2F};
   QVector3D color{0.2F, 0.25F, 0.2F};
-  float cellSize = 1.0F;
+  float cell_size = 1.0F;
   float thickness = 0.06F;
   float thickness = 0.06F;
   float extent = 50.0F;
   float extent = 50.0F;
 };
 };
@@ -88,107 +88,109 @@ struct SelectionRingCmd {
   QMatrix4x4 model;
   QMatrix4x4 model;
   QMatrix4x4 mvp;
   QMatrix4x4 mvp;
   QVector3D color{0, 0, 0};
   QVector3D color{0, 0, 0};
-  float alphaInner = 0.6F;
-  float alphaOuter = 0.25F;
+  float alpha_inner = 0.6F;
+  float alpha_outer = 0.25F;
 };
 };
 
 
 struct SelectionSmokeCmd {
 struct SelectionSmokeCmd {
   QMatrix4x4 model;
   QMatrix4x4 model;
   QMatrix4x4 mvp;
   QMatrix4x4 mvp;
   QVector3D color{1, 1, 1};
   QVector3D color{1, 1, 1};
-  float baseAlpha = 0.15F;
+  float base_alpha = 0.15F;
 };
 };
 
 
 class DrawQueueSoA {
 class DrawQueueSoA {
 public:
 public:
   void clear() {
   void clear() {
-    m_gridCmds.clear();
-    m_selectionRingCmds.clear();
-    m_selectionSmokeCmds.clear();
-    m_cylinderCmds.clear();
-    m_meshCmds.clear();
-    m_fogBatchCmds.clear();
-    m_grassBatchCmds.clear();
-    m_stoneBatchCmds.clear();
-    m_plantBatchCmds.clear();
-    m_terrainChunkCmds.clear();
+    m_grid_cmds.clear();
+    m_selection_ring_cmds.clear();
+    m_selection_smoke_cmds.clear();
+    m_cylinder_cmds.clear();
+    m_mesh_cmds.clear();
+    m_fog_batch_cmds.clear();
+    m_grass_batch_cmds.clear();
+    m_stone_batch_cmds.clear();
+    m_plant_batch_cmds.clear();
+    m_terrain_chunk_cmds.clear();
   }
   }
 
 
-  void submit(const GridCmd &cmd) { m_gridCmds.push_back(cmd); }
+  void submit(const GridCmd &cmd) { m_grid_cmds.push_back(cmd); }
   void submit(const SelectionRingCmd &cmd) {
   void submit(const SelectionRingCmd &cmd) {
-    m_selectionRingCmds.push_back(cmd);
+    m_selection_ring_cmds.push_back(cmd);
   }
   }
   void submit(const SelectionSmokeCmd &cmd) {
   void submit(const SelectionSmokeCmd &cmd) {
-    m_selectionSmokeCmds.push_back(cmd);
+    m_selection_smoke_cmds.push_back(cmd);
+  }
+  void submit(const CylinderCmd &cmd) { m_cylinder_cmds.push_back(cmd); }
+  void submit(const MeshCmd &cmd) { m_mesh_cmds.push_back(cmd); }
+  void submit(const FogBatchCmd &cmd) { m_fog_batch_cmds.push_back(cmd); }
+  void submit(const GrassBatchCmd &cmd) { m_grass_batch_cmds.push_back(cmd); }
+  void submit(const StoneBatchCmd &cmd) { m_stone_batch_cmds.push_back(cmd); }
+  void submit(const PlantBatchCmd &cmd) { m_plant_batch_cmds.push_back(cmd); }
+  void submit(const TerrainChunkCmd &cmd) {
+    m_terrain_chunk_cmds.push_back(cmd);
   }
   }
-  void submit(const CylinderCmd &cmd) { m_cylinderCmds.push_back(cmd); }
-  void submit(const MeshCmd &cmd) { m_meshCmds.push_back(cmd); }
-  void submit(const FogBatchCmd &cmd) { m_fogBatchCmds.push_back(cmd); }
-  void submit(const GrassBatchCmd &cmd) { m_grassBatchCmds.push_back(cmd); }
-  void submit(const StoneBatchCmd &cmd) { m_stoneBatchCmds.push_back(cmd); }
-  void submit(const PlantBatchCmd &cmd) { m_plantBatchCmds.push_back(cmd); }
-  void submit(const TerrainChunkCmd &cmd) { m_terrainChunkCmds.push_back(cmd); }
 
 
   bool empty() const {
   bool empty() const {
-    return m_gridCmds.empty() && m_selectionRingCmds.empty() &&
-           m_selectionSmokeCmds.empty() && m_cylinderCmds.empty() &&
-           m_meshCmds.empty() && m_fogBatchCmds.empty() &&
-           m_grassBatchCmds.empty() && m_stoneBatchCmds.empty() &&
-           m_plantBatchCmds.empty() && m_terrainChunkCmds.empty();
+    return m_grid_cmds.empty() && m_selection_ring_cmds.empty() &&
+           m_selection_smoke_cmds.empty() && m_cylinder_cmds.empty() &&
+           m_mesh_cmds.empty() && m_fog_batch_cmds.empty() &&
+           m_grass_batch_cmds.empty() && m_stone_batch_cmds.empty() &&
+           m_plant_batch_cmds.empty() && m_terrain_chunk_cmds.empty();
   }
   }
 
 
-  void sortForBatching() {
+  void sort_for_batching() {
 
 
-    std::sort(m_meshCmds.begin(), m_meshCmds.end(),
+    std::sort(m_mesh_cmds.begin(), m_mesh_cmds.end(),
               [](const MeshCmd &a, const MeshCmd &b) {
               [](const MeshCmd &a, const MeshCmd &b) {
                 return reinterpret_cast<uintptr_t>(a.texture) <
                 return reinterpret_cast<uintptr_t>(a.texture) <
                        reinterpret_cast<uintptr_t>(b.texture);
                        reinterpret_cast<uintptr_t>(b.texture);
               });
               });
 
 
-    std::sort(m_terrainChunkCmds.begin(), m_terrainChunkCmds.end(),
+    std::sort(m_terrain_chunk_cmds.begin(), m_terrain_chunk_cmds.end(),
               [](const TerrainChunkCmd &a, const TerrainChunkCmd &b) {
               [](const TerrainChunkCmd &a, const TerrainChunkCmd &b) {
-                return a.sortKey < b.sortKey;
+                return a.sort_key < b.sort_key;
               });
               });
   }
   }
 
 
-  const std::vector<GridCmd> &gridCmds() const { return m_gridCmds; }
-  const std::vector<SelectionRingCmd> &selectionRingCmds() const {
-    return m_selectionRingCmds;
+  const std::vector<GridCmd> &grid_cmds() const { return m_grid_cmds; }
+  const std::vector<SelectionRingCmd> &selection_ring_cmds() const {
+    return m_selection_ring_cmds;
   }
   }
-  const std::vector<SelectionSmokeCmd> &selectionSmokeCmds() const {
-    return m_selectionSmokeCmds;
+  const std::vector<SelectionSmokeCmd> &selection_smoke_cmds() const {
+    return m_selection_smoke_cmds;
   }
   }
-  const std::vector<CylinderCmd> &cylinderCmds() const {
-    return m_cylinderCmds;
+  const std::vector<CylinderCmd> &cylinder_cmds() const {
+    return m_cylinder_cmds;
   }
   }
-  const std::vector<MeshCmd> &meshCmds() const { return m_meshCmds; }
-  const std::vector<FogBatchCmd> &fogBatchCmds() const {
-    return m_fogBatchCmds;
+  const std::vector<MeshCmd> &mesh_cmds() const { return m_mesh_cmds; }
+  const std::vector<FogBatchCmd> &fog_batch_cmds() const {
+    return m_fog_batch_cmds;
   }
   }
-  const std::vector<GrassBatchCmd> &grassBatchCmds() const {
-    return m_grassBatchCmds;
+  const std::vector<GrassBatchCmd> &grass_batch_cmds() const {
+    return m_grass_batch_cmds;
   }
   }
-  const std::vector<StoneBatchCmd> &stoneBatchCmds() const {
-    return m_stoneBatchCmds;
+  const std::vector<StoneBatchCmd> &stone_batch_cmds() const {
+    return m_stone_batch_cmds;
   }
   }
-  const std::vector<PlantBatchCmd> &plantBatchCmds() const {
-    return m_plantBatchCmds;
+  const std::vector<PlantBatchCmd> &plant_batch_cmds() const {
+    return m_plant_batch_cmds;
   }
   }
-  const std::vector<TerrainChunkCmd> &terrainChunkCmds() const {
-    return m_terrainChunkCmds;
+  const std::vector<TerrainChunkCmd> &terrain_chunk_cmds() const {
+    return m_terrain_chunk_cmds;
   }
   }
 
 
 private:
 private:
-  std::vector<GridCmd> m_gridCmds;
-  std::vector<SelectionRingCmd> m_selectionRingCmds;
-  std::vector<SelectionSmokeCmd> m_selectionSmokeCmds;
-  std::vector<CylinderCmd> m_cylinderCmds;
-  std::vector<MeshCmd> m_meshCmds;
-  std::vector<FogBatchCmd> m_fogBatchCmds;
-  std::vector<GrassBatchCmd> m_grassBatchCmds;
-  std::vector<StoneBatchCmd> m_stoneBatchCmds;
-  std::vector<PlantBatchCmd> m_plantBatchCmds;
-  std::vector<TerrainChunkCmd> m_terrainChunkCmds;
+  std::vector<GridCmd> m_grid_cmds;
+  std::vector<SelectionRingCmd> m_selection_ring_cmds;
+  std::vector<SelectionSmokeCmd> m_selection_smoke_cmds;
+  std::vector<CylinderCmd> m_cylinder_cmds;
+  std::vector<MeshCmd> m_mesh_cmds;
+  std::vector<FogBatchCmd> m_fog_batch_cmds;
+  std::vector<GrassBatchCmd> m_grass_batch_cmds;
+  std::vector<StoneBatchCmd> m_stone_batch_cmds;
+  std::vector<PlantBatchCmd> m_plant_batch_cmds;
+  std::vector<TerrainChunkCmd> m_terrain_chunk_cmds;
 };
 };
 
 
 } // namespace Render::GL
 } // namespace Render::GL

+ 10 - 10
render/gl/backend.cpp

@@ -1422,21 +1422,21 @@ void Backend::execute(const DrawQueue &queue, const Camera &cam) {
 
 
       switch (batch.type) {
       switch (batch.type) {
       case PrimitiveType::Sphere:
       case PrimitiveType::Sphere:
-        m_primitiveBatchPipeline->uploadSphereInstances(data,
-                                                        batch.instance_count());
-        m_primitiveBatchPipeline->drawSpheres(batch.instance_count(),
-                                              view_proj);
+        m_primitiveBatchPipeline->upload_sphere_instances(
+            data, batch.instance_count());
+        m_primitiveBatchPipeline->draw_spheres(batch.instance_count(),
+                                               view_proj);
         break;
         break;
       case PrimitiveType::Cylinder:
       case PrimitiveType::Cylinder:
-        m_primitiveBatchPipeline->uploadCylinderInstances(
+        m_primitiveBatchPipeline->upload_cylinder_instances(
             data, batch.instance_count());
             data, batch.instance_count());
-        m_primitiveBatchPipeline->drawCylinders(batch.instance_count(),
-                                                view_proj);
+        m_primitiveBatchPipeline->draw_cylinders(batch.instance_count(),
+                                                 view_proj);
         break;
         break;
       case PrimitiveType::Cone:
       case PrimitiveType::Cone:
-        m_primitiveBatchPipeline->uploadConeInstances(data,
-                                                      batch.instance_count());
-        m_primitiveBatchPipeline->drawCones(batch.instance_count(), view_proj);
+        m_primitiveBatchPipeline->upload_cone_instances(data,
+                                                        batch.instance_count());
+        m_primitiveBatchPipeline->draw_cones(batch.instance_count(), view_proj);
         break;
         break;
       }
       }
 
 

+ 124 - 121
render/gl/backend/primitive_batch_pipeline.cpp

@@ -14,25 +14,25 @@ using namespace Render::GL::VertexAttrib;
 using namespace Render::GL::ComponentCount;
 using namespace Render::GL::ComponentCount;
 
 
 PrimitiveBatchPipeline::PrimitiveBatchPipeline(ShaderCache *shaderCache)
 PrimitiveBatchPipeline::PrimitiveBatchPipeline(ShaderCache *shaderCache)
-    : m_shaderCache(shaderCache) {}
+    : m_shader_cache(shaderCache) {}
 
 
 PrimitiveBatchPipeline::~PrimitiveBatchPipeline() { shutdown(); }
 PrimitiveBatchPipeline::~PrimitiveBatchPipeline() { shutdown(); }
 
 
 auto PrimitiveBatchPipeline::initialize() -> bool {
 auto PrimitiveBatchPipeline::initialize() -> bool {
   initializeOpenGLFunctions();
   initializeOpenGLFunctions();
 
 
-  if (m_shaderCache == nullptr) {
+  if (m_shader_cache == nullptr) {
     return false;
     return false;
   }
   }
 
 
-  m_shader = m_shaderCache->get(QStringLiteral("primitive_instanced"));
+  m_shader = m_shader_cache->get(QStringLiteral("primitive_instanced"));
   if (m_shader == nullptr) {
   if (m_shader == nullptr) {
     return false;
     return false;
   }
   }
 
 
-  initializeSphereVao();
-  initializeCylinderVao();
-  initializeConeVao();
+  initialize_sphere_vao();
+  initialize_cylinder_vao();
+  initialize_cone_vao();
   cacheUniforms();
   cacheUniforms();
 
 
   m_initialized = true;
   m_initialized = true;
@@ -40,7 +40,7 @@ auto PrimitiveBatchPipeline::initialize() -> bool {
 }
 }
 
 
 void PrimitiveBatchPipeline::shutdown() {
 void PrimitiveBatchPipeline::shutdown() {
-  shutdownVaos();
+  shutdown_vaos();
   m_initialized = false;
   m_initialized = false;
 }
 }
 
 
@@ -52,12 +52,12 @@ void PrimitiveBatchPipeline::cacheUniforms() {
   }
   }
 }
 }
 
 
-void PrimitiveBatchPipeline::beginFrame() {}
+void PrimitiveBatchPipeline::begin_frame() {}
 
 
-void PrimitiveBatchPipeline::setupInstanceAttributes(GLuint vao,
-                                                     GLuint instanceBuffer) {
+void PrimitiveBatchPipeline::setup_instance_attributes(GLuint vao,
+                                                       GLuint instance_buffer) {
   glBindVertexArray(vao);
   glBindVertexArray(vao);
-  glBindBuffer(GL_ARRAY_BUFFER, instanceBuffer);
+  glBindBuffer(GL_ARRAY_BUFFER, instance_buffer);
 
 
   const auto stride = static_cast<GLsizei>(sizeof(GL::PrimitiveInstanceGpu));
   const auto stride = static_cast<GLsizei>(sizeof(GL::PrimitiveInstanceGpu));
 
 
@@ -88,7 +88,7 @@ void PrimitiveBatchPipeline::setupInstanceAttributes(GLuint vao,
   glBindVertexArray(0);
   glBindVertexArray(0);
 }
 }
 
 
-void PrimitiveBatchPipeline::initializeSphereVao() {
+void PrimitiveBatchPipeline::initialize_sphere_vao() {
   Mesh *unit = getUnitSphere();
   Mesh *unit = getUnitSphere();
   if (unit == nullptr) {
   if (unit == nullptr) {
     return;
     return;
@@ -100,19 +100,19 @@ void PrimitiveBatchPipeline::initializeSphereVao() {
     return;
     return;
   }
   }
 
 
-  glGenVertexArrays(1, &m_sphereVao);
-  glBindVertexArray(m_sphereVao);
+  glGenVertexArrays(1, &m_sphere_vao);
+  glBindVertexArray(m_sphere_vao);
 
 
-  glGenBuffers(1, &m_sphereVertexBuffer);
-  glBindBuffer(GL_ARRAY_BUFFER, m_sphereVertexBuffer);
+  glGenBuffers(1, &m_sphere_vertex_buffer);
+  glBindBuffer(GL_ARRAY_BUFFER, m_sphere_vertex_buffer);
   glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex),
   glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex),
                vertices.data(), GL_STATIC_DRAW);
                vertices.data(), GL_STATIC_DRAW);
 
 
-  glGenBuffers(1, &m_sphereIndexBuffer);
-  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_sphereIndexBuffer);
+  glGenBuffers(1, &m_sphere_index_buffer);
+  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_sphere_index_buffer);
   glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int),
   glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int),
                indices.data(), GL_STATIC_DRAW);
                indices.data(), GL_STATIC_DRAW);
-  m_sphereIndexCount = static_cast<GLsizei>(indices.size());
+  m_sphere_index_count = static_cast<GLsizei>(indices.size());
 
 
   glEnableVertexAttribArray(Position);
   glEnableVertexAttribArray(Position);
   glVertexAttribPointer(Position, Vec3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
   glVertexAttribPointer(Position, Vec3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
@@ -124,18 +124,18 @@ void PrimitiveBatchPipeline::initializeSphereVao() {
   glVertexAttribPointer(TexCoord, Vec2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
   glVertexAttribPointer(TexCoord, Vec2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
                         reinterpret_cast<void *>(offsetof(Vertex, tex_coord)));
                         reinterpret_cast<void *>(offsetof(Vertex, tex_coord)));
 
 
-  glGenBuffers(1, &m_sphereInstanceBuffer);
-  glBindBuffer(GL_ARRAY_BUFFER, m_sphereInstanceBuffer);
-  m_sphereInstanceCapacity = kDefaultInstanceCapacity;
+  glGenBuffers(1, &m_sphere_instance_buffer);
+  glBindBuffer(GL_ARRAY_BUFFER, m_sphere_instance_buffer);
+  m_sphere_instance_capacity = k_default_instance_capacity;
   glBufferData(GL_ARRAY_BUFFER,
   glBufferData(GL_ARRAY_BUFFER,
-               m_sphereInstanceCapacity * sizeof(GL::PrimitiveInstanceGpu),
+               m_sphere_instance_capacity * sizeof(GL::PrimitiveInstanceGpu),
                nullptr, GL_DYNAMIC_DRAW);
                nullptr, GL_DYNAMIC_DRAW);
 
 
-  setupInstanceAttributes(m_sphereVao, m_sphereInstanceBuffer);
+  setup_instance_attributes(m_sphere_vao, m_sphere_instance_buffer);
   glBindVertexArray(0);
   glBindVertexArray(0);
 }
 }
 
 
-void PrimitiveBatchPipeline::initializeCylinderVao() {
+void PrimitiveBatchPipeline::initialize_cylinder_vao() {
   Mesh *unit = getUnitCylinder();
   Mesh *unit = getUnitCylinder();
   if (unit == nullptr) {
   if (unit == nullptr) {
     return;
     return;
@@ -147,19 +147,19 @@ void PrimitiveBatchPipeline::initializeCylinderVao() {
     return;
     return;
   }
   }
 
 
-  glGenVertexArrays(1, &m_cylinderVao);
-  glBindVertexArray(m_cylinderVao);
+  glGenVertexArrays(1, &m_cylinder_vao);
+  glBindVertexArray(m_cylinder_vao);
 
 
-  glGenBuffers(1, &m_cylinderVertexBuffer);
-  glBindBuffer(GL_ARRAY_BUFFER, m_cylinderVertexBuffer);
+  glGenBuffers(1, &m_cylinder_vertex_buffer);
+  glBindBuffer(GL_ARRAY_BUFFER, m_cylinder_vertex_buffer);
   glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex),
   glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex),
                vertices.data(), GL_STATIC_DRAW);
                vertices.data(), GL_STATIC_DRAW);
 
 
-  glGenBuffers(1, &m_cylinderIndexBuffer);
-  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_cylinderIndexBuffer);
+  glGenBuffers(1, &m_cylinder_index_buffer);
+  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_cylinder_index_buffer);
   glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int),
   glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int),
                indices.data(), GL_STATIC_DRAW);
                indices.data(), GL_STATIC_DRAW);
-  m_cylinderIndexCount = static_cast<GLsizei>(indices.size());
+  m_cylinder_index_count = static_cast<GLsizei>(indices.size());
 
 
   glEnableVertexAttribArray(Position);
   glEnableVertexAttribArray(Position);
   glVertexAttribPointer(Position, Vec3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
   glVertexAttribPointer(Position, Vec3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
@@ -171,18 +171,18 @@ void PrimitiveBatchPipeline::initializeCylinderVao() {
   glVertexAttribPointer(TexCoord, Vec2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
   glVertexAttribPointer(TexCoord, Vec2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
                         reinterpret_cast<void *>(offsetof(Vertex, tex_coord)));
                         reinterpret_cast<void *>(offsetof(Vertex, tex_coord)));
 
 
-  glGenBuffers(1, &m_cylinderInstanceBuffer);
-  glBindBuffer(GL_ARRAY_BUFFER, m_cylinderInstanceBuffer);
-  m_cylinderInstanceCapacity = kDefaultInstanceCapacity;
+  glGenBuffers(1, &m_cylinder_instance_buffer);
+  glBindBuffer(GL_ARRAY_BUFFER, m_cylinder_instance_buffer);
+  m_cylinder_instance_capacity = k_default_instance_capacity;
   glBufferData(GL_ARRAY_BUFFER,
   glBufferData(GL_ARRAY_BUFFER,
-               m_cylinderInstanceCapacity * sizeof(GL::PrimitiveInstanceGpu),
+               m_cylinder_instance_capacity * sizeof(GL::PrimitiveInstanceGpu),
                nullptr, GL_DYNAMIC_DRAW);
                nullptr, GL_DYNAMIC_DRAW);
 
 
-  setupInstanceAttributes(m_cylinderVao, m_cylinderInstanceBuffer);
+  setup_instance_attributes(m_cylinder_vao, m_cylinder_instance_buffer);
   glBindVertexArray(0);
   glBindVertexArray(0);
 }
 }
 
 
-void PrimitiveBatchPipeline::initializeConeVao() {
+void PrimitiveBatchPipeline::initialize_cone_vao() {
   Mesh *unit = getUnitCone();
   Mesh *unit = getUnitCone();
   if (unit == nullptr) {
   if (unit == nullptr) {
     return;
     return;
@@ -194,19 +194,19 @@ void PrimitiveBatchPipeline::initializeConeVao() {
     return;
     return;
   }
   }
 
 
-  glGenVertexArrays(1, &m_coneVao);
-  glBindVertexArray(m_coneVao);
+  glGenVertexArrays(1, &m_cone_vao);
+  glBindVertexArray(m_cone_vao);
 
 
-  glGenBuffers(1, &m_coneVertexBuffer);
-  glBindBuffer(GL_ARRAY_BUFFER, m_coneVertexBuffer);
+  glGenBuffers(1, &m_cone_vertex_buffer);
+  glBindBuffer(GL_ARRAY_BUFFER, m_cone_vertex_buffer);
   glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex),
   glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex),
                vertices.data(), GL_STATIC_DRAW);
                vertices.data(), GL_STATIC_DRAW);
 
 
-  glGenBuffers(1, &m_coneIndexBuffer);
-  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_coneIndexBuffer);
+  glGenBuffers(1, &m_cone_index_buffer);
+  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_cone_index_buffer);
   glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int),
   glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int),
                indices.data(), GL_STATIC_DRAW);
                indices.data(), GL_STATIC_DRAW);
-  m_coneIndexCount = static_cast<GLsizei>(indices.size());
+  m_cone_index_count = static_cast<GLsizei>(indices.size());
 
 
   glEnableVertexAttribArray(Position);
   glEnableVertexAttribArray(Position);
   glVertexAttribPointer(Position, Vec3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
   glVertexAttribPointer(Position, Vec3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
@@ -218,82 +218,83 @@ void PrimitiveBatchPipeline::initializeConeVao() {
   glVertexAttribPointer(TexCoord, Vec2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
   glVertexAttribPointer(TexCoord, Vec2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
                         reinterpret_cast<void *>(offsetof(Vertex, tex_coord)));
                         reinterpret_cast<void *>(offsetof(Vertex, tex_coord)));
 
 
-  glGenBuffers(1, &m_coneInstanceBuffer);
-  glBindBuffer(GL_ARRAY_BUFFER, m_coneInstanceBuffer);
-  m_coneInstanceCapacity = kDefaultInstanceCapacity;
+  glGenBuffers(1, &m_cone_instance_buffer);
+  glBindBuffer(GL_ARRAY_BUFFER, m_cone_instance_buffer);
+  m_cone_instance_capacity = k_default_instance_capacity;
   glBufferData(GL_ARRAY_BUFFER,
   glBufferData(GL_ARRAY_BUFFER,
-               m_coneInstanceCapacity * sizeof(GL::PrimitiveInstanceGpu),
+               m_cone_instance_capacity * sizeof(GL::PrimitiveInstanceGpu),
                nullptr, GL_DYNAMIC_DRAW);
                nullptr, GL_DYNAMIC_DRAW);
 
 
-  setupInstanceAttributes(m_coneVao, m_coneInstanceBuffer);
+  setup_instance_attributes(m_cone_vao, m_cone_instance_buffer);
   glBindVertexArray(0);
   glBindVertexArray(0);
 }
 }
 
 
-void PrimitiveBatchPipeline::shutdownVaos() {
-  if (m_sphereVao != 0) {
-    glDeleteVertexArrays(1, &m_sphereVao);
-    m_sphereVao = 0;
+void PrimitiveBatchPipeline::shutdown_vaos() {
+  if (m_sphere_vao != 0) {
+    glDeleteVertexArrays(1, &m_sphere_vao);
+    m_sphere_vao = 0;
   }
   }
-  if (m_sphereVertexBuffer != 0) {
-    glDeleteBuffers(1, &m_sphereVertexBuffer);
-    m_sphereVertexBuffer = 0;
+  if (m_sphere_vertex_buffer != 0) {
+    glDeleteBuffers(1, &m_sphere_vertex_buffer);
+    m_sphere_vertex_buffer = 0;
   }
   }
-  if (m_sphereIndexBuffer != 0) {
-    glDeleteBuffers(1, &m_sphereIndexBuffer);
-    m_sphereIndexBuffer = 0;
+  if (m_sphere_index_buffer != 0) {
+    glDeleteBuffers(1, &m_sphere_index_buffer);
+    m_sphere_index_buffer = 0;
   }
   }
-  if (m_sphereInstanceBuffer != 0) {
-    glDeleteBuffers(1, &m_sphereInstanceBuffer);
-    m_sphereInstanceBuffer = 0;
+  if (m_sphere_instance_buffer != 0) {
+    glDeleteBuffers(1, &m_sphere_instance_buffer);
+    m_sphere_instance_buffer = 0;
   }
   }
 
 
-  if (m_cylinderVao != 0) {
-    glDeleteVertexArrays(1, &m_cylinderVao);
-    m_cylinderVao = 0;
+  if (m_cylinder_vao != 0) {
+    glDeleteVertexArrays(1, &m_cylinder_vao);
+    m_cylinder_vao = 0;
   }
   }
-  if (m_cylinderVertexBuffer != 0) {
-    glDeleteBuffers(1, &m_cylinderVertexBuffer);
-    m_cylinderVertexBuffer = 0;
+  if (m_cylinder_vertex_buffer != 0) {
+    glDeleteBuffers(1, &m_cylinder_vertex_buffer);
+    m_cylinder_vertex_buffer = 0;
   }
   }
-  if (m_cylinderIndexBuffer != 0) {
-    glDeleteBuffers(1, &m_cylinderIndexBuffer);
-    m_cylinderIndexBuffer = 0;
+  if (m_cylinder_index_buffer != 0) {
+    glDeleteBuffers(1, &m_cylinder_index_buffer);
+    m_cylinder_index_buffer = 0;
   }
   }
-  if (m_cylinderInstanceBuffer != 0) {
-    glDeleteBuffers(1, &m_cylinderInstanceBuffer);
-    m_cylinderInstanceBuffer = 0;
+  if (m_cylinder_instance_buffer != 0) {
+    glDeleteBuffers(1, &m_cylinder_instance_buffer);
+    m_cylinder_instance_buffer = 0;
   }
   }
 
 
-  if (m_coneVao != 0) {
-    glDeleteVertexArrays(1, &m_coneVao);
-    m_coneVao = 0;
+  if (m_cone_vao != 0) {
+    glDeleteVertexArrays(1, &m_cone_vao);
+    m_cone_vao = 0;
   }
   }
-  if (m_coneVertexBuffer != 0) {
-    glDeleteBuffers(1, &m_coneVertexBuffer);
-    m_coneVertexBuffer = 0;
+  if (m_cone_vertex_buffer != 0) {
+    glDeleteBuffers(1, &m_cone_vertex_buffer);
+    m_cone_vertex_buffer = 0;
   }
   }
-  if (m_coneIndexBuffer != 0) {
-    glDeleteBuffers(1, &m_coneIndexBuffer);
-    m_coneIndexBuffer = 0;
+  if (m_cone_index_buffer != 0) {
+    glDeleteBuffers(1, &m_cone_index_buffer);
+    m_cone_index_buffer = 0;
   }
   }
-  if (m_coneInstanceBuffer != 0) {
-    glDeleteBuffers(1, &m_coneInstanceBuffer);
-    m_coneInstanceBuffer = 0;
+  if (m_cone_instance_buffer != 0) {
+    glDeleteBuffers(1, &m_cone_instance_buffer);
+    m_cone_instance_buffer = 0;
   }
   }
 }
 }
 
 
-void PrimitiveBatchPipeline::uploadSphereInstances(
+void PrimitiveBatchPipeline::upload_sphere_instances(
     const GL::PrimitiveInstanceGpu *data, std::size_t count) {
     const GL::PrimitiveInstanceGpu *data, std::size_t count) {
   if (count == 0 || data == nullptr) {
   if (count == 0 || data == nullptr) {
     return;
     return;
   }
   }
 
 
-  glBindBuffer(GL_ARRAY_BUFFER, m_sphereInstanceBuffer);
+  glBindBuffer(GL_ARRAY_BUFFER, m_sphere_instance_buffer);
 
 
-  if (count > m_sphereInstanceCapacity) {
-    m_sphereInstanceCapacity = static_cast<std::size_t>(count * kGrowthFactor);
+  if (count > m_sphere_instance_capacity) {
+    m_sphere_instance_capacity =
+        static_cast<std::size_t>(count * k_growth_factor);
     glBufferData(GL_ARRAY_BUFFER,
     glBufferData(GL_ARRAY_BUFFER,
-                 m_sphereInstanceCapacity * sizeof(GL::PrimitiveInstanceGpu),
+                 m_sphere_instance_capacity * sizeof(GL::PrimitiveInstanceGpu),
                  nullptr, GL_DYNAMIC_DRAW);
                  nullptr, GL_DYNAMIC_DRAW);
   }
   }
 
 
@@ -301,19 +302,20 @@ void PrimitiveBatchPipeline::uploadSphereInstances(
                   data);
                   data);
 }
 }
 
 
-void PrimitiveBatchPipeline::uploadCylinderInstances(
+void PrimitiveBatchPipeline::upload_cylinder_instances(
     const GL::PrimitiveInstanceGpu *data, std::size_t count) {
     const GL::PrimitiveInstanceGpu *data, std::size_t count) {
   if (count == 0 || data == nullptr) {
   if (count == 0 || data == nullptr) {
     return;
     return;
   }
   }
 
 
-  glBindBuffer(GL_ARRAY_BUFFER, m_cylinderInstanceBuffer);
+  glBindBuffer(GL_ARRAY_BUFFER, m_cylinder_instance_buffer);
 
 
-  if (count > m_cylinderInstanceCapacity) {
-    m_cylinderInstanceCapacity =
-        static_cast<std::size_t>(count * kGrowthFactor);
+  if (count > m_cylinder_instance_capacity) {
+    m_cylinder_instance_capacity =
+        static_cast<std::size_t>(count * k_growth_factor);
     glBufferData(GL_ARRAY_BUFFER,
     glBufferData(GL_ARRAY_BUFFER,
-                 m_cylinderInstanceCapacity * sizeof(GL::PrimitiveInstanceGpu),
+                 m_cylinder_instance_capacity *
+                     sizeof(GL::PrimitiveInstanceGpu),
                  nullptr, GL_DYNAMIC_DRAW);
                  nullptr, GL_DYNAMIC_DRAW);
   }
   }
 
 
@@ -321,18 +323,19 @@ void PrimitiveBatchPipeline::uploadCylinderInstances(
                   data);
                   data);
 }
 }
 
 
-void PrimitiveBatchPipeline::uploadConeInstances(
+void PrimitiveBatchPipeline::upload_cone_instances(
     const GL::PrimitiveInstanceGpu *data, std::size_t count) {
     const GL::PrimitiveInstanceGpu *data, std::size_t count) {
   if (count == 0 || data == nullptr) {
   if (count == 0 || data == nullptr) {
     return;
     return;
   }
   }
 
 
-  glBindBuffer(GL_ARRAY_BUFFER, m_coneInstanceBuffer);
+  glBindBuffer(GL_ARRAY_BUFFER, m_cone_instance_buffer);
 
 
-  if (count > m_coneInstanceCapacity) {
-    m_coneInstanceCapacity = static_cast<std::size_t>(count * kGrowthFactor);
+  if (count > m_cone_instance_capacity) {
+    m_cone_instance_capacity =
+        static_cast<std::size_t>(count * k_growth_factor);
     glBufferData(GL_ARRAY_BUFFER,
     glBufferData(GL_ARRAY_BUFFER,
-                 m_coneInstanceCapacity * sizeof(GL::PrimitiveInstanceGpu),
+                 m_cone_instance_capacity * sizeof(GL::PrimitiveInstanceGpu),
                  nullptr, GL_DYNAMIC_DRAW);
                  nullptr, GL_DYNAMIC_DRAW);
   }
   }
 
 
@@ -340,53 +343,53 @@ void PrimitiveBatchPipeline::uploadConeInstances(
                   data);
                   data);
 }
 }
 
 
-void PrimitiveBatchPipeline::drawSpheres(std::size_t count,
-                                         const QMatrix4x4 &viewProj) {
-  if (count == 0 || m_sphereVao == 0 || m_shader == nullptr) {
+void PrimitiveBatchPipeline::draw_spheres(std::size_t count,
+                                          const QMatrix4x4 &view_proj) {
+  if (count == 0 || m_sphere_vao == 0 || m_shader == nullptr) {
     return;
     return;
   }
   }
 
 
   m_shader->use();
   m_shader->use();
-  m_shader->setUniform(m_uniforms.view_proj, viewProj);
+  m_shader->setUniform(m_uniforms.view_proj, view_proj);
   m_shader->setUniform(m_uniforms.light_dir, QVector3D(0.35F, 0.8F, 0.45F));
   m_shader->setUniform(m_uniforms.light_dir, QVector3D(0.35F, 0.8F, 0.45F));
   m_shader->setUniform(m_uniforms.ambient_strength, 0.3F);
   m_shader->setUniform(m_uniforms.ambient_strength, 0.3F);
 
 
-  glBindVertexArray(m_sphereVao);
-  glDrawElementsInstanced(GL_TRIANGLES, m_sphereIndexCount, GL_UNSIGNED_INT,
+  glBindVertexArray(m_sphere_vao);
+  glDrawElementsInstanced(GL_TRIANGLES, m_sphere_index_count, GL_UNSIGNED_INT,
                           nullptr, static_cast<GLsizei>(count));
                           nullptr, static_cast<GLsizei>(count));
   glBindVertexArray(0);
   glBindVertexArray(0);
 }
 }
 
 
-void PrimitiveBatchPipeline::drawCylinders(std::size_t count,
-                                           const QMatrix4x4 &viewProj) {
-  if (count == 0 || m_cylinderVao == 0 || m_shader == nullptr) {
+void PrimitiveBatchPipeline::draw_cylinders(std::size_t count,
+                                            const QMatrix4x4 &view_proj) {
+  if (count == 0 || m_cylinder_vao == 0 || m_shader == nullptr) {
     return;
     return;
   }
   }
 
 
   m_shader->use();
   m_shader->use();
-  m_shader->setUniform(m_uniforms.view_proj, viewProj);
+  m_shader->setUniform(m_uniforms.view_proj, view_proj);
   m_shader->setUniform(m_uniforms.light_dir, QVector3D(0.35F, 0.8F, 0.45F));
   m_shader->setUniform(m_uniforms.light_dir, QVector3D(0.35F, 0.8F, 0.45F));
   m_shader->setUniform(m_uniforms.ambient_strength, 0.3F);
   m_shader->setUniform(m_uniforms.ambient_strength, 0.3F);
 
 
-  glBindVertexArray(m_cylinderVao);
-  glDrawElementsInstanced(GL_TRIANGLES, m_cylinderIndexCount, GL_UNSIGNED_INT,
+  glBindVertexArray(m_cylinder_vao);
+  glDrawElementsInstanced(GL_TRIANGLES, m_cylinder_index_count, GL_UNSIGNED_INT,
                           nullptr, static_cast<GLsizei>(count));
                           nullptr, static_cast<GLsizei>(count));
   glBindVertexArray(0);
   glBindVertexArray(0);
 }
 }
 
 
-void PrimitiveBatchPipeline::drawCones(std::size_t count,
-                                       const QMatrix4x4 &viewProj) {
-  if (count == 0 || m_coneVao == 0 || m_shader == nullptr) {
+void PrimitiveBatchPipeline::draw_cones(std::size_t count,
+                                        const QMatrix4x4 &view_proj) {
+  if (count == 0 || m_cone_vao == 0 || m_shader == nullptr) {
     return;
     return;
   }
   }
 
 
   m_shader->use();
   m_shader->use();
-  m_shader->setUniform(m_uniforms.view_proj, viewProj);
+  m_shader->setUniform(m_uniforms.view_proj, view_proj);
   m_shader->setUniform(m_uniforms.light_dir, QVector3D(0.35F, 0.8F, 0.45F));
   m_shader->setUniform(m_uniforms.light_dir, QVector3D(0.35F, 0.8F, 0.45F));
   m_shader->setUniform(m_uniforms.ambient_strength, 0.3F);
   m_shader->setUniform(m_uniforms.ambient_strength, 0.3F);
 
 
-  glBindVertexArray(m_coneVao);
-  glDrawElementsInstanced(GL_TRIANGLES, m_coneIndexCount, GL_UNSIGNED_INT,
+  glBindVertexArray(m_cone_vao);
+  glDrawElementsInstanced(GL_TRIANGLES, m_cone_index_count, GL_UNSIGNED_INT,
                           nullptr, static_cast<GLsizei>(count));
                           nullptr, static_cast<GLsizei>(count));
   glBindVertexArray(0);
   glBindVertexArray(0);
 }
 }

+ 38 - 38
render/gl/backend/primitive_batch_pipeline.h

@@ -23,18 +23,18 @@ public:
     return m_initialized;
     return m_initialized;
   }
   }
 
 
-  void beginFrame();
+  void begin_frame();
 
 
-  void uploadSphereInstances(const GL::PrimitiveInstanceGpu *data,
-                             std::size_t count);
-  void uploadCylinderInstances(const GL::PrimitiveInstanceGpu *data,
+  void upload_sphere_instances(const GL::PrimitiveInstanceGpu *data,
                                std::size_t count);
                                std::size_t count);
-  void uploadConeInstances(const GL::PrimitiveInstanceGpu *data,
-                           std::size_t count);
+  void upload_cylinder_instances(const GL::PrimitiveInstanceGpu *data,
+                                 std::size_t count);
+  void upload_cone_instances(const GL::PrimitiveInstanceGpu *data,
+                             std::size_t count);
 
 
-  void drawSpheres(std::size_t count, const QMatrix4x4 &viewProj);
-  void drawCylinders(std::size_t count, const QMatrix4x4 &viewProj);
-  void drawCones(std::size_t count, const QMatrix4x4 &viewProj);
+  void draw_spheres(std::size_t count, const QMatrix4x4 &view_proj);
+  void draw_cylinders(std::size_t count, const QMatrix4x4 &view_proj);
+  void draw_cones(std::size_t count, const QMatrix4x4 &view_proj);
 
 
   [[nodiscard]] auto shader() const -> GL::Shader * { return m_shader; }
   [[nodiscard]] auto shader() const -> GL::Shader * { return m_shader; }
 
 
@@ -47,41 +47,41 @@ public:
   Uniforms m_uniforms;
   Uniforms m_uniforms;
 
 
 private:
 private:
-  void initializeSphereVao();
-  void initializeCylinderVao();
-  void initializeConeVao();
-  void shutdownVaos();
+  void initialize_sphere_vao();
+  void initialize_cylinder_vao();
+  void initialize_cone_vao();
+  void shutdown_vaos();
 
 
-  void setupInstanceAttributes(GLuint vao, GLuint instanceBuffer);
+  void setup_instance_attributes(GLuint vao, GLuint instance_buffer);
 
 
-  GL::ShaderCache *m_shaderCache;
+  GL::ShaderCache *m_shader_cache;
   bool m_initialized{false};
   bool m_initialized{false};
 
 
   GL::Shader *m_shader{nullptr};
   GL::Shader *m_shader{nullptr};
 
 
-  GLuint m_sphereVao{0};
-  GLuint m_sphereVertexBuffer{0};
-  GLuint m_sphereIndexBuffer{0};
-  GLuint m_sphereInstanceBuffer{0};
-  GLsizei m_sphereIndexCount{0};
-  std::size_t m_sphereInstanceCapacity{0};
-
-  GLuint m_cylinderVao{0};
-  GLuint m_cylinderVertexBuffer{0};
-  GLuint m_cylinderIndexBuffer{0};
-  GLuint m_cylinderInstanceBuffer{0};
-  GLsizei m_cylinderIndexCount{0};
-  std::size_t m_cylinderInstanceCapacity{0};
-
-  GLuint m_coneVao{0};
-  GLuint m_coneVertexBuffer{0};
-  GLuint m_coneIndexBuffer{0};
-  GLuint m_coneInstanceBuffer{0};
-  GLsizei m_coneIndexCount{0};
-  std::size_t m_coneInstanceCapacity{0};
-
-  static constexpr std::size_t kDefaultInstanceCapacity = 4096;
-  static constexpr float kGrowthFactor = 1.5F;
+  GLuint m_sphere_vao{0};
+  GLuint m_sphere_vertex_buffer{0};
+  GLuint m_sphere_index_buffer{0};
+  GLuint m_sphere_instance_buffer{0};
+  GLsizei m_sphere_index_count{0};
+  std::size_t m_sphere_instance_capacity{0};
+
+  GLuint m_cylinder_vao{0};
+  GLuint m_cylinder_vertex_buffer{0};
+  GLuint m_cylinder_index_buffer{0};
+  GLuint m_cylinder_instance_buffer{0};
+  GLsizei m_cylinder_index_count{0};
+  std::size_t m_cylinder_instance_capacity{0};
+
+  GLuint m_cone_vao{0};
+  GLuint m_cone_vertex_buffer{0};
+  GLuint m_cone_index_buffer{0};
+  GLuint m_cone_instance_buffer{0};
+  GLsizei m_cone_index_count{0};
+  std::size_t m_cone_instance_capacity{0};
+
+  static constexpr std::size_t k_default_instance_capacity = 4096;
+  static constexpr float k_growth_factor = 1.5F;
 };
 };
 
 
 } // namespace Render::GL::BackendPipelines
 } // namespace Render::GL::BackendPipelines

+ 2 - 1
tests/CMakeLists.txt

@@ -17,12 +17,13 @@ add_executable(standard_of_iron_tests
     render/horse_animation_controller_test.cpp
     render/horse_animation_controller_test.cpp
     render/rider_proportions_test.cpp
     render/rider_proportions_test.cpp
     render/horse_equipment_renderers_test.cpp
     render/horse_equipment_renderers_test.cpp
+    test_main.cpp
 )
 )
 
 
 # Link against GTest, project libraries
 # Link against GTest, project libraries
 target_link_libraries(standard_of_iron_tests
 target_link_libraries(standard_of_iron_tests
     PRIVATE
     PRIVATE
-        GTest::gtest_main
+        GTest::gtest
         GTest::gmock
         GTest::gmock
         Qt${QT_VERSION_MAJOR}::Core
         Qt${QT_VERSION_MAJOR}::Core
         Qt${QT_VERSION_MAJOR}::Gui
         Qt${QT_VERSION_MAJOR}::Gui

+ 33 - 29
tests/map/minimap_generator_test.cpp

@@ -1,32 +1,35 @@
-#include "map/minimap/minimap_generator.h"
 #include "map/map_definition.h"
 #include "map/map_definition.h"
-#include <gtest/gtest.h>
+#include "map/minimap/minimap_generator.h"
 #include <QImage>
 #include <QImage>
+#include <gtest/gtest.h>
 
 
 using namespace Game::Map;
 using namespace Game::Map;
 using namespace Game::Map::Minimap;
 using namespace Game::Map::Minimap;
 
 
 class MinimapGeneratorTest : public ::testing::Test {
 class MinimapGeneratorTest : public ::testing::Test {
 protected:
 protected:
+  // No additional setup needed; the global test environment handles
+  // QGuiApplication.
+
   void SetUp() override {
   void SetUp() override {
     // Create a simple test map
     // Create a simple test map
-    testMap.name = "Test Map";
-    testMap.grid.width = 50;
-    testMap.grid.height = 50;
-    testMap.grid.tile_size = 1.0F;
+    test_map.name = "Test Map";
+    test_map.grid.width = 50;
+    test_map.grid.height = 50;
+    test_map.grid.tile_size = 1.0F;
 
 
     // Set up biome with default colors
     // 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);
+    test_map.biome.grass_primary = QVector3D(0.3F, 0.6F, 0.28F);
+    test_map.biome.grass_secondary = QVector3D(0.44F, 0.7F, 0.32F);
+    test_map.biome.soil_color = QVector3D(0.28F, 0.24F, 0.18F);
   }
   }
 
 
-  MapDefinition testMap;
+  MapDefinition test_map;
 };
 };
 
 
 TEST_F(MinimapGeneratorTest, GeneratesValidImage) {
 TEST_F(MinimapGeneratorTest, GeneratesValidImage) {
   MinimapGenerator generator;
   MinimapGenerator generator;
-  QImage result = generator.generate(testMap);
+  QImage result = generator.generate(test_map);
 
 
   // Check that image was created
   // Check that image was created
   EXPECT_FALSE(result.isNull());
   EXPECT_FALSE(result.isNull());
@@ -39,10 +42,10 @@ TEST_F(MinimapGeneratorTest, ImageDimensionsMatchGrid) {
   config.pixels_per_tile = 2.0F;
   config.pixels_per_tile = 2.0F;
   MinimapGenerator generator(config);
   MinimapGenerator generator(config);
 
 
-  QImage result = generator.generate(testMap);
+  QImage result = generator.generate(test_map);
 
 
-  const int expected_width = testMap.grid.width * config.pixels_per_tile;
-  const int expected_height = testMap.grid.height * config.pixels_per_tile;
+  const int expected_width = test_map.grid.width * config.pixels_per_tile;
+  const int expected_height = test_map.grid.height * config.pixels_per_tile;
 
 
   EXPECT_EQ(result.width(), expected_width);
   EXPECT_EQ(result.width(), expected_width);
   EXPECT_EQ(result.height(), expected_height);
   EXPECT_EQ(result.height(), expected_height);
@@ -54,13 +57,14 @@ TEST_F(MinimapGeneratorTest, RendersRivers) {
   river.start = QVector3D(10.0F, 0.0F, 10.0F);
   river.start = QVector3D(10.0F, 0.0F, 10.0F);
   river.end = QVector3D(40.0F, 0.0F, 40.0F);
   river.end = QVector3D(40.0F, 0.0F, 40.0F);
   river.width = 3.0F;
   river.width = 3.0F;
-  testMap.rivers.push_back(river);
+  test_map.rivers.push_back(river);
 
 
   MinimapGenerator generator;
   MinimapGenerator generator;
-  QImage result = generator.generate(testMap);
+  QImage result = generator.generate(test_map);
 
 
   EXPECT_FALSE(result.isNull());
   EXPECT_FALSE(result.isNull());
-  // River should have been rendered (we can't easily verify pixels, but check image is valid)
+  // River should have been rendered (we can't easily verify pixels, but check
+  // image is valid)
 }
 }
 
 
 TEST_F(MinimapGeneratorTest, RendersTerrainFeatures) {
 TEST_F(MinimapGeneratorTest, RendersTerrainFeatures) {
@@ -72,10 +76,10 @@ TEST_F(MinimapGeneratorTest, RendersTerrainFeatures) {
   hill.width = 10.0F;
   hill.width = 10.0F;
   hill.depth = 10.0F;
   hill.depth = 10.0F;
   hill.height = 3.0F;
   hill.height = 3.0F;
-  testMap.terrain.push_back(hill);
+  test_map.terrain.push_back(hill);
 
 
   MinimapGenerator generator;
   MinimapGenerator generator;
-  QImage result = generator.generate(testMap);
+  QImage result = generator.generate(test_map);
 
 
   EXPECT_FALSE(result.isNull());
   EXPECT_FALSE(result.isNull());
 }
 }
@@ -87,10 +91,10 @@ TEST_F(MinimapGeneratorTest, RendersRoads) {
   road.end = QVector3D(45.0F, 0.0F, 45.0F);
   road.end = QVector3D(45.0F, 0.0F, 45.0F);
   road.width = 3.0F;
   road.width = 3.0F;
   road.style = "default";
   road.style = "default";
-  testMap.roads.push_back(road);
+  test_map.roads.push_back(road);
 
 
   MinimapGenerator generator;
   MinimapGenerator generator;
-  QImage result = generator.generate(testMap);
+  QImage result = generator.generate(test_map);
 
 
   EXPECT_FALSE(result.isNull());
   EXPECT_FALSE(result.isNull());
 }
 }
@@ -102,24 +106,24 @@ TEST_F(MinimapGeneratorTest, RendersStructures) {
   barracks.x = 25.0F;
   barracks.x = 25.0F;
   barracks.z = 25.0F;
   barracks.z = 25.0F;
   barracks.player_id = 1;
   barracks.player_id = 1;
-  testMap.spawns.push_back(barracks);
+  test_map.spawns.push_back(barracks);
 
 
   MinimapGenerator generator;
   MinimapGenerator generator;
-  QImage result = generator.generate(testMap);
+  QImage result = generator.generate(test_map);
 
 
   EXPECT_FALSE(result.isNull());
   EXPECT_FALSE(result.isNull());
 }
 }
 
 
 TEST_F(MinimapGeneratorTest, HandlesEmptyMap) {
 TEST_F(MinimapGeneratorTest, HandlesEmptyMap) {
   // Empty map with no features
   // 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);
+  MapDefinition empty_map;
+  empty_map.grid.width = 10;
+  empty_map.grid.height = 10;
+  empty_map.grid.tile_size = 1.0F;
+  empty_map.biome.grass_primary = QVector3D(0.3F, 0.6F, 0.28F);
 
 
   MinimapGenerator generator;
   MinimapGenerator generator;
-  QImage result = generator.generate(emptyMap);
+  QImage result = generator.generate(empty_map);
 
 
   EXPECT_FALSE(result.isNull());
   EXPECT_FALSE(result.isNull());
   EXPECT_GT(result.width(), 0);
   EXPECT_GT(result.width(), 0);

+ 10 - 0
tests/test_main.cpp

@@ -0,0 +1,10 @@
+#include <QCoreApplication>
+#include <QGuiApplication>
+#include <gtest/gtest.h>
+
+int main(int argc, char **argv) {
+  qputenv("QT_QPA_PLATFORM", "offscreen");
+  QGuiApplication app(argc, argv);
+  ::testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}

+ 55 - 20
ui/qml/HUDTop.qml

@@ -331,6 +331,7 @@ Item {
 
 
                 spacing: 12
                 spacing: 12
                 Layout.alignment: Qt.AlignVCenter
                 Layout.alignment: Qt.AlignVCenter
+                Layout.rightMargin: 260
 
 
                 Row {
                 Row {
                     id: statsRow
                     id: statsRow
@@ -439,32 +440,66 @@ Item {
                     Layout.preferredWidth: Math.round(topPanel.height * 2.2)
                     Layout.preferredWidth: Math.round(topPanel.height * 2.2)
                     Layout.minimumWidth: Math.round(topPanel.height * 1.6)
                     Layout.minimumWidth: Math.round(topPanel.height * 1.6)
                     Layout.preferredHeight: topPanel.height - 8
                     Layout.preferredHeight: topPanel.height - 8
+                }
 
 
-                    Rectangle {
-                        anchors.fill: parent
-                        color: "#0f1a22"
-                        radius: 8
-                        border.width: 2
-                        border.color: "#3498db"
+            }
 
 
-                        Rectangle {
-                            anchors.fill: parent
-                            anchors.margins: 3
-                            radius: 6
-                            color: "#0a0f14"
-
-                            Label {
-                                anchors.centerIn: parent
-                                text: qsTr("MINIMAP")
-                                color: "#3f5362"
-                                font.pixelSize: 12
-                                font.bold: true
-                            }
+        }
 
 
-                        }
+    }
 
 
+    Rectangle {
+        id: minimapContainer
+
+        visible: !topRoot.ultraCompact
+        width: 240
+        height: 240
+        anchors.right: parent.right
+        anchors.top: parent.top
+        anchors.rightMargin: 8
+        anchors.topMargin: 8
+        z: 100
+        color: "#0f1a22"
+        radius: 8
+        border.width: 2
+        border.color: "#3498db"
+
+        Rectangle {
+            anchors.fill: parent
+            anchors.margins: 3
+            radius: 6
+            color: "#0a0f14"
+
+            Image {
+                id: minimapImage
+
+                property int imageVersion: 0
+
+                anchors.fill: parent
+                anchors.margins: 2
+                source: imageVersion > 0 ? "image://minimap/v" + imageVersion : ""
+                fillMode: Image.PreserveAspectFit
+                smooth: true
+                cache: false
+                asynchronous: false
+
+                Connections {
+                    function onMinimap_image_changed() {
+                        Qt.callLater(function() {
+                            minimapImage.imageVersion++;
+                        });
                     }
                     }
 
 
+                    target: game
+                }
+
+                Label {
+                    anchors.centerIn: parent
+                    text: qsTr("MINIMAP")
+                    color: "#3f5362"
+                    font.pixelSize: 12
+                    font.bold: true
+                    visible: parent.status !== Image.Ready
                 }
                 }
 
 
             }
             }

+ 18 - 18
ui/qml/ProductionPanel.qml

@@ -379,9 +379,9 @@ Rectangle {
                                 anchors.fill: parent
                                 anchors.fill: parent
                                 hoverEnabled: true
                                 hoverEnabled: true
                                 onClicked: {
                                 onClicked: {
-                                    if (parent.isEnabled) {
+                                    if (parent.isEnabled)
                                         productionPanel.recruitUnit("archer");
                                         productionPanel.recruitUnit("archer");
-                                    }
+
                                 }
                                 }
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 ToolTip.visible: containsMouse
                                 ToolTip.visible: containsMouse
@@ -485,9 +485,9 @@ Rectangle {
                                 anchors.fill: parent
                                 anchors.fill: parent
                                 hoverEnabled: true
                                 hoverEnabled: true
                                 onClicked: {
                                 onClicked: {
-                                    if (parent.isEnabled) {
+                                    if (parent.isEnabled)
                                         productionPanel.recruitUnit("swordsman");
                                         productionPanel.recruitUnit("swordsman");
-                                    }
+
                                 }
                                 }
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 ToolTip.visible: containsMouse
                                 ToolTip.visible: containsMouse
@@ -591,9 +591,9 @@ Rectangle {
                                 anchors.fill: parent
                                 anchors.fill: parent
                                 hoverEnabled: true
                                 hoverEnabled: true
                                 onClicked: {
                                 onClicked: {
-                                    if (parent.isEnabled) {
+                                    if (parent.isEnabled)
                                         productionPanel.recruitUnit("spearman");
                                         productionPanel.recruitUnit("spearman");
-                                    }
+
                                 }
                                 }
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 ToolTip.visible: containsMouse
                                 ToolTip.visible: containsMouse
@@ -697,9 +697,9 @@ Rectangle {
                                 anchors.fill: parent
                                 anchors.fill: parent
                                 hoverEnabled: true
                                 hoverEnabled: true
                                 onClicked: {
                                 onClicked: {
-                                    if (parent.isEnabled) {
+                                    if (parent.isEnabled)
                                         productionPanel.recruitUnit("horse_swordsman");
                                         productionPanel.recruitUnit("horse_swordsman");
-                                    }
+
                                 }
                                 }
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 ToolTip.visible: containsMouse
                                 ToolTip.visible: containsMouse
@@ -803,9 +803,9 @@ Rectangle {
                                 anchors.fill: parent
                                 anchors.fill: parent
                                 hoverEnabled: true
                                 hoverEnabled: true
                                 onClicked: {
                                 onClicked: {
-                                    if (parent.isEnabled) {
+                                    if (parent.isEnabled)
                                         productionPanel.recruitUnit("horse_archer");
                                         productionPanel.recruitUnit("horse_archer");
-                                    }
+
                                 }
                                 }
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 ToolTip.visible: containsMouse
                                 ToolTip.visible: containsMouse
@@ -909,9 +909,9 @@ Rectangle {
                                 anchors.fill: parent
                                 anchors.fill: parent
                                 hoverEnabled: true
                                 hoverEnabled: true
                                 onClicked: {
                                 onClicked: {
-                                    if (parent.isEnabled) {
+                                    if (parent.isEnabled)
                                         productionPanel.recruitUnit("horse_spearman");
                                         productionPanel.recruitUnit("horse_spearman");
-                                    }
+
                                 }
                                 }
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 ToolTip.visible: containsMouse
                                 ToolTip.visible: containsMouse
@@ -1015,9 +1015,9 @@ Rectangle {
                                 anchors.fill: parent
                                 anchors.fill: parent
                                 hoverEnabled: true
                                 hoverEnabled: true
                                 onClicked: {
                                 onClicked: {
-                                    if (parent.isEnabled) {
+                                    if (parent.isEnabled)
                                         productionPanel.recruitUnit("catapult");
                                         productionPanel.recruitUnit("catapult");
-                                    }
+
                                 }
                                 }
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 ToolTip.visible: containsMouse
                                 ToolTip.visible: containsMouse
@@ -1121,9 +1121,9 @@ Rectangle {
                                 anchors.fill: parent
                                 anchors.fill: parent
                                 hoverEnabled: true
                                 hoverEnabled: true
                                 onClicked: {
                                 onClicked: {
-                                    if (parent.isEnabled) {
+                                    if (parent.isEnabled)
                                         productionPanel.recruitUnit("ballista");
                                         productionPanel.recruitUnit("ballista");
-                                    }
+
                                 }
                                 }
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 ToolTip.visible: containsMouse
                                 ToolTip.visible: containsMouse
@@ -1227,9 +1227,9 @@ Rectangle {
                                 anchors.fill: parent
                                 anchors.fill: parent
                                 hoverEnabled: true
                                 hoverEnabled: true
                                 onClicked: {
                                 onClicked: {
-                                    if (parent.isEnabled) {
+                                    if (parent.isEnabled)
                                         productionPanel.recruitUnit("healer");
                                         productionPanel.recruitUnit("healer");
-                                    }
+
                                 }
                                 }
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
                                 ToolTip.visible: containsMouse
                                 ToolTip.visible: containsMouse