瀏覽代碼

merge changes from main

djeada 1 天之前
父節點
當前提交
a9d202a6ec
共有 46 個文件被更改,包括 3671 次插入1602 次删除
  1. 19 0
      CMakeLists.txt
  2. 99 0
      app/core/ambient_state_manager.cpp
  3. 29 0
      app/core/ambient_state_manager.h
  4. 93 0
      app/core/audio_resource_loader.cpp
  5. 8 0
      app/core/audio_resource_loader.h
  6. 98 0
      app/core/camera_controller.cpp
  7. 38 0
      app/core/camera_controller.h
  8. 229 1111
      app/core/game_engine.cpp
  9. 33 42
      app/core/game_engine.h
  10. 283 0
      app/core/game_state_restorer.cpp
  11. 71 0
      app/core/game_state_restorer.h
  12. 228 0
      app/core/input_command_handler.cpp
  13. 70 0
      app/core/input_command_handler.h
  14. 114 0
      app/core/level_orchestrator.cpp
  15. 74 0
      app/core/level_orchestrator.h
  16. 227 0
      app/core/minimap_manager.cpp
  17. 47 0
      app/core/minimap_manager.h
  18. 81 0
      app/core/renderer_bootstrap.cpp
  19. 52 0
      app/core/renderer_bootstrap.h
  20. 48 0
      app/models/map_preview_image_provider.cpp
  21. 25 0
      app/models/map_preview_image_provider.h
  22. 13 5
      app/models/minimap_image_provider.cpp
  23. 2 0
      app/models/minimap_image_provider.h
  24. 1 0
      game/CMakeLists.txt
  25. 118 24
      game/core/serialization.cpp
  26. 17 17
      game/core/serialization.h
  27. 156 0
      game/map/minimap/map_preview_generator.cpp
  28. 40 0
      game/map/minimap/map_preview_generator.h
  29. 17 13
      game/map/minimap/minimap_generator.cpp
  30. 5 4
      game/systems/camera_follow_system.cpp
  31. 3 3
      game/systems/camera_follow_system.h
  32. 1 1
      game/systems/camera_service.cpp
  33. 30 30
      game/systems/save_load_service.cpp
  34. 16 16
      game/systems/save_load_service.h
  35. 48 47
      game/systems/save_storage.cpp
  36. 14 14
      game/systems/save_storage.h
  37. 8 0
      main.cpp
  38. 1 0
      qml_resources.qrc
  39. 1 1
      render/entity/nations/carthage/barracks_renderer.cpp
  40. 1 1
      render/entity/nations/roman/barracks_renderer.cpp
  41. 0 1
      render/gl/backend.h
  42. 793 34
      tests/core/serialization_test.cpp
  43. 52 52
      tests/db/save_storage_test.cpp
  44. 174 0
      ui/qml/MapPreview.qml
  45. 193 186
      ui/qml/MapSelect.qml
  46. 1 0
      ui/qml/qmldir

+ 19 - 0
CMakeLists.txt

@@ -125,12 +125,21 @@ if(QT_VERSION_MAJOR EQUAL 6)
         main.cpp
         app/core/game_engine.cpp
         app/core/language_manager.cpp
+        app/core/minimap_manager.cpp
+        app/core/ambient_state_manager.cpp
+        app/core/audio_resource_loader.cpp
+        app/core/renderer_bootstrap.cpp
+        app/core/level_orchestrator.cpp
+        app/core/input_command_handler.cpp
+        app/core/camera_controller.cpp
+        app/core/game_state_restorer.cpp
         app/models/audio_system_proxy.cpp
         app/models/graphics_settings_proxy.cpp
         app/models/cursor_manager.cpp
         app/models/hover_tracker.cpp
         app/models/selected_units_model.cpp
         app/models/minimap_image_provider.cpp
+        app/models/map_preview_image_provider.cpp
         app/controllers/command_controller.cpp
         app/controllers/action_vfx.cpp
         app/utils/json_vec_utils.cpp
@@ -142,12 +151,21 @@ else()
         main.cpp
         app/core/game_engine.cpp
         app/core/language_manager.cpp
+        app/core/minimap_manager.cpp
+        app/core/ambient_state_manager.cpp
+        app/core/audio_resource_loader.cpp
+        app/core/renderer_bootstrap.cpp
+        app/core/level_orchestrator.cpp
+        app/core/input_command_handler.cpp
+        app/core/camera_controller.cpp
+        app/core/game_state_restorer.cpp
         app/models/audio_system_proxy.cpp
         app/models/graphics_settings_proxy.cpp
         app/models/cursor_manager.cpp
         app/models/hover_tracker.cpp
         app/models/selected_units_model.cpp
         app/models/minimap_image_provider.cpp
+        app/models/map_preview_image_provider.cpp
         app/controllers/command_controller.cpp
         app/controllers/action_vfx.cpp
         app/utils/json_vec_utils.cpp
@@ -165,6 +183,7 @@ if(QT_VERSION_MAJOR EQUAL 6)
             ui/qml/Main.qml
             ui/qml/MainMenu.qml
             ui/qml/MapSelect.qml
+            ui/qml/MapPreview.qml
             ui/qml/MapListPanel.qml
             ui/qml/PlayerListItem.qml
             ui/qml/PlayerConfigPanel.qml

+ 99 - 0
app/core/ambient_state_manager.cpp

@@ -0,0 +1,99 @@
+#include "ambient_state_manager.h"
+
+#include "game/core/component.h"
+#include "game/core/world.h"
+#include "game_engine.h"
+#include <QDebug>
+
+AmbientStateManager::AmbientStateManager() = default;
+
+void AmbientStateManager::update(float dt, Engine::Core::World *world,
+                                 int local_owner_id,
+                                 const EntityCache &entity_cache,
+                                 const QString &victory_state) {
+  m_ambient_check_timer += dt;
+  const float check_interval = 2.0F;
+
+  if (m_ambient_check_timer < check_interval) {
+    return;
+  }
+  m_ambient_check_timer = 0.0F;
+
+  Engine::Core::AmbientState new_state = Engine::Core::AmbientState::PEACEFUL;
+
+  if (!victory_state.isEmpty()) {
+    if (victory_state == "victory") {
+      new_state = Engine::Core::AmbientState::VICTORY;
+    } else if (victory_state == "defeat") {
+      new_state = Engine::Core::AmbientState::DEFEAT;
+    }
+  } else if (is_player_in_combat(world, local_owner_id)) {
+    new_state = Engine::Core::AmbientState::COMBAT;
+  } else if (entity_cache.enemy_barracks_alive &&
+             entity_cache.player_barracks_alive) {
+    new_state = Engine::Core::AmbientState::TENSE;
+  }
+
+  if (new_state != m_current_ambient_state) {
+    Engine::Core::AmbientState const previous_state = m_current_ambient_state;
+    m_current_ambient_state = new_state;
+
+    Engine::Core::EventManager::instance().publish(
+        Engine::Core::AmbientStateChangedEvent(new_state, previous_state));
+
+    qInfo() << "Ambient state changed from" << static_cast<int>(previous_state)
+            << "to" << static_cast<int>(new_state);
+  }
+}
+
+auto AmbientStateManager::is_player_in_combat(
+    Engine::Core::World *world, int local_owner_id) const -> bool {
+  if (!world) {
+    return false;
+  }
+
+  auto units = world->get_entities_with<Engine::Core::UnitComponent>();
+  const float combat_check_radius = 15.0F;
+
+  for (auto *entity : units) {
+    auto *unit = entity->get_component<Engine::Core::UnitComponent>();
+    if ((unit == nullptr) || unit->owner_id != local_owner_id ||
+        unit->health <= 0) {
+      continue;
+    }
+
+    if (entity->has_component<Engine::Core::AttackTargetComponent>()) {
+      return true;
+    }
+
+    auto *transform = entity->get_component<Engine::Core::TransformComponent>();
+    if (transform == nullptr) {
+      continue;
+    }
+
+    for (auto *other_entity : units) {
+      auto *other_unit =
+          other_entity->get_component<Engine::Core::UnitComponent>();
+      if ((other_unit == nullptr) || other_unit->owner_id == local_owner_id ||
+          other_unit->health <= 0) {
+        continue;
+      }
+
+      auto *other_transform =
+          other_entity->get_component<Engine::Core::TransformComponent>();
+      if (other_transform == nullptr) {
+        continue;
+      }
+
+      float const dx = transform->position.x - other_transform->position.x;
+      float const dz = transform->position.z - other_transform->position.z;
+      float const dist_sq = dx * dx + dz * dz;
+
+      if (dist_sq < combat_check_radius * combat_check_radius) {
+        return true;
+      }
+    }
+  }
+
+  return false;
+}

+ 29 - 0
app/core/ambient_state_manager.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include "game/core/event_manager.h"
+
+namespace Engine::Core {
+class World;
+}
+
+struct EntityCache;
+
+class AmbientStateManager {
+public:
+  AmbientStateManager();
+
+  void update(float dt, Engine::Core::World *world, int local_owner_id,
+              const EntityCache &entity_cache, const QString &victory_state);
+
+  [[nodiscard]] Engine::Core::AmbientState current_state() const {
+    return m_current_ambient_state;
+  }
+
+private:
+  [[nodiscard]] bool is_player_in_combat(Engine::Core::World *world,
+                                         int local_owner_id) const;
+
+  Engine::Core::AmbientState m_current_ambient_state =
+      Engine::Core::AmbientState::PEACEFUL;
+  float m_ambient_check_timer = 0.0F;
+};

+ 93 - 0
app/core/audio_resource_loader.cpp

@@ -0,0 +1,93 @@
+#include "audio_resource_loader.h"
+
+#include "game/audio/AudioSystem.h"
+#include <QCoreApplication>
+#include <QDebug>
+#include <QDir>
+
+void AudioResourceLoader::load_audio_resources() {
+  auto &audio_sys = AudioSystem::getInstance();
+
+  QString const base_path =
+      QCoreApplication::applicationDirPath() + "/assets/audio/";
+  qInfo() << "Loading audio resources from:" << base_path;
+
+  QDir const audio_dir(base_path);
+  if (!audio_dir.exists()) {
+    qWarning() << "Audio assets directory does not exist:" << base_path;
+    qWarning() << "Application directory:"
+               << QCoreApplication::applicationDirPath();
+    return;
+  }
+
+  if (audio_sys.loadSound("archer_voice",
+                          (base_path + "voices/archer_voice.wav").toStdString(),
+                          AudioCategory::VOICE)) {
+    qInfo() << "Loaded archer voice";
+  } else {
+    qWarning() << "Failed to load archer voice from:"
+               << (base_path + "voices/archer_voice.wav");
+  }
+
+  if (audio_sys.loadSound(
+          "swordsman_voice",
+          (base_path + "voices/swordsman_voice.wav").toStdString(),
+          AudioCategory::VOICE)) {
+    qInfo() << "Loaded swordsman voice";
+  } else {
+    qWarning() << "Failed to load swordsman voice from:"
+               << (base_path + "voices/swordsman_voice.wav");
+  }
+
+  if (audio_sys.loadSound(
+          "spearman_voice",
+          (base_path + "voices/spearman_voice.wav").toStdString(),
+          AudioCategory::VOICE)) {
+    qInfo() << "Loaded spearman voice";
+  } else {
+    qWarning() << "Failed to load spearman voice from:"
+               << (base_path + "voices/spearman_voice.wav");
+  }
+
+  if (audio_sys.loadMusic("music_peaceful",
+                          (base_path + "music/peaceful.wav").toStdString())) {
+    qInfo() << "Loaded peaceful music";
+  } else {
+    qWarning() << "Failed to load peaceful music from:"
+               << (base_path + "music/peaceful.wav");
+  }
+
+  if (audio_sys.loadMusic("music_tense",
+                          (base_path + "music/tense.wav").toStdString())) {
+    qInfo() << "Loaded tense music";
+  } else {
+    qWarning() << "Failed to load tense music from:"
+               << (base_path + "music/tense.wav");
+  }
+
+  if (audio_sys.loadMusic("music_combat",
+                          (base_path + "music/combat.wav").toStdString())) {
+    qInfo() << "Loaded combat music";
+  } else {
+    qWarning() << "Failed to load combat music from:"
+               << (base_path + "music/combat.wav");
+  }
+
+  if (audio_sys.loadMusic("music_victory",
+                          (base_path + "music/victory.wav").toStdString())) {
+    qInfo() << "Loaded victory music";
+  } else {
+    qWarning() << "Failed to load victory music from:"
+               << (base_path + "music/victory.wav");
+  }
+
+  if (audio_sys.loadMusic("music_defeat",
+                          (base_path + "music/defeat.wav").toStdString())) {
+    qInfo() << "Loaded defeat music";
+  } else {
+    qWarning() << "Failed to load defeat music from:"
+               << (base_path + "music/defeat.wav");
+  }
+
+  qInfo() << "Audio resources loading complete";
+}

+ 8 - 0
app/core/audio_resource_loader.h

@@ -0,0 +1,8 @@
+#pragma once
+
+#include <QString>
+
+class AudioResourceLoader {
+public:
+  static void load_audio_resources();
+};

+ 98 - 0
app/core/camera_controller.cpp

@@ -0,0 +1,98 @@
+#include "camera_controller.h"
+
+#include "game/core/world.h"
+#include "game/systems/camera_service.h"
+#include "game/systems/game_state_serializer.h"
+#include "render/gl/camera.h"
+#include <QDebug>
+#include <cmath>
+
+CameraController::CameraController(Render::GL::Camera *camera,
+                                   Game::Systems::CameraService *camera_service,
+                                   Engine::Core::World *world)
+    : m_camera(camera), m_camera_service(camera_service), m_world(world) {}
+
+void CameraController::move(float dx, float dz) {
+  if (!m_camera || !m_camera_service) {
+    return;
+  }
+  m_camera_service->move(*m_camera, dx, dz);
+}
+
+void CameraController::elevate(float dy) {
+  if (!m_camera || !m_camera_service) {
+    return;
+  }
+  m_camera_service->elevate(*m_camera, dy);
+}
+
+void CameraController::reset(int local_owner_id,
+                             const Game::Systems::LevelSnapshot &level) {
+  if (!m_camera || !m_world || !m_camera_service) {
+    return;
+  }
+  m_camera_service->resetCamera(*m_camera, *m_world, local_owner_id,
+                                level.player_unit_id);
+}
+
+void CameraController::zoom(float delta) {
+  if (!m_camera || !m_camera_service) {
+    return;
+  }
+  m_camera_service->zoom(*m_camera, delta);
+}
+
+auto CameraController::distance() const -> float {
+  if (!m_camera || !m_camera_service) {
+    return 0.0F;
+  }
+  return m_camera_service->get_distance(*m_camera);
+}
+
+void CameraController::yaw(float degrees) {
+  if (!m_camera || !m_camera_service) {
+    return;
+  }
+  m_camera_service->yaw(*m_camera, degrees);
+}
+
+void CameraController::orbit(float yaw_deg, float pitch_deg) {
+  if (!m_camera || !m_camera_service) {
+    return;
+  }
+
+  if (!std::isfinite(yaw_deg) || !std::isfinite(pitch_deg)) {
+    qWarning() << "CameraController::orbit received invalid input, ignoring:"
+               << yaw_deg << pitch_deg;
+    return;
+  }
+
+  m_camera_service->orbit(*m_camera, yaw_deg, pitch_deg);
+}
+
+void CameraController::orbit_direction(int direction, bool shift) {
+  if (!m_camera || !m_camera_service) {
+    return;
+  }
+  m_camera_service->orbit_direction(*m_camera, direction, shift);
+}
+
+void CameraController::follow_selection(bool enable) {
+  if (!m_camera || !m_world || !m_camera_service) {
+    return;
+  }
+  m_camera_service->follow_selection(*m_camera, *m_world, enable);
+}
+
+void CameraController::set_follow_lerp(float alpha) {
+  if (!m_camera || !m_camera_service) {
+    return;
+  }
+  m_camera_service->set_follow_lerp(*m_camera, alpha);
+}
+
+void CameraController::update_follow(bool follow_enabled) {
+  if (follow_enabled && m_camera && m_world && m_camera_service) {
+    m_camera_service->update_follow(*m_camera, *m_world, follow_enabled);
+  }
+}

+ 38 - 0
app/core/camera_controller.h

@@ -0,0 +1,38 @@
+#pragma once
+
+namespace Engine::Core {
+class World;
+}
+
+namespace Render::GL {
+class Camera;
+}
+
+namespace Game::Systems {
+class CameraService;
+struct LevelSnapshot;
+} // namespace Game::Systems
+
+class CameraController {
+public:
+  CameraController(Render::GL::Camera *camera,
+                   Game::Systems::CameraService *camera_service,
+                   Engine::Core::World *world);
+
+  void move(float dx, float dz);
+  void elevate(float dy);
+  void reset(int local_owner_id, const Game::Systems::LevelSnapshot &level);
+  void zoom(float delta);
+  [[nodiscard]] float distance() const;
+  void yaw(float degrees);
+  void orbit(float yaw_deg, float pitch_deg);
+  void orbit_direction(int direction, bool shift);
+  void follow_selection(bool enable);
+  void set_follow_lerp(float alpha);
+  void update_follow(bool follow_enabled);
+
+private:
+  Render::GL::Camera *m_camera;
+  Game::Systems::CameraService *m_camera_service;
+  Engine::Core::World *m_world;
+};

+ 229 - 1111
app/core/game_engine.cpp

@@ -6,14 +6,22 @@
 #include "../models/cursor_manager.h"
 #include "../models/hover_tracker.h"
 #include "AudioEventHandler.h"
+#include "ambient_state_manager.h"
 #include "app/models/cursor_mode.h"
 #include "app/utils/engine_view_helpers.h"
 #include "app/utils/movement_utils.h"
 #include "app/utils/selection_utils.h"
+#include "audio_resource_loader.h"
+#include "camera_controller.h"
 #include "core/system.h"
 #include "game/audio/AudioSystem.h"
 #include "game/units/spawn_type.h"
 #include "game/units/troop_type.h"
+#include "game_state_restorer.h"
+#include "input_command_handler.h"
+#include "level_orchestrator.h"
+#include "minimap_manager.h"
+#include "renderer_bootstrap.h"
 #include <QBuffer>
 #include <QCoreApplication>
 #include <QCursor>
@@ -55,6 +63,7 @@
 #include "game/map/map_catalog.h"
 #include "game/map/map_loader.h"
 #include "game/map/map_transformer.h"
+#include "game/map/minimap/map_preview_generator.h"
 #include "game/map/minimap/minimap_generator.h"
 #include "game/map/minimap/unit_layer.h"
 #include "game/map/skirmish_loader.h"
@@ -97,7 +106,6 @@
 #include "render/geom/arrow.h"
 #include "render/geom/patrol_flags.h"
 #include "render/geom/stone.h"
-#include "render/gl/backend.h"
 #include "render/gl/bootstrap.h"
 #include "render/gl/camera.h"
 #include "render/ground/biome_renderer.h"
@@ -134,55 +142,26 @@ GameEngine::GameEngine(QObject *parent)
   Game::Systems::GlobalStatsRegistry::instance().initialize();
 
   m_world = std::make_unique<Engine::Core::World>();
-  m_renderer = std::make_unique<Render::GL::Renderer>();
-  m_camera = std::make_unique<Render::GL::Camera>();
-  m_ground = std::make_unique<Render::GL::GroundRenderer>();
-  m_terrain = std::make_unique<Render::GL::TerrainRenderer>();
-  m_biome = std::make_unique<Render::GL::BiomeRenderer>();
-  m_river = std::make_unique<Render::GL::RiverRenderer>();
-  m_road = std::make_unique<Render::GL::RoadRenderer>();
-  m_riverbank = std::make_unique<Render::GL::RiverbankRenderer>();
-  m_bridge = std::make_unique<Render::GL::BridgeRenderer>();
-  m_fog = std::make_unique<Render::GL::FogRenderer>();
-  m_stone = std::make_unique<Render::GL::StoneRenderer>();
-  m_plant = std::make_unique<Render::GL::PlantRenderer>();
-  m_pine = std::make_unique<Render::GL::PineRenderer>();
-  m_olive = std::make_unique<Render::GL::OliveRenderer>();
-  m_firecamp = std::make_unique<Render::GL::FireCampRenderer>();
-
-  m_passes = {m_ground.get(), m_terrain.get(),   m_river.get(),
-              m_road.get(),   m_riverbank.get(), m_bridge.get(),
-              m_biome.get(),  m_stone.get(),     m_plant.get(),
-              m_pine.get(),   m_olive.get(),     m_firecamp.get(),
-              m_fog.get()};
-
-  std::unique_ptr<Engine::Core::System> arrow_sys =
-      std::make_unique<Game::Systems::ArrowSystem>();
-  m_world->add_system(std::move(arrow_sys));
-
-  std::unique_ptr<Engine::Core::System> projectile_sys =
-      std::make_unique<Game::Systems::ProjectileSystem>();
-  m_world->add_system(std::move(projectile_sys));
-
-  m_world->add_system(std::make_unique<Game::Systems::MovementSystem>());
-  m_world->add_system(std::make_unique<Game::Systems::PatrolSystem>());
-  m_world->add_system(std::make_unique<Game::Systems::CombatSystem>());
-  m_world->add_system(std::make_unique<Game::Systems::CatapultAttackSystem>());
-  m_world->add_system(std::make_unique<Game::Systems::BallistaAttackSystem>());
-  m_world->add_system(std::make_unique<Game::Systems::HealingBeamSystem>());
-  m_world->add_system(std::make_unique<Game::Systems::HealingSystem>());
-  m_world->add_system(std::make_unique<Game::Systems::CaptureSystem>());
-  m_world->add_system(std::make_unique<Game::Systems::AISystem>());
-  m_world->add_system(std::make_unique<Game::Systems::ProductionSystem>());
-  m_world->add_system(
-      std::make_unique<Game::Systems::TerrainAlignmentSystem>());
-  m_world->add_system(std::make_unique<Game::Systems::CleanupSystem>());
-
-  {
-    std::unique_ptr<Engine::Core::System> sel_sys =
-        std::make_unique<Game::Systems::SelectionSystem>();
-    m_world->add_system(std::move(sel_sys));
-  }
+
+  auto rendering = RendererBootstrap::initialize_rendering();
+  m_renderer = std::move(rendering.renderer);
+  m_camera = std::move(rendering.camera);
+  m_ground = std::move(rendering.ground);
+  m_terrain = std::move(rendering.terrain);
+  m_biome = std::move(rendering.biome);
+  m_river = std::move(rendering.river);
+  m_road = std::move(rendering.road);
+  m_riverbank = std::move(rendering.riverbank);
+  m_bridge = std::move(rendering.bridge);
+  m_fog = std::move(rendering.fog);
+  m_stone = std::move(rendering.stone);
+  m_plant = std::move(rendering.plant);
+  m_pine = std::move(rendering.pine);
+  m_olive = std::move(rendering.olive);
+  m_firecamp = std::move(rendering.firecamp);
+  m_passes = std::move(rendering.passes);
+
+  RendererBootstrap::initialize_world_systems(*m_world);
 
   m_pickingService = std::make_unique<Game::Systems::PickingService>();
   m_victoryService = std::make_unique<Game::Systems::VictoryService>();
@@ -215,13 +194,24 @@ GameEngine::GameEngine(QObject *parent)
 
   if (AudioSystem::getInstance().initialize()) {
     qInfo() << "AudioSystem initialized successfully";
-    load_audio_resources();
+    AudioResourceLoader::load_audio_resources();
   } else {
     qWarning() << "Failed to initialize AudioSystem";
   }
 
   m_audio_systemProxy = std::make_unique<App::Models::AudioSystemProxy>(this);
 
+  m_minimap_manager = std::make_unique<MinimapManager>();
+  m_ambient_state_manager = std::make_unique<AmbientStateManager>();
+
+  m_input_handler = std::make_unique<InputCommandHandler>(
+      m_world.get(), m_selectionController.get(), m_commandController.get(),
+      m_cursorManager.get(), m_hoverTracker.get(), m_pickingService.get(),
+      m_camera.get());
+
+  m_camera_controller = std::make_unique<CameraController>(
+      m_camera.get(), m_cameraService.get(), m_world.get());
+
   m_audioEventHandler =
       std::make_unique<Game::Audio::AudioEventHandler>(m_world.get());
   if (m_audioEventHandler->initialize()) {
@@ -374,10 +364,9 @@ void GameEngine::on_map_clicked(qreal sx, qreal sy) {
     return;
   }
   ensure_initialized();
-  if (m_selectionController && m_camera) {
-    m_selectionController->on_click_select(sx, sy, false, m_viewport.width,
-                                           m_viewport.height, m_camera.get(),
-                                           m_runtime.local_owner_id);
+  if (m_input_handler) {
+    m_input_handler->on_map_clicked(sx, sy, m_runtime.local_owner_id,
+                                    m_viewport);
   }
 }
 
@@ -386,62 +375,9 @@ void GameEngine::on_right_click(qreal sx, qreal sy) {
     return;
   }
   ensure_initialized();
-  auto *selection_system =
-      m_world->get_system<Game::Systems::SelectionSystem>();
-  if (selection_system == nullptr) {
-    return;
-  }
-
-  if (m_cursorManager->mode() == CursorMode::Patrol ||
-      m_cursorManager->mode() == CursorMode::Attack) {
-    set_cursor_mode(CursorMode::Normal);
-    return;
-  }
-
-  const auto &sel = selection_system->get_selected_units();
-  if (sel.empty()) {
-
-    return;
-  }
-
-  if (m_pickingService && m_camera) {
-    Engine::Core::EntityID const target_id = m_pickingService->pick_unit_first(
-        float(sx), float(sy), *m_world, *m_camera, m_viewport.width,
-        m_viewport.height, 0);
-
-    if (target_id != 0U) {
-      auto *target_entity = m_world->get_entity(target_id);
-      if (target_entity != nullptr) {
-        auto *target_unit =
-            target_entity->get_component<Engine::Core::UnitComponent>();
-        if (target_unit != nullptr) {
-
-          bool const is_enemy =
-              (target_unit->owner_id != m_runtime.local_owner_id);
-
-          if (is_enemy) {
-
-            Game::Systems::CommandService::attack_target(*m_world, sel,
-                                                         target_id, true);
-            return;
-          }
-        }
-      }
-    }
-  }
-
-  if (m_pickingService && m_camera) {
-    QVector3D hit;
-    if (m_pickingService->screen_to_ground(QPointF(sx, sy), *m_camera,
-                                           m_viewport.width, m_viewport.height,
-                                           hit)) {
-      auto targets = Game::Systems::FormationPlanner::spreadFormation(
-          int(sel.size()), hit,
-          Game::GameConfig::instance().gameplay().formation_spacing_default);
-      Game::Systems::CommandService::MoveOptions opts;
-      opts.group_move = sel.size() > 1;
-      Game::Systems::CommandService::moveUnits(*m_world, sel, targets, opts);
-    }
+  if (m_input_handler) {
+    m_input_handler->on_right_click(sx, sy, m_runtime.local_owner_id,
+                                    m_viewport);
   }
 }
 
@@ -450,91 +386,44 @@ void GameEngine::on_attack_click(qreal sx, qreal sy) {
     return;
   }
   ensure_initialized();
-  if (!m_commandController || !m_camera) {
-    return;
-  }
-
-  auto result = m_commandController->onAttackClick(
-      sx, sy, m_viewport.width, m_viewport.height, m_camera.get());
-
-  auto *selection_system =
-      m_world->get_system<Game::Systems::SelectionSystem>();
-  if ((selection_system == nullptr) || !m_pickingService || !m_camera ||
-      !m_world) {
-    return;
-  }
-
-  const auto &selected = selection_system->get_selected_units();
-  if (!selected.empty()) {
-    Engine::Core::EntityID const target_id = m_pickingService->pick_unit_first(
-        float(sx), float(sy), *m_world, *m_camera, m_viewport.width,
-        m_viewport.height, 0);
-
-    if (target_id != 0) {
-      auto *target_entity = m_world->get_entity(target_id);
-      if (target_entity != nullptr) {
-        auto *target_unit =
-            target_entity->get_component<Engine::Core::UnitComponent>();
-        if ((target_unit != nullptr) &&
-            target_unit->owner_id != m_runtime.local_owner_id) {
-          App::Controllers::ActionVFX::spawnAttackArrow(m_world.get(),
-                                                        target_id);
-        }
-      }
-    }
-  }
-
-  if (result.resetCursorToNormal) {
-    set_cursor_mode(CursorMode::Normal);
+  if (m_input_handler) {
+    m_input_handler->on_attack_click(sx, sy, m_viewport);
   }
 }
 
 void GameEngine::reset_movement(Engine::Core::Entity *entity) {
-  App::Utils::reset_movement(entity);
+  InputCommandHandler::reset_movement(entity);
 }
 
 void GameEngine::on_stop_command() {
-  if (!m_commandController) {
+  if (!m_input_handler) {
     return;
   }
   ensure_initialized();
-
-  auto result = m_commandController->onStopCommand();
-  if (result.resetCursorToNormal) {
-    set_cursor_mode(CursorMode::Normal);
-  }
+  m_input_handler->on_stop_command();
 }
 
 void GameEngine::on_hold_command() {
-  if (!m_commandController) {
+  if (!m_input_handler) {
     return;
   }
   ensure_initialized();
-
-  auto result = m_commandController->onHoldCommand();
-  if (result.resetCursorToNormal) {
-    set_cursor_mode(CursorMode::Normal);
-  }
+  m_input_handler->on_hold_command();
 }
 
 auto GameEngine::any_selected_in_hold_mode() const -> bool {
-  if (!m_commandController) {
+  if (!m_input_handler) {
     return false;
   }
-  return m_commandController->anySelectedInHoldMode();
+  return m_input_handler->any_selected_in_hold_mode();
 }
 
 void GameEngine::on_patrol_click(qreal sx, qreal sy) {
-  if (!m_commandController || !m_camera) {
+  if (!m_input_handler || !m_camera) {
     return;
   }
   ensure_initialized();
-
-  auto result = m_commandController->onPatrolClick(
-      sx, sy, m_viewport.width, m_viewport.height, m_camera.get());
-  if (result.resetCursorToNormal) {
-    set_cursor_mode(CursorMode::Normal);
-  }
+  m_input_handler->on_patrol_click(sx, sy, m_viewport);
 }
 
 void GameEngine::update_cursor(Qt::CursorShape newCursor) {
@@ -593,14 +482,9 @@ void GameEngine::set_hover_at_screen(qreal sx, qreal sy) {
     return;
   }
   ensure_initialized();
-  if (!m_hoverTracker || !m_camera || !m_world) {
-    return;
+  if (m_input_handler) {
+    m_input_handler->set_hover_at_screen(sx, sy, m_viewport);
   }
-
-  m_cursorManager->updateCursorShape(m_window);
-
-  m_hoverTracker->update_hover(float(sx), float(sy), *m_world, *m_camera,
-                               m_viewport.width, m_viewport.height);
 }
 
 void GameEngine::on_click_select(qreal sx, qreal sy, bool additive) {
@@ -608,10 +492,9 @@ void GameEngine::on_click_select(qreal sx, qreal sy, bool additive) {
     return;
   }
   ensure_initialized();
-  if (m_selectionController && m_camera) {
-    m_selectionController->on_click_select(sx, sy, additive, m_viewport.width,
-                                           m_viewport.height, m_camera.get(),
-                                           m_runtime.local_owner_id);
+  if (m_input_handler) {
+    m_input_handler->on_click_select(sx, sy, additive, m_runtime.local_owner_id,
+                                     m_viewport);
   }
 }
 
@@ -621,27 +504,24 @@ void GameEngine::on_area_selected(qreal x1, qreal y1, qreal x2, qreal y2,
     return;
   }
   ensure_initialized();
-  if (m_selectionController && m_camera) {
-    m_selectionController->on_area_selected(
-        x1, y1, x2, y2, additive, m_viewport.width, m_viewport.height,
-        m_camera.get(), m_runtime.local_owner_id);
+  if (m_input_handler) {
+    m_input_handler->on_area_selected(x1, y1, x2, y2, additive,
+                                      m_runtime.local_owner_id, m_viewport);
   }
 }
 
 void GameEngine::select_all_troops() {
   ensure_initialized();
-  if (m_selectionController) {
-    m_selectionController->select_all_player_troops(m_runtime.local_owner_id);
+  if (m_input_handler) {
+    m_input_handler->select_all_troops(m_runtime.local_owner_id);
   }
 }
 
 void GameEngine::select_unit_by_id(int unitId) {
   ensure_initialized();
-  if (!m_selectionController || (unitId <= 0)) {
-    return;
+  if (m_input_handler) {
+    m_input_handler->select_unit_by_id(unitId, m_runtime.local_owner_id);
   }
-  m_selectionController->select_single_unit(
-      static_cast<Engine::Core::EntityID>(unitId), m_runtime.local_owner_id);
 }
 
 void GameEngine::ensure_initialized() {
@@ -693,7 +573,11 @@ void GameEngine::update(float dt) {
   }
 
   if (!m_runtime.paused && !m_runtime.loading) {
-    update_ambient_state(dt);
+    if (m_ambient_state_manager) {
+      m_ambient_state_manager->update(dt, m_world.get(),
+                                      m_runtime.local_owner_id, m_entity_cache,
+                                      m_runtime.victory_state);
+    }
   }
 
   if (m_renderer) {
@@ -709,37 +593,42 @@ void GameEngine::update(float dt) {
 
     auto &visibility_service = Game::Map::VisibilityService::instance();
     if (visibility_service.is_initialized()) {
-
-      m_runtime.visibilityUpdateAccumulator += dt;
+      m_runtime.visibility_update_accumulator += dt;
       const float visibility_update_interval =
           Game::GameConfig::instance().gameplay().visibility_update_interval;
-      if (m_runtime.visibilityUpdateAccumulator >= visibility_update_interval) {
-        m_runtime.visibilityUpdateAccumulator = 0.0F;
+      if (m_runtime.visibility_update_accumulator >=
+          visibility_update_interval) {
+        m_runtime.visibility_update_accumulator = 0.0F;
         visibility_service.update(*m_world, m_runtime.local_owner_id);
       }
 
       const auto new_version = visibility_service.version();
-      if (new_version != m_runtime.visibilityVersion) {
+      if (new_version != m_runtime.visibility_version) {
         if (m_fog) {
           m_fog->update_mask(visibility_service.getWidth(),
                              visibility_service.getHeight(),
                              visibility_service.getTileSize(),
                              visibility_service.snapshotCells());
         }
-        m_runtime.visibilityVersion = new_version;
+        m_runtime.visibility_version = new_version;
       }
     }
 
-    update_minimap_fog(dt);
+    if (m_minimap_manager) {
+      m_minimap_manager->update_fog(dt, m_runtime.local_owner_id);
+      auto *selection_system =
+          m_world->get_system<Game::Systems::SelectionSystem>();
+      m_minimap_manager->update_units(m_world.get(), selection_system);
+      emit minimap_image_changed();
+    }
   }
 
   if (m_victoryService && m_world) {
     m_victoryService->update(*m_world, dt);
   }
 
-  if (m_followSelectionEnabled && m_camera && m_world && m_cameraService) {
-    m_cameraService->update_follow(*m_camera, *m_world,
-                                   m_followSelectionEnabled);
+  if (m_camera_controller) {
+    m_camera_controller->update_follow(m_followSelectionEnabled);
   }
 
   if (m_selectedUnitsModel != nullptr) {
@@ -747,9 +636,9 @@ void GameEngine::update(float dt) {
         m_world->get_system<Game::Systems::SelectionSystem>();
     if ((selection_system != nullptr) &&
         !selection_system->get_selected_units().empty()) {
-      m_runtime.selectionRefreshCounter++;
-      if (m_runtime.selectionRefreshCounter >= 15) {
-        m_runtime.selectionRefreshCounter = 0;
+      m_runtime.selection_refresh_counter++;
+      if (m_runtime.selection_refresh_counter >= 15) {
+        m_runtime.selection_refresh_counter = 0;
         emit selected_units_data_changed();
       }
     }
@@ -806,9 +695,8 @@ void GameEngine::render(int pixelWidth, int pixelHeight) {
     }
   }
 
-  {
-    Render::GL::render_healer_auras(m_renderer.get(), m_renderer->resources(),
-                                    m_world.get());
+  if (auto *res = m_renderer->resources()) {
+    Render::GL::render_healer_auras(m_renderer.get(), res, m_world.get());
   }
 
   if (auto *res = m_renderer->resources()) {
@@ -823,10 +711,10 @@ void GameEngine::render(int pixelWidth, int pixelHeight) {
 
   qreal const current_x = global_cursor_x();
   qreal const current_y = global_cursor_y();
-  if (current_x != m_runtime.lastCursorX ||
-      current_y != m_runtime.lastCursorY) {
-    m_runtime.lastCursorX = current_x;
-    m_runtime.lastCursorY = current_y;
+  if (current_x != m_runtime.last_cursor_x ||
+      current_y != m_runtime.last_cursor_y) {
+    m_runtime.last_cursor_x = current_x;
+    m_runtime.last_cursor_y = current_y;
     emit global_cursor_changed();
   }
 }
@@ -863,97 +751,72 @@ void GameEngine::sync_selection_flags() {
 
 void GameEngine::camera_move(float dx, float dz) {
   ensure_initialized();
-  if (!m_camera || !m_cameraService) {
-    return;
+  if (m_camera_controller) {
+    m_camera_controller->move(dx, dz);
   }
-
-  m_cameraService->move(*m_camera, dx, dz);
 }
 
 void GameEngine::camera_elevate(float dy) {
   ensure_initialized();
-  if (!m_camera || !m_cameraService) {
-    return;
+  if (m_camera_controller) {
+    m_camera_controller->elevate(dy);
   }
-
-  m_cameraService->elevate(*m_camera, dy);
 }
 
 void GameEngine::reset_camera() {
   ensure_initialized();
-  if (!m_camera || !m_world || !m_cameraService) {
-    return;
+  if (m_camera_controller) {
+    m_camera_controller->reset(m_runtime.local_owner_id, m_level);
   }
-
-  m_cameraService->resetCamera(*m_camera, *m_world, m_runtime.local_owner_id,
-                               m_level.player_unit_id);
 }
 
 void GameEngine::camera_zoom(float delta) {
   ensure_initialized();
-  if (!m_camera || !m_cameraService) {
-    return;
+  if (m_camera_controller) {
+    m_camera_controller->zoom(delta);
   }
-
-  m_cameraService->zoom(*m_camera, delta);
 }
 
 auto GameEngine::camera_distance() const -> float {
-  if (!m_camera || !m_cameraService) {
-    return 0.0F;
+  if (m_camera_controller) {
+    return m_camera_controller->distance();
   }
-  return m_cameraService->get_distance(*m_camera);
+  return 0.0F;
 }
 
 void GameEngine::camera_yaw(float degrees) {
   ensure_initialized();
-  if (!m_camera || !m_cameraService) {
-    return;
+  if (m_camera_controller) {
+    m_camera_controller->yaw(degrees);
   }
-
-  m_cameraService->yaw(*m_camera, degrees);
 }
 
 void GameEngine::camera_orbit(float yaw_deg, float pitch_deg) {
   ensure_initialized();
-  if (!m_camera || !m_cameraService) {
-    return;
-  }
-
-  if (!std::isfinite(yaw_deg) || !std::isfinite(pitch_deg)) {
-    qWarning() << "GameEngine::camera_orbit received invalid input, ignoring:"
-               << yaw_deg << pitch_deg;
-    return;
+  if (m_camera_controller) {
+    m_camera_controller->orbit(yaw_deg, pitch_deg);
   }
-
-  m_cameraService->orbit(*m_camera, yaw_deg, pitch_deg);
 }
 
 void GameEngine::camera_orbit_direction(int direction, bool shift) {
-  if (!m_camera || !m_cameraService) {
-    return;
+  if (m_camera_controller) {
+    m_camera_controller->orbit_direction(direction, shift);
   }
-
-  m_cameraService->orbit_direction(*m_camera, direction, shift);
 }
 
 void GameEngine::camera_follow_selection(bool enable) {
   ensure_initialized();
   m_followSelectionEnabled = enable;
-  if (!m_camera || !m_world || !m_cameraService) {
-    return;
+  if (m_camera_controller) {
+    m_camera_controller->follow_selection(enable);
   }
-
-  m_cameraService->follow_selection(*m_camera, *m_world, enable);
 }
 
 void GameEngine::camera_set_follow_lerp(float alpha) {
   ensure_initialized();
-  if (!m_camera || !m_cameraService) {
-    return;
+  if (m_camera_controller) {
+    m_camera_controller->set_follow_lerp(alpha);
   }
-
-  m_cameraService->set_follow_lerp(*m_camera, alpha);
 }
 
 auto GameEngine::selected_units_model() -> QAbstractItemModel * {
@@ -972,7 +835,7 @@ auto GameEngine::has_units_selected() const -> bool {
 }
 
 auto GameEngine::player_troop_count() const -> int {
-  return m_entityCache.playerTroopCount;
+  return m_entity_cache.player_troop_count;
 }
 
 auto GameEngine::has_selected_type(const QString &type) const -> bool {
@@ -1191,23 +1054,23 @@ void GameEngine::start_campaign_mission(const QString &campaign_id) {
     return;
   }
 
-  QVariantMap selectedCampaign;
+  QVariantMap selected_campaign;
   for (const auto &campaign : campaigns) {
-    auto campaignMap = campaign.toMap();
-    if (campaignMap.value("id").toString() == campaign_id) {
-      selectedCampaign = campaignMap;
+    auto campaign_map = campaign.toMap();
+    if (campaign_map.value("id").toString() == campaign_id) {
+      selected_campaign = campaign_map;
       break;
     }
   }
 
-  if (selectedCampaign.isEmpty()) {
+  if (selected_campaign.isEmpty()) {
     set_error("Campaign not found: " + campaign_id);
     return;
   }
 
   m_current_campaign_id = campaign_id;
 
-  QString mapPath = selectedCampaign.value("mapPath").toString();
+  QString map_path = selected_campaign.value("mapPath").toString();
 
   QVariantList playerConfigs;
 
@@ -1229,7 +1092,7 @@ void GameEngine::start_campaign_mission(const QString &campaign_id) {
   player2.insert("isHuman", false);
   playerConfigs.append(player2);
 
-  start_skirmish(mapPath, playerConfigs);
+  start_skirmish(map_path, playerConfigs);
 }
 
 void GameEngine::mark_current_mission_completed() {
@@ -1285,58 +1148,40 @@ void GameEngine::start_skirmish(const QString &map_path,
       m_hoverTracker->update_hover(-1, -1, *m_world, *m_camera, 0, 0);
     }
 
-    m_entityCache.reset();
-
-    Game::Map::SkirmishLoader loader(*m_world, *m_renderer, *m_camera);
-    loader.set_ground_renderer(m_ground.get());
-    loader.set_terrain_renderer(m_terrain.get());
-    loader.set_biome_renderer(m_biome.get());
-    loader.set_river_renderer(m_river.get());
-    loader.set_road_renderer(m_road.get());
-    loader.set_riverbank_renderer(m_riverbank.get());
-    loader.set_bridge_renderer(m_bridge.get());
-    loader.set_fog_renderer(m_fog.get());
-    loader.set_stone_renderer(m_stone.get());
-    loader.set_plant_renderer(m_plant.get());
-    loader.set_pine_renderer(m_pine.get());
-    loader.set_olive_renderer(m_olive.get());
-    loader.set_fire_camp_renderer(m_firecamp.get());
-
-    loader.set_on_owners_updated([this]() { emit owner_info_changed(); });
-
-    loader.set_on_visibility_mask_ready([this]() {
-      m_runtime.visibilityVersion =
+    LevelOrchestrator orchestrator;
+    LevelOrchestrator::RendererRefs renderers{
+        m_renderer.get(), m_camera.get(), m_ground.get(),  m_terrain.get(),
+        m_biome.get(),    m_river.get(),  m_road.get(),    m_riverbank.get(),
+        m_bridge.get(),   m_fog.get(),    m_stone.get(),   m_plant.get(),
+        m_pine.get(),     m_olive.get(),  m_firecamp.get()};
+
+    auto visibility_ready = [this]() {
+      m_runtime.visibility_version =
           Game::Map::VisibilityService::instance().version();
-      m_runtime.visibilityUpdateAccumulator = 0.0F;
-    });
+      m_runtime.visibility_update_accumulator = 0.0F;
+    };
 
-    int updated_player_id = m_selected_player_id;
-    auto result = loader.start(map_path, playerConfigs, m_selected_player_id,
-                               updated_player_id);
+    auto owner_update = [this]() { emit owner_info_changed(); };
 
-    if (updated_player_id != m_selected_player_id) {
-      m_selected_player_id = updated_player_id;
+    auto load_result = orchestrator.load_skirmish(
+        map_path, playerConfigs, m_selected_player_id, *m_world, renderers,
+        m_level, m_entity_cache, m_victoryService.get(),
+        m_minimap_manager.get(), visibility_ready, owner_update);
+
+    if (load_result.updated_player_id != m_selected_player_id) {
+      m_selected_player_id = load_result.updated_player_id;
       emit selected_player_id_changed();
     }
 
-    if (!result.ok && !result.errorMessage.isEmpty()) {
-      set_error(result.errorMessage);
+    if (!load_result.success) {
+      set_error(load_result.error_message);
+      m_runtime.loading = false;
+      return;
     }
 
-    m_runtime.local_owner_id = updated_player_id;
-    m_level.map_name = result.map_name;
-    m_level.player_unit_id = result.player_unit_id;
-    m_level.cam_fov = result.cam_fov;
-    m_level.cam_near = result.cam_near;
-    m_level.cam_far = result.cam_far;
-    m_level.max_troops_per_player = result.max_troops_per_player;
-
-    Game::GameConfig::instance().set_max_troops_per_player(
-        result.max_troops_per_player);
+    m_runtime.local_owner_id = load_result.updated_player_id;
 
     if (m_victoryService) {
-      m_victoryService->configure(result.victoryConfig,
-                                  m_runtime.local_owner_id);
       m_victoryService->setVictoryCallback([this](const QString &state) {
         if (m_runtime.victory_state != state) {
           m_runtime.victory_state = state;
@@ -1349,45 +1194,12 @@ void GameEngine::start_skirmish(const QString &map_path,
       });
     }
 
-    if (result.has_focus_position && m_camera) {
-      const auto &cam_config = Game::GameConfig::instance().camera();
-      m_camera->set_rts_view(result.focusPosition, cam_config.default_distance,
-                             cam_config.default_pitch, cam_config.default_yaw);
-    }
-
-    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;
 
-    if (auto *ai_system = m_world->get_system<Game::Systems::AISystem>()) {
-      ai_system->reinitialize();
-    }
-
-    rebuild_entity_cache();
-    auto &troops = Game::Systems::TroopCountRegistry::instance();
-    troops.rebuild_from_world(*m_world);
+    GameStateRestorer::rebuild_entity_cache(m_world.get(), m_entity_cache,
+                                            m_runtime.local_owner_id);
 
-    auto &stats_registry = Game::Systems::GlobalStatsRegistry::instance();
-    stats_registry.rebuild_from_world(*m_world);
-
-    auto &owner_registry = Game::Systems::OwnerRegistry::instance();
-    const auto &all_owners = owner_registry.get_all_owners();
-    for (const auto &owner : all_owners) {
-      if (owner.type == Game::Systems::OwnerType::Player ||
-          owner.type == Game::Systems::OwnerType::AI) {
-        stats_registry.mark_game_start(owner.owner_id);
-      }
-    }
-
-    m_currentAmbientState = Engine::Core::AmbientState::PEACEFUL;
-    m_ambientCheckTimer = 0.0F;
+    m_ambient_state_manager = std::make_unique<AmbientStateManager>();
 
     Engine::Core::EventManager::instance().publish(
         Engine::Core::AmbientStateChangedEvent(
@@ -1400,7 +1212,7 @@ void GameEngine::start_skirmish(const QString &map_path,
 
 void GameEngine::open_settings() {
   if (m_saveLoadService) {
-    m_saveLoadService->openSettings();
+    m_saveLoadService->open_settings();
   }
 }
 
@@ -1427,12 +1239,12 @@ auto GameEngine::load_from_slot(const QString &slot) -> bool {
   m_runtime.loading = true;
 
   if (!m_saveLoadService->load_game_from_slot(*m_world, slot)) {
-    set_error(m_saveLoadService->getLastError());
+    set_error(m_saveLoadService->get_last_error());
     m_runtime.loading = false;
     return false;
   }
 
-  const QJsonObject meta = m_saveLoadService->getLastMetadata();
+  const QJsonObject meta = m_saveLoadService->get_last_metadata();
 
   Game::Systems::GameStateSerializer::restoreLevelFromMetadata(meta, m_level);
   Game::Systems::GameStateSerializer::restoreCameraFromMetadata(
@@ -1443,15 +1255,24 @@ auto GameEngine::load_from_slot(const QString &slot) -> bool {
                                                                  runtime_snap);
   apply_runtime_snapshot(runtime_snap);
 
-  restore_environment_from_metadata(meta);
+  GameStateRestorer::RendererRefs renderers{
+      m_renderer.get(), m_camera.get(), m_ground.get(),  m_terrain.get(),
+      m_biome.get(),    m_river.get(),  m_road.get(),    m_riverbank.get(),
+      m_bridge.get(),   m_fog.get(),    m_stone.get(),   m_plant.get(),
+      m_pine.get(),     m_olive.get(),  m_firecamp.get()};
+  GameStateRestorer::restore_environment_from_metadata(
+      meta, m_world.get(), renderers, m_level, m_runtime.local_owner_id,
+      m_viewport);
 
   auto unit_reg = std::make_shared<Game::Units::UnitFactoryRegistry>();
   Game::Units::registerBuiltInUnits(*unit_reg);
   Game::Map::MapTransformer::setFactoryRegistry(unit_reg);
   qInfo() << "Factory registry reinitialized after loading saved game";
 
-  rebuild_registries_after_load();
-  rebuild_entity_cache();
+  GameStateRestorer::rebuild_registries_after_load(
+      m_world.get(), m_selected_player_id, m_level, m_runtime.local_owner_id);
+  GameStateRestorer::rebuild_entity_cache(m_world.get(), m_entity_cache,
+                                          m_runtime.local_owner_id);
 
   if (auto *ai_system = m_world->get_system<Game::Systems::AISystem>()) {
     qInfo() << "Reinitializing AI system after loading saved game";
@@ -1482,9 +1303,9 @@ auto GameEngine::save_to_slot(const QString &slot,
       *m_world, m_camera.get(), m_level, runtime_snap);
   meta["title"] = title;
   const QByteArray screenshot = capture_screenshot();
-  if (!m_saveLoadService->saveGameToSlot(*m_world, slot, title,
-                                         m_level.map_name, meta, screenshot)) {
-    set_error(m_saveLoadService->getLastError());
+  if (!m_saveLoadService->save_game_to_slot(
+          *m_world, slot, title, m_level.map_name, meta, screenshot)) {
+    set_error(m_saveLoadService->get_last_error());
     return false;
   }
   emit save_slots_changed();
@@ -1508,10 +1329,10 @@ auto GameEngine::delete_save_slot(const QString &slotName) -> bool {
     return false;
   }
 
-  bool const success = m_saveLoadService->deleteSaveSlot(slotName);
+  bool const success = m_saveLoadService->delete_save_slot(slotName);
 
   if (!success) {
-    QString const error = m_saveLoadService->getLastError();
+    QString const error = m_saveLoadService->get_last_error();
     qWarning() << "Failed to delete save slot:" << error;
     set_error(error);
   } else {
@@ -1521,9 +1342,38 @@ auto GameEngine::delete_save_slot(const QString &slotName) -> bool {
   return success;
 }
 
+auto GameEngine::to_runtime_snapshot() const -> Game::Systems::RuntimeSnapshot {
+  Game::Systems::RuntimeSnapshot snapshot;
+  snapshot.paused = m_runtime.paused;
+  snapshot.time_scale = m_runtime.time_scale;
+  snapshot.local_owner_id = m_runtime.local_owner_id;
+  snapshot.victory_state = m_runtime.victory_state;
+  snapshot.cursor_mode = static_cast<int>(m_runtime.cursor_mode);
+  snapshot.selected_player_id = m_selected_player_id;
+  snapshot.follow_selection = m_followSelectionEnabled;
+  return snapshot;
+}
+
+void GameEngine::apply_runtime_snapshot(
+    const Game::Systems::RuntimeSnapshot &snapshot) {
+  m_runtime.paused = snapshot.paused;
+  m_runtime.time_scale = snapshot.time_scale;
+  m_runtime.local_owner_id = snapshot.local_owner_id;
+  m_runtime.victory_state = snapshot.victory_state;
+  m_selected_player_id = snapshot.selected_player_id;
+  m_followSelectionEnabled = snapshot.follow_selection;
+
+  m_runtime.cursor_mode = static_cast<CursorMode>(snapshot.cursor_mode);
+  if (m_cursorManager) {
+    m_cursorManager->setMode(m_runtime.cursor_mode);
+  }
+}
+
+auto GameEngine::capture_screenshot() const -> QByteArray { return {}; }
+
 void GameEngine::exit_game() {
   if (m_saveLoadService) {
-    m_saveLoadService->exitGame();
+    m_saveLoadService->exit_game();
   }
 }
 
@@ -1600,23 +1450,23 @@ void GameEngine::on_unit_spawned(const Engine::Core::UnitSpawnedEvent &event) {
 
   if (event.owner_id == m_runtime.local_owner_id) {
     if (event.spawn_type == Game::Units::SpawnType::Barracks) {
-      m_entityCache.playerBarracksAlive = true;
+      m_entity_cache.player_barracks_alive = true;
     } else {
       int const production_cost =
           Game::Units::TroopConfig::instance().getProductionCost(
               event.spawn_type);
-      m_entityCache.playerTroopCount += production_cost;
+      m_entity_cache.player_troop_count += production_cost;
     }
   } else if (owners.is_ai(event.owner_id)) {
     if (event.spawn_type == Game::Units::SpawnType::Barracks) {
-      m_entityCache.enemyBarracksCount++;
-      m_entityCache.enemyBarracksAlive = true;
+      m_entity_cache.enemy_barracks_count++;
+      m_entity_cache.enemy_barracks_alive = true;
     }
   }
 
   auto emit_if_changed = [&] {
-    if (m_entityCache.playerTroopCount != m_runtime.lastTroopCount) {
-      m_runtime.lastTroopCount = m_entityCache.playerTroopCount;
+    if (m_entity_cache.player_troop_count != m_runtime.last_troop_count) {
+      m_runtime.last_troop_count = m_entity_cache.player_troop_count;
       emit troop_count_changed();
     }
   };
@@ -1628,768 +1478,36 @@ void GameEngine::on_unit_died(const Engine::Core::UnitDiedEvent &event) {
 
   if (event.owner_id == m_runtime.local_owner_id) {
     if (event.spawn_type == Game::Units::SpawnType::Barracks) {
-      m_entityCache.playerBarracksAlive = false;
+      m_entity_cache.player_barracks_alive = false;
     } else {
       int const production_cost =
           Game::Units::TroopConfig::instance().getProductionCost(
               event.spawn_type);
-      m_entityCache.playerTroopCount -= production_cost;
-      m_entityCache.playerTroopCount =
-          std::max(0, m_entityCache.playerTroopCount);
+      m_entity_cache.player_troop_count -= production_cost;
+      m_entity_cache.player_troop_count =
+          std::max(0, m_entity_cache.player_troop_count);
     }
   } else if (owners.is_ai(event.owner_id)) {
     if (event.spawn_type == Game::Units::SpawnType::Barracks) {
-      m_entityCache.enemyBarracksCount--;
-      m_entityCache.enemyBarracksCount =
-          std::max(0, m_entityCache.enemyBarracksCount);
-      m_entityCache.enemyBarracksAlive = (m_entityCache.enemyBarracksCount > 0);
+      m_entity_cache.enemy_barracks_count--;
+      m_entity_cache.enemy_barracks_count =
+          std::max(0, m_entity_cache.enemy_barracks_count);
+      m_entity_cache.enemy_barracks_alive =
+          (m_entity_cache.enemy_barracks_count > 0);
     }
   }
-
-  sync_selection_flags();
-
-  auto emit_if_changed = [&] {
-    if (m_entityCache.playerTroopCount != m_runtime.lastTroopCount) {
-      m_runtime.lastTroopCount = m_entityCache.playerTroopCount;
-      emit troop_count_changed();
-    }
-  };
-  emit_if_changed();
 }
 
-void GameEngine::rebuild_entity_cache() {
-  if (!m_world) {
-    m_entityCache.reset();
-    return;
-  }
-
-  m_entityCache.reset();
-
-  auto &owners = Game::Systems::OwnerRegistry::instance();
-  auto entities = m_world->get_entities_with<Engine::Core::UnitComponent>();
-  for (auto *e : entities) {
-    auto *unit = e->get_component<Engine::Core::UnitComponent>();
-    if ((unit == nullptr) || unit->health <= 0) {
-      continue;
-    }
-
-    if (unit->owner_id == m_runtime.local_owner_id) {
-      if (unit->spawn_type == Game::Units::SpawnType::Barracks) {
-        m_entityCache.playerBarracksAlive = true;
-      } else {
-        int const production_cost =
-            Game::Units::TroopConfig::instance().getProductionCost(
-                unit->spawn_type);
-        m_entityCache.playerTroopCount += production_cost;
-      }
-    } else if (owners.is_ai(unit->owner_id)) {
-      if (unit->spawn_type == Game::Units::SpawnType::Barracks) {
-        m_entityCache.enemyBarracksCount++;
-        m_entityCache.enemyBarracksAlive = true;
-      }
-    }
+auto GameEngine::minimap_image() const -> QImage {
+  if (m_minimap_manager) {
+    return m_minimap_manager->get_image();
   }
-
-  auto emit_if_changed = [&] {
-    if (m_entityCache.playerTroopCount != m_runtime.lastTroopCount) {
-      m_runtime.lastTroopCount = m_entityCache.playerTroopCount;
-      emit troop_count_changed();
-    }
-  };
-  emit_if_changed();
+  return QImage();
 }
 
-void GameEngine::rebuild_registries_after_load() {
-  if (!m_world) {
-    return;
-  }
-
-  auto &owner_registry = Game::Systems::OwnerRegistry::instance();
-  m_runtime.local_owner_id = owner_registry.get_local_player_id();
-
-  auto &troops = Game::Systems::TroopCountRegistry::instance();
-  troops.rebuild_from_world(*m_world);
-
-  auto &stats_registry = Game::Systems::GlobalStatsRegistry::instance();
-  stats_registry.rebuild_from_world(*m_world);
-
-  const auto &all_owners = owner_registry.get_all_owners();
-  for (const auto &owner : all_owners) {
-    if (owner.type == Game::Systems::OwnerType::Player ||
-        owner.type == Game::Systems::OwnerType::AI) {
-      stats_registry.mark_game_start(owner.owner_id);
-    }
-  }
-
-  rebuild_building_collisions();
-
-  m_level.player_unit_id = 0;
-  auto units = m_world->get_entities_with<Engine::Core::UnitComponent>();
-  for (auto *entity : units) {
-    auto *unit = entity->get_component<Engine::Core::UnitComponent>();
-    if (unit == nullptr) {
-      continue;
-    }
-    if (unit->owner_id == m_runtime.local_owner_id) {
-      m_level.player_unit_id = entity->get_id();
-      break;
-    }
-  }
-
-  if (m_selected_player_id != m_runtime.local_owner_id) {
-    m_selected_player_id = m_runtime.local_owner_id;
-    emit selected_player_id_changed();
-  }
-}
-
-void GameEngine::rebuild_building_collisions() {
-  auto &registry = Game::Systems::BuildingCollisionRegistry::instance();
-  registry.clear();
-  if (!m_world) {
-    return;
-  }
-
-  auto buildings =
-      m_world->get_entities_with<Engine::Core::BuildingComponent>();
-  for (auto *entity : buildings) {
-    auto *transform = entity->get_component<Engine::Core::TransformComponent>();
-    auto *unit = entity->get_component<Engine::Core::UnitComponent>();
-    if ((transform == nullptr) || (unit == nullptr)) {
-      continue;
-    }
-
-    registry.register_building(
-        entity->get_id(), Game::Units::spawn_typeToString(unit->spawn_type),
-        transform->position.x, transform->position.z, unit->owner_id);
-  }
-}
-
-auto GameEngine::to_runtime_snapshot() const -> Game::Systems::RuntimeSnapshot {
-  Game::Systems::RuntimeSnapshot snap;
-  snap.paused = m_runtime.paused;
-  snap.time_scale = m_runtime.time_scale;
-  snap.local_owner_id = m_runtime.local_owner_id;
-  snap.victory_state = m_runtime.victory_state;
-  snap.cursor_mode = CursorModeUtils::toInt(m_runtime.cursor_mode);
-  snap.selected_player_id = m_selected_player_id;
-  snap.follow_selection = m_followSelectionEnabled;
-  return snap;
-}
-
-void GameEngine::apply_runtime_snapshot(
-    const Game::Systems::RuntimeSnapshot &snapshot) {
-  m_runtime.local_owner_id = snapshot.local_owner_id;
-  set_paused(snapshot.paused);
-  set_game_speed(snapshot.time_scale);
-
-  if (snapshot.victory_state != m_runtime.victory_state) {
-    m_runtime.victory_state = snapshot.victory_state;
-    emit victory_state_changed();
-  }
-
-  set_cursor_mode(CursorModeUtils::fromInt(snapshot.cursor_mode));
-
-  if (snapshot.selected_player_id != m_selected_player_id) {
-    m_selected_player_id = snapshot.selected_player_id;
-    emit selected_player_id_changed();
-  }
-
-  if (snapshot.follow_selection != m_followSelectionEnabled) {
-    m_followSelectionEnabled = snapshot.follow_selection;
-    if (m_camera && m_cameraService && m_world) {
-      m_cameraService->follow_selection(*m_camera, *m_world,
-                                        m_followSelectionEnabled);
-    }
-  }
-}
-
-auto GameEngine::capture_screenshot() const -> QByteArray {
-  if (m_window == nullptr) {
-    return {};
-  }
-
-  QImage const image = m_window->grabWindow();
-  if (image.isNull()) {
-    return {};
-  }
-
-  const QSize target_size(320, 180);
-  QImage const scaled =
-      image.scaled(target_size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
-
-  QByteArray buffer;
-  QBuffer q_buffer(&buffer);
-  if (!q_buffer.open(QIODevice::WriteOnly)) {
-    return {};
-  }
-
-  if (!scaled.save(&q_buffer, "PNG")) {
-    return {};
-  }
-
-  return buffer;
-}
-
-void GameEngine::restore_environment_from_metadata(
-    const QJsonObject &metadata) {
-  if (!m_world) {
-    return;
-  }
-
-  const auto fallback_grid_width = metadata.value("grid_width").toInt(50);
-  const auto fallback_grid_height = metadata.value("grid_height").toInt(50);
-  const float fallback_tile_size =
-      static_cast<float>(metadata.value("tile_size").toDouble(1.0));
-
-  auto &terrain_service = Game::Map::TerrainService::instance();
-
-  bool const terrain_already_restored = terrain_service.is_initialized();
-
-  Game::Map::MapDefinition def;
-  QString map_error;
-  bool loaded_definition = false;
-  const QString &map_path = m_level.map_path;
-
-  if (!terrain_already_restored && !map_path.isEmpty()) {
-    loaded_definition =
-        Game::Map::MapLoader::loadFromJsonFile(map_path, def, &map_error);
-    if (!loaded_definition) {
-      qWarning() << "GameEngine: Failed to load map definition from" << map_path
-                 << "during save load:" << map_error;
-    }
-  }
-
-  if (!terrain_already_restored && loaded_definition) {
-    terrain_service.initialize(def);
-
-    if (!def.name.isEmpty()) {
-      m_level.map_name = def.name;
-    }
-
-    m_level.cam_fov = def.camera.fovY;
-    m_level.cam_near = def.camera.near_plane;
-    m_level.cam_far = def.camera.far_plane;
-  }
-
-  if (m_renderer && m_camera) {
-    if (loaded_definition) {
-      Game::Map::Environment::apply(def, *m_renderer, *m_camera);
-
-      generate_minimap_for_map(def);
-    } else {
-      Game::Map::Environment::applyDefault(*m_renderer, *m_camera);
-    }
-  }
-
-  if (terrain_service.is_initialized()) {
-    const auto *height_map = terrain_service.get_height_map();
-    const int grid_width =
-        (height_map != nullptr) ? height_map->getWidth() : fallback_grid_width;
-    const int grid_height = (height_map != nullptr) ? height_map->getHeight()
-                                                    : fallback_grid_height;
-    const float tile_size = (height_map != nullptr) ? height_map->getTileSize()
-                                                    : fallback_tile_size;
-
-    if (m_ground) {
-      m_ground->configure(tile_size, grid_width, grid_height);
-      m_ground->setBiome(terrain_service.biome_settings());
-    }
-
-    if (height_map != nullptr) {
-      if (m_terrain) {
-        m_terrain->configure(*height_map, terrain_service.biome_settings());
-      }
-      if (m_river) {
-        m_river->configure(height_map->getRiverSegments(),
-                           height_map->getTileSize());
-      }
-      if (m_road) {
-        m_road->configure(terrain_service.road_segments(),
-                          height_map->getTileSize());
-      }
-      if (m_riverbank) {
-        m_riverbank->configure(height_map->getRiverSegments(), *height_map);
-      }
-      if (m_bridge) {
-        m_bridge->configure(height_map->getBridges(),
-                            height_map->getTileSize());
-      }
-      if (m_biome) {
-        m_biome->configure(*height_map, terrain_service.biome_settings());
-        m_biome->refreshGrass();
-      }
-      if (m_stone) {
-        m_stone->configure(*height_map, terrain_service.biome_settings());
-      }
-      if (m_plant) {
-        m_plant->configure(*height_map, terrain_service.biome_settings());
-      }
-      if (m_pine) {
-        m_pine->configure(*height_map, terrain_service.biome_settings());
-      }
-      if (m_olive) {
-        m_olive->configure(*height_map, terrain_service.biome_settings());
-      }
-      if (m_firecamp) {
-        m_firecamp->configure(*height_map, terrain_service.biome_settings());
-      }
-    }
-
-    Game::Systems::CommandService::initialize(grid_width, grid_height);
-
-    auto &visibility_service = Game::Map::VisibilityService::instance();
-    visibility_service.initialize(grid_width, grid_height, tile_size);
-    visibility_service.computeImmediate(*m_world, m_runtime.local_owner_id);
-
-    if (m_fog && visibility_service.is_initialized()) {
-      m_fog->update_mask(
-          visibility_service.getWidth(), visibility_service.getHeight(),
-          visibility_service.getTileSize(), visibility_service.snapshotCells());
-    }
-
-    m_runtime.visibilityVersion = visibility_service.version();
-    m_runtime.visibilityUpdateAccumulator = 0.0F;
-  } else {
-    if (m_renderer && m_camera) {
-      Game::Map::Environment::applyDefault(*m_renderer, *m_camera);
-    }
-
-    Game::Map::MapDefinition fallback_def;
-    fallback_def.grid.width = fallback_grid_width;
-    fallback_def.grid.height = fallback_grid_height;
-    fallback_def.grid.tile_size = fallback_tile_size;
-    fallback_def.max_troops_per_player = m_level.max_troops_per_player;
-    terrain_service.initialize(fallback_def);
-
-    if (m_ground) {
-      m_ground->configure(fallback_tile_size, fallback_grid_width,
-                          fallback_grid_height);
-    }
-
-    Game::Systems::CommandService::initialize(fallback_grid_width,
-                                              fallback_grid_height);
-
-    auto &visibility_service = Game::Map::VisibilityService::instance();
-    visibility_service.initialize(fallback_grid_width, fallback_grid_height,
-                                  fallback_tile_size);
-    visibility_service.computeImmediate(*m_world, m_runtime.local_owner_id);
-
-    if (m_fog && visibility_service.is_initialized()) {
-      m_fog->update_mask(
-          visibility_service.getWidth(), visibility_service.getHeight(),
-          visibility_service.getTileSize(), visibility_service.snapshotCells());
-    }
-    m_runtime.visibilityVersion = visibility_service.version();
-    m_runtime.visibilityUpdateAccumulator = 0.0F;
-  }
-}
-
-auto GameEngine::has_patrol_preview_waypoint() const -> bool {
-  return m_commandController && m_commandController->hasPatrolFirstWaypoint();
-}
-
-auto GameEngine::get_patrol_preview_waypoint() const -> QVector3D {
-  if (!m_commandController) {
-    return {};
-  }
-  return m_commandController->getPatrolFirstWaypoint();
-}
-
-void GameEngine::update_ambient_state(float dt) {
-
-  m_ambientCheckTimer += dt;
-  const float check_interval = 2.0F;
-
-  if (m_ambientCheckTimer < check_interval) {
-    return;
-  }
-  m_ambientCheckTimer = 0.0F;
-
-  Engine::Core::AmbientState new_state = Engine::Core::AmbientState::PEACEFUL;
-
-  if (!m_runtime.victory_state.isEmpty()) {
-    if (m_runtime.victory_state == "victory") {
-      new_state = Engine::Core::AmbientState::VICTORY;
-    } else if (m_runtime.victory_state == "defeat") {
-      new_state = Engine::Core::AmbientState::DEFEAT;
-    }
-  } else if (is_player_in_combat()) {
-
-    new_state = Engine::Core::AmbientState::COMBAT;
-  } else if (m_entityCache.enemyBarracksAlive &&
-             m_entityCache.playerBarracksAlive) {
-
-    new_state = Engine::Core::AmbientState::TENSE;
-  }
-
-  if (new_state != m_currentAmbientState) {
-    Engine::Core::AmbientState const previous_state = m_currentAmbientState;
-    m_currentAmbientState = new_state;
-
-    Engine::Core::EventManager::instance().publish(
-        Engine::Core::AmbientStateChangedEvent(new_state, previous_state));
-
-    qInfo() << "Ambient state changed from" << static_cast<int>(previous_state)
-            << "to" << static_cast<int>(new_state);
-  }
-}
-
-auto GameEngine::is_player_in_combat() const -> bool {
-  if (!m_world) {
-    return false;
-  }
-
-  auto units = m_world->get_entities_with<Engine::Core::UnitComponent>();
-  const float combat_check_radius = 15.0F;
-
-  for (auto *entity : units) {
-    auto *unit = entity->get_component<Engine::Core::UnitComponent>();
-    if ((unit == nullptr) || unit->owner_id != m_runtime.local_owner_id ||
-        unit->health <= 0) {
-      continue;
-    }
-
-    if (entity->has_component<Engine::Core::AttackTargetComponent>()) {
-      return true;
-    }
-
-    auto *transform = entity->get_component<Engine::Core::TransformComponent>();
-    if (transform == nullptr) {
-      continue;
-    }
-
-    for (auto *other_entity : units) {
-      auto *other_unit =
-          other_entity->get_component<Engine::Core::UnitComponent>();
-      if ((other_unit == nullptr) ||
-          other_unit->owner_id == m_runtime.local_owner_id ||
-          other_unit->health <= 0) {
-        continue;
-      }
-
-      auto *other_transform =
-          other_entity->get_component<Engine::Core::TransformComponent>();
-      if (other_transform == nullptr) {
-        continue;
-      }
-
-      float const dx = transform->position.x - other_transform->position.x;
-      float const dz = transform->position.z - other_transform->position.z;
-      float const dist_sq = dx * dx + dz * dz;
-
-      if (dist_sq < combat_check_radius * combat_check_radius) {
-        return true;
-      }
-    }
-  }
-
-  return false;
-}
-
-void GameEngine::load_audio_resources() {
-  auto &audio_sys = AudioSystem::getInstance();
-
-  QString const base_path =
-      QCoreApplication::applicationDirPath() + "/assets/audio/";
-  qInfo() << "Loading audio resources from:" << base_path;
-
-  QDir const audio_dir(base_path);
-  if (!audio_dir.exists()) {
-    qWarning() << "Audio assets directory does not exist:" << base_path;
-    qWarning() << "Application directory:"
-               << QCoreApplication::applicationDirPath();
-    return;
-  }
-
-  if (audio_sys.loadSound("archer_voice",
-                          (base_path + "voices/archer_voice.wav").toStdString(),
-                          AudioCategory::VOICE)) {
-    qInfo() << "Loaded archer voice";
-  } else {
-    qWarning() << "Failed to load archer voice from:"
-               << (base_path + "voices/archer_voice.wav");
-  }
-
-  if (audio_sys.loadSound(
-          "swordsman_voice",
-          (base_path + "voices/swordsman_voice.wav").toStdString(),
-          AudioCategory::VOICE)) {
-    qInfo() << "Loaded swordsman voice";
-  } else {
-    qWarning() << "Failed to load swordsman voice from:"
-               << (base_path + "voices/swordsman_voice.wav");
-  }
-
-  if (audio_sys.loadSound(
-          "spearman_voice",
-          (base_path + "voices/spearman_voice.wav").toStdString(),
-          AudioCategory::VOICE)) {
-    qInfo() << "Loaded spearman voice";
-  } else {
-    qWarning() << "Failed to load spearman voice from:"
-               << (base_path + "voices/spearman_voice.wav");
-  }
-
-  if (audio_sys.loadMusic("music_peaceful",
-                          (base_path + "music/peaceful.wav").toStdString())) {
-    qInfo() << "Loaded peaceful music";
-  } else {
-    qWarning() << "Failed to load peaceful music from:"
-               << (base_path + "music/peaceful.wav");
-  }
-
-  if (audio_sys.loadMusic("music_tense",
-                          (base_path + "music/tense.wav").toStdString())) {
-    qInfo() << "Loaded tense music";
-  } else {
-    qWarning() << "Failed to load tense music from:"
-               << (base_path + "music/tense.wav");
-  }
-
-  if (audio_sys.loadMusic("music_combat",
-                          (base_path + "music/combat.wav").toStdString())) {
-    qInfo() << "Loaded combat music";
-  } else {
-    qWarning() << "Failed to load combat music from:"
-               << (base_path + "music/combat.wav");
-  }
-
-  if (audio_sys.loadMusic("music_victory",
-                          (base_path + "music/victory.wav").toStdString())) {
-    qInfo() << "Loaded victory music";
-  } else {
-    qWarning() << "Failed to load victory music from:"
-               << (base_path + "music/victory.wav");
-  }
-
-  if (audio_sys.loadMusic("music_defeat",
-                          (base_path + "music/defeat.wav").toStdString())) {
-    qInfo() << "Loaded defeat music";
-  } else {
-    qWarning() << "Failed to load defeat music from:"
-               << (base_path + "music/defeat.wav");
-  }
-
-  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.is_initialized()) {
-
-    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::is_building_spawn(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();
+auto GameEngine::generate_map_preview(const QString &map_path,
+                                      const QVariantList &player_configs) const
+    -> QImage {
+  Game::Map::Minimap::MapPreviewGenerator generator;
+  return generator.generate_preview(map_path, player_configs);
 }

+ 33 - 42
app/core/game_engine.h

@@ -7,9 +7,14 @@
 #include "../utils/engine_view_helpers.h"
 #include "../utils/movement_utils.h"
 #include "../utils/selection_utils.h"
+#include "ambient_state_manager.h"
+#include "camera_controller.h"
 #include "game/audio/AudioEventHandler.h"
 #include "game/core/event_manager.h"
 #include "game/systems/game_state_serializer.h"
+#include "input_command_handler.h"
+#include "minimap_manager.h"
+#include "renderer_bootstrap.h"
 #include <QJsonObject>
 #include <QList>
 #include <QMatrix4x4>
@@ -81,6 +86,20 @@ class AudioSystemProxy;
 
 class QQuickWindow;
 
+struct EntityCache {
+  int player_troop_count = 0;
+  bool player_barracks_alive = false;
+  bool enemy_barracks_alive = false;
+  int enemy_barracks_count = 0;
+
+  void reset() {
+    player_troop_count = 0;
+    player_barracks_alive = false;
+    enemy_barracks_alive = false;
+    enemy_barracks_count = 0;
+  }
+};
+
 class GameEngine : public QObject {
   Q_OBJECT
 public:
@@ -214,6 +233,9 @@ public:
   Q_INVOKABLE bool delete_save_slot(const QString &slot_name);
   Q_INVOKABLE void exit_game();
   Q_INVOKABLE [[nodiscard]] QVariantList get_owner_info() const;
+  Q_INVOKABLE [[nodiscard]] QImage
+  generate_map_preview(const QString &map_path,
+                       const QVariantList &player_configs) const;
 
   [[nodiscard]] QImage minimap_image() const;
 
@@ -244,31 +266,13 @@ private:
     CursorMode cursor_mode{CursorMode::Normal};
     QString last_error = "";
     Qt::CursorShape current_cursor = Qt::ArrowCursor;
-    int lastTroopCount = 0;
-    std::uint64_t visibilityVersion = 0;
-    float visibilityUpdateAccumulator = 0.0F;
-    qreal lastCursorX = -1.0;
-    qreal lastCursorY = -1.0;
-    int selectionRefreshCounter = 0;
+    int last_troop_count = 0;
+    std::uint64_t visibility_version = 0;
+    float visibility_update_accumulator = 0.0F;
+    qreal last_cursor_x = -1.0;
+    qreal last_cursor_y = -1.0;
+    int selection_refresh_counter = 0;
   };
-  struct EntityCache {
-    int playerTroopCount = 0;
-    bool playerBarracksAlive = false;
-    bool enemyBarracksAlive = false;
-    int enemyBarracksCount = 0;
-
-    void reset() {
-      playerTroopCount = 0;
-      playerBarracksAlive = false;
-      enemyBarracksAlive = false;
-      enemyBarracksCount = 0;
-    }
-  };
-  struct ViewportState {
-    int width = 0;
-    int height = 0;
-  };
-
   bool screen_to_ground(const QPointF &screenPt, QVector3D &outWorld);
   bool world_to_screen(const QVector3D &world, QPointF &outScreen) const;
   void sync_selection_flags();
@@ -317,14 +321,10 @@ private:
   std::unique_ptr<Game::Map::MapCatalog> m_mapCatalog;
   std::unique_ptr<Game::Audio::AudioEventHandler> m_audioEventHandler;
   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;
+  std::unique_ptr<MinimapManager> m_minimap_manager;
+  std::unique_ptr<AmbientStateManager> m_ambient_state_manager;
+  std::unique_ptr<InputCommandHandler> m_input_handler;
+  std::unique_ptr<CameraController> m_camera_controller;
   QQuickWindow *m_window = nullptr;
   RuntimeState m_runtime;
   ViewportState m_viewport;
@@ -341,18 +341,9 @@ private:
       m_unit_died_subscription;
   Engine::Core::ScopedEventSubscription<Engine::Core::UnitSpawnedEvent>
       m_unit_spawned_subscription;
-  EntityCache m_entityCache;
-  Engine::Core::AmbientState m_currentAmbientState =
-      Engine::Core::AmbientState::PEACEFUL;
-  float m_ambientCheckTimer = 0.0F;
+  EntityCache m_entity_cache;
 
-  void update_ambient_state(float dt);
-  [[nodiscard]] bool is_player_in_combat() const;
-  static void load_audio_resources();
   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:
   void selected_units_changed();
   void selected_units_data_changed();

+ 283 - 0
app/core/game_state_restorer.cpp

@@ -0,0 +1,283 @@
+#include "game_state_restorer.h"
+
+#include "game/core/component.h"
+#include "game/core/world.h"
+#include "game/game_config.h"
+#include "game/map/environment.h"
+#include "game/map/map_loader.h"
+#include "game/map/terrain_service.h"
+#include "game/map/visibility_service.h"
+#include "game/systems/building_collision_registry.h"
+#include "game/systems/command_service.h"
+#include "game/systems/game_state_serializer.h"
+#include "game/systems/global_stats_registry.h"
+#include "game/systems/owner_registry.h"
+#include "game/systems/troop_count_registry.h"
+#include "game/units/troop_config.h"
+#include "game/units/troop_type.h"
+#include "game_engine.h"
+#include "minimap_manager.h"
+#include "render/gl/camera.h"
+#include "render/ground/biome_renderer.h"
+#include "render/ground/bridge_renderer.h"
+#include "render/ground/firecamp_renderer.h"
+#include "render/ground/fog_renderer.h"
+#include "render/ground/ground_renderer.h"
+#include "render/ground/olive_renderer.h"
+#include "render/ground/pine_renderer.h"
+#include "render/ground/plant_renderer.h"
+#include "render/ground/river_renderer.h"
+#include "render/ground/riverbank_renderer.h"
+#include "render/ground/road_renderer.h"
+#include "render/ground/stone_renderer.h"
+#include "render/ground/terrain_renderer.h"
+#include "render/scene_renderer.h"
+#include <QDebug>
+
+void GameStateRestorer::rebuild_entity_cache(Engine::Core::World *world,
+                                             EntityCache &entity_cache,
+                                             int local_owner_id) {
+  if (!world) {
+    entity_cache.reset();
+    return;
+  }
+
+  entity_cache.reset();
+
+  auto &owners = Game::Systems::OwnerRegistry::instance();
+  auto entities = world->get_entities_with<Engine::Core::UnitComponent>();
+  for (auto *e : entities) {
+    auto *unit = e->get_component<Engine::Core::UnitComponent>();
+    if ((unit == nullptr) || unit->health <= 0) {
+      continue;
+    }
+
+    if (unit->owner_id == local_owner_id) {
+      if (unit->spawn_type == Game::Units::SpawnType::Barracks) {
+        entity_cache.player_barracks_alive = true;
+      } else {
+        int const production_cost =
+            Game::Units::TroopConfig::instance().getProductionCost(
+                unit->spawn_type);
+        entity_cache.player_troop_count += production_cost;
+      }
+    } else if (owners.is_ai(unit->owner_id)) {
+      if (unit->spawn_type == Game::Units::SpawnType::Barracks) {
+        entity_cache.enemy_barracks_count++;
+        entity_cache.enemy_barracks_alive = true;
+      }
+    }
+  }
+}
+
+void GameStateRestorer::rebuild_registries_after_load(
+    Engine::Core::World *world, int &selected_player_id,
+    Game::Systems::LevelSnapshot &level, int local_owner_id) {
+  if (!world) {
+    return;
+  }
+
+  auto &owner_registry = Game::Systems::OwnerRegistry::instance();
+
+  auto &troops = Game::Systems::TroopCountRegistry::instance();
+  troops.rebuild_from_world(*world);
+
+  auto &stats_registry = Game::Systems::GlobalStatsRegistry::instance();
+  stats_registry.rebuild_from_world(*world);
+
+  const auto &all_owners = owner_registry.get_all_owners();
+  for (const auto &owner : all_owners) {
+    if (owner.type == Game::Systems::OwnerType::Player ||
+        owner.type == Game::Systems::OwnerType::AI) {
+      stats_registry.mark_game_start(owner.owner_id);
+    }
+  }
+
+  rebuild_building_collisions(world);
+
+  level.player_unit_id = 0;
+  auto units = world->get_entities_with<Engine::Core::UnitComponent>();
+  for (auto *entity : units) {
+    auto *unit = entity->get_component<Engine::Core::UnitComponent>();
+    if (unit == nullptr) {
+      continue;
+    }
+    if (unit->owner_id == local_owner_id) {
+      level.player_unit_id = entity->get_id();
+      break;
+    }
+  }
+
+  if (selected_player_id != local_owner_id) {
+    selected_player_id = local_owner_id;
+  }
+}
+
+void GameStateRestorer::rebuild_building_collisions(
+    Engine::Core::World *world) {
+  auto &registry = Game::Systems::BuildingCollisionRegistry::instance();
+  registry.clear();
+  if (!world) {
+    return;
+  }
+
+  auto buildings = world->get_entities_with<Engine::Core::BuildingComponent>();
+  for (auto *entity : buildings) {
+    auto *transform = entity->get_component<Engine::Core::TransformComponent>();
+    auto *unit = entity->get_component<Engine::Core::UnitComponent>();
+    if ((transform == nullptr) || (unit == nullptr)) {
+      continue;
+    }
+
+    registry.register_building(
+        entity->get_id(), Game::Units::spawn_typeToString(unit->spawn_type),
+        transform->position.x, transform->position.z, unit->owner_id);
+  }
+}
+
+void GameStateRestorer::restore_environment_from_metadata(
+    const QJsonObject &metadata, Engine::Core::World *world,
+    const RendererRefs &renderers, Game::Systems::LevelSnapshot &level,
+    int local_owner_id, const ViewportState &viewport) {
+  if (!world) {
+    return;
+  }
+
+  const auto fallback_grid_width = metadata.value("grid_width").toInt(50);
+  const auto fallback_grid_height = metadata.value("grid_height").toInt(50);
+  const float fallback_tile_size =
+      static_cast<float>(metadata.value("tile_size").toDouble(1.0));
+
+  auto &terrain_service = Game::Map::TerrainService::instance();
+
+  bool const terrain_already_restored = terrain_service.is_initialized();
+
+  Game::Map::MapDefinition def;
+  QString map_error;
+  bool loaded_definition = false;
+  const QString &map_path = level.map_path;
+
+  if (!terrain_already_restored && !map_path.isEmpty()) {
+    loaded_definition =
+        Game::Map::MapLoader::loadFromJsonFile(map_path, def, &map_error);
+    if (!loaded_definition) {
+      qWarning() << "GameStateRestorer: Failed to load map definition from"
+                 << map_path << "during save load:" << map_error;
+    }
+  }
+
+  if (!terrain_already_restored && loaded_definition) {
+    terrain_service.initialize(def);
+
+    if (!def.name.isEmpty()) {
+      level.map_name = def.name;
+    }
+
+    level.cam_fov = def.camera.fovY;
+    level.cam_near = def.camera.near_plane;
+    level.cam_far = def.camera.far_plane;
+  }
+
+  if (renderers.renderer && renderers.camera) {
+    if (loaded_definition) {
+      Game::Map::Environment::apply(def, *renderers.renderer,
+                                    *renderers.camera);
+    } else {
+      Game::Map::Environment::applyDefault(*renderers.renderer,
+                                           *renderers.camera);
+    }
+  }
+
+  if (terrain_service.is_initialized()) {
+    const auto *height_map = terrain_service.get_height_map();
+    const int grid_width =
+        (height_map != nullptr) ? height_map->getWidth() : fallback_grid_width;
+    const int grid_height = (height_map != nullptr) ? height_map->getHeight()
+                                                    : fallback_grid_height;
+    const float tile_size = (height_map != nullptr) ? height_map->getTileSize()
+                                                    : fallback_tile_size;
+
+    if (renderers.ground) {
+      renderers.ground->configure(tile_size, grid_width, grid_height);
+      renderers.ground->setBiome(terrain_service.biome_settings());
+    }
+
+    if (height_map != nullptr) {
+      if (renderers.terrain) {
+        renderers.terrain->configure(*height_map,
+                                     terrain_service.biome_settings());
+      }
+      if (renderers.river) {
+        renderers.river->configure(height_map->getRiverSegments(),
+                                   height_map->getTileSize());
+      }
+      if (renderers.road) {
+        renderers.road->configure(terrain_service.road_segments(),
+                                  height_map->getTileSize());
+      }
+      if (renderers.riverbank) {
+        renderers.riverbank->configure(height_map->getRiverSegments(),
+                                       *height_map);
+      }
+      if (renderers.bridge) {
+        renderers.bridge->configure(height_map->getBridges(),
+                                    height_map->getTileSize());
+      }
+      if (renderers.biome) {
+        renderers.biome->configure(*height_map,
+                                   terrain_service.biome_settings());
+      }
+      if (renderers.stone) {
+        renderers.stone->configure(*height_map,
+                                   terrain_service.biome_settings());
+      }
+      if (renderers.plant) {
+        renderers.plant->configure(*height_map,
+                                   terrain_service.biome_settings());
+      }
+      if (renderers.pine) {
+        renderers.pine->configure(*height_map,
+                                  terrain_service.biome_settings());
+      }
+      if (renderers.olive) {
+        renderers.olive->configure(*height_map,
+                                   terrain_service.biome_settings());
+      }
+      if (renderers.firecamp) {
+        renderers.firecamp->configure(*height_map,
+                                      terrain_service.biome_settings());
+      }
+    }
+
+    Game::Systems::CommandService::initialize(grid_width, grid_height);
+
+    auto &visibility_service = Game::Map::VisibilityService::instance();
+    visibility_service.initialize(grid_width, grid_height, tile_size);
+    visibility_service.computeImmediate(*world, local_owner_id);
+
+    if (renderers.fog && visibility_service.is_initialized()) {
+      renderers.fog->update_mask(
+          visibility_service.getWidth(), visibility_service.getHeight(),
+          visibility_service.getTileSize(), visibility_service.snapshotCells());
+    }
+  } else {
+    Game::Map::MapDefinition fallback_def;
+    fallback_def.grid.width = fallback_grid_width;
+    fallback_def.grid.height = fallback_grid_height;
+    fallback_def.grid.tile_size = fallback_tile_size;
+
+    Game::Systems::CommandService::initialize(fallback_grid_width,
+                                              fallback_grid_height);
+
+    auto &visibility_service = Game::Map::VisibilityService::instance();
+    visibility_service.initialize(fallback_grid_width, fallback_grid_height,
+                                  fallback_tile_size);
+    visibility_service.computeImmediate(*world, local_owner_id);
+
+    if (renderers.fog && visibility_service.is_initialized()) {
+      renderers.fog->update_mask(
+          visibility_service.getWidth(), visibility_service.getHeight(),
+          visibility_service.getTileSize(), visibility_service.snapshotCells());
+    }
+  }
+}

+ 71 - 0
app/core/game_state_restorer.h

@@ -0,0 +1,71 @@
+#pragma once
+
+#include <QJsonObject>
+
+namespace Engine::Core {
+class World;
+}
+
+namespace Render::GL {
+class Renderer;
+class Camera;
+class GroundRenderer;
+class TerrainRenderer;
+class BiomeRenderer;
+class RiverRenderer;
+class RoadRenderer;
+class RiverbankRenderer;
+class BridgeRenderer;
+class FogRenderer;
+class StoneRenderer;
+class PlantRenderer;
+class PineRenderer;
+class OliveRenderer;
+class FireCampRenderer;
+} // namespace Render::GL
+
+namespace Game::Systems {
+struct LevelSnapshot;
+struct RuntimeSnapshot;
+} // namespace Game::Systems
+
+class EntityCache;
+
+struct ViewportState;
+
+class GameStateRestorer {
+public:
+  struct RendererRefs {
+    Render::GL::Renderer *renderer;
+    Render::GL::Camera *camera;
+    Render::GL::GroundRenderer *ground;
+    Render::GL::TerrainRenderer *terrain;
+    Render::GL::BiomeRenderer *biome;
+    Render::GL::RiverRenderer *river;
+    Render::GL::RoadRenderer *road;
+    Render::GL::RiverbankRenderer *riverbank;
+    Render::GL::BridgeRenderer *bridge;
+    Render::GL::FogRenderer *fog;
+    Render::GL::StoneRenderer *stone;
+    Render::GL::PlantRenderer *plant;
+    Render::GL::PineRenderer *pine;
+    Render::GL::OliveRenderer *olive;
+    Render::GL::FireCampRenderer *firecamp;
+  };
+
+  static void restore_environment_from_metadata(
+      const QJsonObject &metadata, Engine::Core::World *world,
+      const RendererRefs &renderers, Game::Systems::LevelSnapshot &level,
+      int local_owner_id, const ViewportState &viewport);
+
+  static void rebuild_registries_after_load(Engine::Core::World *world,
+                                            int &selected_player_id,
+                                            Game::Systems::LevelSnapshot &level,
+                                            int local_owner_id);
+
+  static void rebuild_building_collisions(Engine::Core::World *world);
+
+  static void rebuild_entity_cache(Engine::Core::World *world,
+                                   EntityCache &entity_cache,
+                                   int local_owner_id);
+};

+ 228 - 0
app/core/input_command_handler.cpp

@@ -0,0 +1,228 @@
+#include "input_command_handler.h"
+
+#include "../controllers/action_vfx.h"
+#include "../controllers/command_controller.h"
+#include "../models/cursor_manager.h"
+#include "../models/cursor_mode.h"
+#include "../models/hover_tracker.h"
+#include "../utils/movement_utils.h"
+#include "game/core/component.h"
+#include "game/core/world.h"
+#include "game/game_config.h"
+#include "game/systems/command_service.h"
+#include "game/systems/formation_planner.h"
+#include "game/systems/picking_service.h"
+#include "game/systems/selection_system.h"
+#include "render/gl/camera.h"
+
+InputCommandHandler::InputCommandHandler(
+    Engine::Core::World *world,
+    Game::Systems::SelectionController *selection_controller,
+    App::Controllers::CommandController *command_controller,
+    CursorManager *cursor_manager, HoverTracker *hover_tracker,
+    Game::Systems::PickingService *picking_service, Render::GL::Camera *camera)
+    : m_world(world), m_selection_controller(selection_controller),
+      m_command_controller(command_controller),
+      m_cursor_manager(cursor_manager), m_hover_tracker(hover_tracker),
+      m_picking_service(picking_service), m_camera(camera) {}
+
+void InputCommandHandler::on_map_clicked(qreal sx, qreal sy, int local_owner_id,
+                                         const ViewportState &viewport) {
+  if (m_selection_controller && m_camera) {
+    m_selection_controller->on_click_select(sx, sy, false, viewport.width,
+                                            viewport.height, m_camera,
+                                            local_owner_id);
+  }
+}
+
+void InputCommandHandler::on_right_click(qreal sx, qreal sy, int local_owner_id,
+                                         const ViewportState &viewport) {
+  if (!m_world) {
+    return;
+  }
+
+  auto *selection_system =
+      m_world->get_system<Game::Systems::SelectionSystem>();
+  if (selection_system == nullptr) {
+    return;
+  }
+
+  if (m_cursor_manager->mode() == CursorMode::Patrol ||
+      m_cursor_manager->mode() == CursorMode::Attack) {
+    m_cursor_manager->setMode(CursorMode::Normal);
+    return;
+  }
+
+  const auto &sel = selection_system->get_selected_units();
+  if (sel.empty()) {
+    return;
+  }
+
+  if (m_picking_service && m_camera) {
+    Engine::Core::EntityID const target_id = m_picking_service->pick_unit_first(
+        float(sx), float(sy), *m_world, *m_camera, viewport.width,
+        viewport.height, 0);
+
+    if (target_id != 0U) {
+      auto *target_entity = m_world->get_entity(target_id);
+      if (target_entity != nullptr) {
+        auto *target_unit =
+            target_entity->get_component<Engine::Core::UnitComponent>();
+        if (target_unit != nullptr) {
+          bool const is_enemy = (target_unit->owner_id != local_owner_id);
+
+          if (is_enemy) {
+            Game::Systems::CommandService::attack_target(*m_world, sel,
+                                                         target_id, true);
+            return;
+          }
+        }
+      }
+    }
+  }
+
+  if (m_picking_service && m_camera) {
+    QVector3D hit;
+    if (m_picking_service->screen_to_ground(
+            QPointF(sx, sy), *m_camera, viewport.width, viewport.height, hit)) {
+      auto targets = Game::Systems::FormationPlanner::spreadFormation(
+          int(sel.size()), hit,
+          Game::GameConfig::instance().gameplay().formation_spacing_default);
+      Game::Systems::CommandService::MoveOptions opts;
+      opts.group_move = sel.size() > 1;
+      Game::Systems::CommandService::moveUnits(*m_world, sel, targets, opts);
+    }
+  }
+}
+
+void InputCommandHandler::on_attack_click(qreal sx, qreal sy,
+                                          const ViewportState &viewport) {
+  if (!m_command_controller || !m_camera) {
+    return;
+  }
+
+  auto result = m_command_controller->onAttackClick(sx, sy, viewport.width,
+                                                    viewport.height, m_camera);
+
+  auto *selection_system =
+      m_world->get_system<Game::Systems::SelectionSystem>();
+  if ((selection_system == nullptr) || !m_picking_service || !m_camera ||
+      !m_world) {
+    return;
+  }
+
+  const auto &selected = selection_system->get_selected_units();
+  if (!selected.empty()) {
+    Engine::Core::EntityID const target_id = m_picking_service->pick_unit_first(
+        float(sx), float(sy), *m_world, *m_camera, viewport.width,
+        viewport.height, 0);
+
+    if (target_id != 0) {
+      auto *target_entity = m_world->get_entity(target_id);
+      if (target_entity != nullptr) {
+        auto *target_unit =
+            target_entity->get_component<Engine::Core::UnitComponent>();
+        if ((target_unit != nullptr)) {
+          App::Controllers::ActionVFX::spawnAttackArrow(m_world, target_id);
+        }
+      }
+    }
+  }
+
+  if (result.resetCursorToNormal) {
+    m_cursor_manager->setMode(CursorMode::Normal);
+  }
+}
+
+void InputCommandHandler::reset_movement(Engine::Core::Entity *entity) {
+  App::Utils::reset_movement(entity);
+}
+
+void InputCommandHandler::on_stop_command() {
+  if (!m_command_controller) {
+    return;
+  }
+
+  auto result = m_command_controller->onStopCommand();
+  if (result.resetCursorToNormal) {
+    m_cursor_manager->setMode(CursorMode::Normal);
+  }
+}
+
+void InputCommandHandler::on_hold_command() {
+  if (!m_command_controller) {
+    return;
+  }
+
+  auto result = m_command_controller->onHoldCommand();
+  if (result.resetCursorToNormal) {
+    m_cursor_manager->setMode(CursorMode::Normal);
+  }
+}
+
+auto InputCommandHandler::any_selected_in_hold_mode() const -> bool {
+  if (!m_command_controller) {
+    return false;
+  }
+  return m_command_controller->anySelectedInHoldMode();
+}
+
+void InputCommandHandler::on_patrol_click(qreal sx, qreal sy,
+                                          const ViewportState &viewport) {
+  if (!m_command_controller || !m_camera) {
+    return;
+  }
+
+  auto result = m_command_controller->onPatrolClick(sx, sy, viewport.width,
+                                                    viewport.height, m_camera);
+  if (result.resetCursorToNormal) {
+    m_cursor_manager->setMode(CursorMode::Normal);
+  }
+}
+
+void InputCommandHandler::on_click_select(qreal sx, qreal sy, bool additive,
+                                          int local_owner_id,
+                                          const ViewportState &viewport) {
+  if (m_selection_controller && m_camera) {
+    m_selection_controller->on_click_select(sx, sy, additive, viewport.width,
+                                            viewport.height, m_camera,
+                                            local_owner_id);
+  }
+}
+
+void InputCommandHandler::on_area_selected(qreal x1, qreal y1, qreal x2,
+                                           qreal y2, bool additive,
+                                           int local_owner_id,
+                                           const ViewportState &viewport) {
+  if (m_selection_controller && m_camera) {
+    m_selection_controller->on_area_selected(x1, y1, x2, y2, additive,
+                                             viewport.width, viewport.height,
+                                             m_camera, local_owner_id);
+  }
+}
+
+void InputCommandHandler::select_all_troops(int local_owner_id) {
+  if (m_selection_controller) {
+    m_selection_controller->select_all_player_troops(local_owner_id);
+  }
+}
+
+void InputCommandHandler::select_unit_by_id(int unit_id, int local_owner_id) {
+  if (!m_selection_controller || (unit_id <= 0)) {
+    return;
+  }
+  m_selection_controller->select_single_unit(
+      static_cast<Engine::Core::EntityID>(unit_id), local_owner_id);
+}
+
+void InputCommandHandler::set_hover_at_screen(qreal sx, qreal sy,
+                                              const ViewportState &viewport) {
+  if (!m_hover_tracker || !m_camera || !m_world) {
+    return;
+  }
+
+  m_cursor_manager->updateCursorShape(nullptr);
+
+  m_hover_tracker->update_hover(float(sx), float(sy), *m_world, *m_camera,
+                                viewport.width, viewport.height);
+}

+ 70 - 0
app/core/input_command_handler.h

@@ -0,0 +1,70 @@
+#pragma once
+
+#include <QPointF>
+#include <QString>
+
+namespace Engine::Core {
+class World;
+class Entity;
+using EntityID = unsigned int;
+} // namespace Engine::Core
+
+namespace Render::GL {
+class Camera;
+}
+
+namespace Game::Systems {
+class SelectionSystem;
+class SelectionController;
+class PickingService;
+} // namespace Game::Systems
+
+namespace App::Controllers {
+class CommandController;
+}
+
+class CursorManager;
+class HoverTracker;
+
+struct ViewportState {
+  int width = 0;
+  int height = 0;
+};
+
+class InputCommandHandler {
+public:
+  InputCommandHandler(Engine::Core::World *world,
+                      Game::Systems::SelectionController *selection_controller,
+                      App::Controllers::CommandController *command_controller,
+                      CursorManager *cursor_manager,
+                      HoverTracker *hover_tracker,
+                      Game::Systems::PickingService *picking_service,
+                      Render::GL::Camera *camera);
+
+  void on_map_clicked(qreal sx, qreal sy, int local_owner_id,
+                      const ViewportState &viewport);
+  void on_right_click(qreal sx, qreal sy, int local_owner_id,
+                      const ViewportState &viewport);
+  void on_attack_click(qreal sx, qreal sy, const ViewportState &viewport);
+  void on_stop_command();
+  void on_hold_command();
+  [[nodiscard]] bool any_selected_in_hold_mode() const;
+  void on_patrol_click(qreal sx, qreal sy, const ViewportState &viewport);
+  void on_click_select(qreal sx, qreal sy, bool additive, int local_owner_id,
+                       const ViewportState &viewport);
+  void on_area_selected(qreal x1, qreal y1, qreal x2, qreal y2, bool additive,
+                        int local_owner_id, const ViewportState &viewport);
+  void select_all_troops(int local_owner_id);
+  void select_unit_by_id(int unit_id, int local_owner_id);
+  void set_hover_at_screen(qreal sx, qreal sy, const ViewportState &viewport);
+  static void reset_movement(Engine::Core::Entity *entity);
+
+private:
+  Engine::Core::World *m_world;
+  Game::Systems::SelectionController *m_selection_controller;
+  App::Controllers::CommandController *m_command_controller;
+  CursorManager *m_cursor_manager;
+  HoverTracker *m_hover_tracker;
+  Game::Systems::PickingService *m_picking_service;
+  Render::GL::Camera *m_camera;
+};

+ 114 - 0
app/core/level_orchestrator.cpp

@@ -0,0 +1,114 @@
+#include "level_orchestrator.h"
+
+#include "game/core/world.h"
+#include "game/game_config.h"
+#include "game/map/map_loader.h"
+#include "game/map/skirmish_loader.h"
+#include "game/systems/ai_system.h"
+#include "game/systems/game_state_serializer.h"
+#include "game/systems/global_stats_registry.h"
+#include "game/systems/owner_registry.h"
+#include "game/systems/troop_count_registry.h"
+#include "game/systems/victory_service.h"
+#include "game_engine.h"
+#include "minimap_manager.h"
+#include "render/gl/camera.h"
+#include "render/scene_renderer.h"
+#include <QDebug>
+
+auto LevelOrchestrator::load_skirmish(
+    const QString &map_path, const QVariantList &player_configs,
+    int selected_player_id, Engine::Core::World &world,
+    const RendererRefs &renderers, Game::Systems::LevelSnapshot &level,
+    EntityCache &entity_cache, Game::Systems::VictoryService *victory_service,
+    MinimapManager *minimap_manager, VisibilityReadyCallback visibility_ready,
+    OwnerUpdateCallback owner_update) -> LevelLoadResult {
+
+  LevelLoadResult result;
+  result.updated_player_id = selected_player_id;
+
+  entity_cache.reset();
+
+  Game::Map::SkirmishLoader loader(world, *renderers.renderer,
+                                   *renderers.camera);
+  loader.set_ground_renderer(renderers.ground);
+  loader.set_terrain_renderer(renderers.terrain);
+  loader.set_biome_renderer(renderers.biome);
+  loader.set_river_renderer(renderers.river);
+  loader.set_road_renderer(renderers.road);
+  loader.set_riverbank_renderer(renderers.riverbank);
+  loader.set_bridge_renderer(renderers.bridge);
+  loader.set_fog_renderer(renderers.fog);
+  loader.set_stone_renderer(renderers.stone);
+  loader.set_plant_renderer(renderers.plant);
+  loader.set_pine_renderer(renderers.pine);
+  loader.set_olive_renderer(renderers.olive);
+  loader.set_fire_camp_renderer(renderers.firecamp);
+
+  loader.set_on_owners_updated(owner_update);
+  loader.set_on_visibility_mask_ready(visibility_ready);
+
+  auto load_result = loader.start(map_path, player_configs, selected_player_id,
+                                  result.updated_player_id);
+
+  if (!load_result.ok) {
+    result.success = false;
+    result.error_message = load_result.errorMessage;
+    return result;
+  }
+
+  level.map_name = load_result.map_name;
+  level.player_unit_id = load_result.player_unit_id;
+  level.cam_fov = load_result.cam_fov;
+  level.cam_near = load_result.cam_near;
+  level.cam_far = load_result.cam_far;
+  level.max_troops_per_player = load_result.max_troops_per_player;
+
+  Game::GameConfig::instance().set_max_troops_per_player(
+      load_result.max_troops_per_player);
+
+  if (victory_service) {
+    victory_service->configure(load_result.victoryConfig,
+                               result.updated_player_id);
+  }
+
+  if (load_result.has_focus_position && renderers.camera) {
+    const auto &cam_config = Game::GameConfig::instance().camera();
+    renderers.camera->set_rts_view(
+        load_result.focusPosition, cam_config.default_distance,
+        cam_config.default_pitch, cam_config.default_yaw);
+  }
+
+  Game::Map::MapDefinition map_def;
+  QString map_error;
+  if (Game::Map::MapLoader::loadFromJsonFile(map_path, map_def, &map_error)) {
+    if (minimap_manager) {
+      minimap_manager->generate_for_map(map_def);
+    }
+  } else {
+    qWarning() << "LevelOrchestrator: Failed to load map for minimap:"
+               << map_error;
+  }
+
+  if (auto *ai_system = world.get_system<Game::Systems::AISystem>()) {
+    ai_system->reinitialize();
+  }
+
+  auto &troops = Game::Systems::TroopCountRegistry::instance();
+  troops.rebuild_from_world(world);
+
+  auto &stats_registry = Game::Systems::GlobalStatsRegistry::instance();
+  stats_registry.rebuild_from_world(world);
+
+  auto &owner_registry = Game::Systems::OwnerRegistry::instance();
+  const auto &all_owners = owner_registry.get_all_owners();
+  for (const auto &owner : all_owners) {
+    if (owner.type == Game::Systems::OwnerType::Player ||
+        owner.type == Game::Systems::OwnerType::AI) {
+      stats_registry.mark_game_start(owner.owner_id);
+    }
+  }
+
+  result.success = true;
+  return result;
+}

+ 74 - 0
app/core/level_orchestrator.h

@@ -0,0 +1,74 @@
+#pragma once
+
+#include <QString>
+#include <QVariantList>
+#include <functional>
+#include <memory>
+
+namespace Engine::Core {
+class World;
+}
+
+namespace Render::GL {
+class Renderer;
+class Camera;
+class GroundRenderer;
+class TerrainRenderer;
+class BiomeRenderer;
+class RiverRenderer;
+class RoadRenderer;
+class RiverbankRenderer;
+class BridgeRenderer;
+class FogRenderer;
+class StoneRenderer;
+class PlantRenderer;
+class PineRenderer;
+class OliveRenderer;
+class FireCampRenderer;
+} // namespace Render::GL
+
+namespace Game::Systems {
+struct LevelSnapshot;
+class VictoryService;
+} // namespace Game::Systems
+
+class MinimapManager;
+class EntityCache;
+
+struct LevelLoadResult {
+  bool success = false;
+  QString error_message;
+  int updated_player_id = 1;
+};
+
+class LevelOrchestrator {
+public:
+  struct RendererRefs {
+    Render::GL::Renderer *renderer;
+    Render::GL::Camera *camera;
+    Render::GL::GroundRenderer *ground;
+    Render::GL::TerrainRenderer *terrain;
+    Render::GL::BiomeRenderer *biome;
+    Render::GL::RiverRenderer *river;
+    Render::GL::RoadRenderer *road;
+    Render::GL::RiverbankRenderer *riverbank;
+    Render::GL::BridgeRenderer *bridge;
+    Render::GL::FogRenderer *fog;
+    Render::GL::StoneRenderer *stone;
+    Render::GL::PlantRenderer *plant;
+    Render::GL::PineRenderer *pine;
+    Render::GL::OliveRenderer *olive;
+    Render::GL::FireCampRenderer *firecamp;
+  };
+
+  using VisibilityReadyCallback = std::function<void()>;
+  using OwnerUpdateCallback = std::function<void()>;
+
+  LevelLoadResult load_skirmish(
+      const QString &map_path, const QVariantList &player_configs,
+      int selected_player_id, Engine::Core::World &world,
+      const RendererRefs &renderers, Game::Systems::LevelSnapshot &level,
+      EntityCache &entity_cache, Game::Systems::VictoryService *victory_service,
+      MinimapManager *minimap_manager, VisibilityReadyCallback visibility_ready,
+      OwnerUpdateCallback owner_update);
+};

+ 227 - 0
app/core/minimap_manager.cpp

@@ -0,0 +1,227 @@
+#include "minimap_manager.h"
+
+#include "game/core/component.h"
+#include "game/core/world.h"
+#include "game/map/map_loader.h"
+#include "game/map/minimap/minimap_generator.h"
+#include "game/map/minimap/unit_layer.h"
+#include "game/map/visibility_service.h"
+#include "game/systems/selection_system.h"
+#include "game/units/troop_type.h"
+#include <QDebug>
+#include <QPainter>
+#include <algorithm>
+#include <unordered_set>
+
+MinimapManager::MinimapManager() = default;
+
+MinimapManager::~MinimapManager() = default;
+
+void MinimapManager::generate_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() << "MinimapManager: 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() << "MinimapManager: 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_fog(0.0F, 1);
+  } else {
+    qWarning() << "MinimapManager: Failed to generate minimap";
+  }
+}
+
+void MinimapManager::update_fog(float dt, int local_owner_id) {
+  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.is_initialized()) {
+    if (m_minimap_image != m_minimap_base_image) {
+      m_minimap_image = m_minimap_base_image;
+    }
+    return;
+  }
+
+  const auto current_version = visibility_service.version();
+  if (current_version == m_minimap_fog_version && !m_minimap_image.isNull()) {
+    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;
+    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);
+      }
+    }
+  }
+}
+
+void MinimapManager::update_units(
+    Engine::Core::World *world,
+    Game::Systems::SelectionSystem *selection_system) {
+  if (m_minimap_image.isNull() || !m_unit_layer || !world) {
+    return;
+  }
+
+  std::vector<Game::Map::Minimap::UnitMarker> markers;
+
+  constexpr size_t EXPECTED_MAX_UNITS = 128;
+  markers.reserve(EXPECTED_MAX_UNITS);
+
+  std::unordered_set<Engine::Core::EntityID> selected_ids;
+  if (selection_system) {
+    const auto &sel = selection_system->get_selected_units();
+    selected_ids.insert(sel.begin(), sel.end());
+  }
+
+  {
+    const std::lock_guard<std::recursive_mutex> lock(world->get_entity_mutex());
+    const auto &entities = 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::is_building_spawn(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);
+  }
+}

+ 47 - 0
app/core/minimap_manager.h

@@ -0,0 +1,47 @@
+#pragma once
+
+#include <QImage>
+#include <memory>
+#include <vector>
+
+namespace Game::Map {
+struct MapDefinition;
+namespace Minimap {
+class UnitLayer;
+}
+} // namespace Game::Map
+
+namespace Engine::Core {
+class World;
+using EntityID = unsigned int;
+} // namespace Engine::Core
+
+namespace Game::Systems {
+class SelectionSystem;
+}
+
+class MinimapManager {
+public:
+  MinimapManager();
+  ~MinimapManager();
+
+  void generate_for_map(const Game::Map::MapDefinition &map_def);
+  void update_fog(float dt, int local_owner_id);
+  void update_units(Engine::Core::World *world,
+                    Game::Systems::SelectionSystem *selection_system);
+
+  [[nodiscard]] const QImage &get_image() const { return m_minimap_image; }
+  [[nodiscard]] bool has_minimap() const {
+    return !m_minimap_base_image.isNull();
+  }
+
+private:
+  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;
+};

+ 81 - 0
app/core/renderer_bootstrap.cpp

@@ -0,0 +1,81 @@
+#include "renderer_bootstrap.h"
+
+#include "game/core/world.h"
+#include "game/systems/ai_system.h"
+#include "game/systems/arrow_system.h"
+#include "game/systems/ballista_attack_system.h"
+#include "game/systems/capture_system.h"
+#include "game/systems/catapult_attack_system.h"
+#include "game/systems/cleanup_system.h"
+#include "game/systems/combat_system.h"
+#include "game/systems/healing_beam_system.h"
+#include "game/systems/healing_system.h"
+#include "game/systems/movement_system.h"
+#include "game/systems/patrol_system.h"
+#include "game/systems/production_system.h"
+#include "game/systems/projectile_system.h"
+#include "game/systems/selection_system.h"
+#include "game/systems/terrain_alignment_system.h"
+#include "render/gl/camera.h"
+#include "render/ground/biome_renderer.h"
+#include "render/ground/bridge_renderer.h"
+#include "render/ground/firecamp_renderer.h"
+#include "render/ground/fog_renderer.h"
+#include "render/ground/ground_renderer.h"
+#include "render/ground/olive_renderer.h"
+#include "render/ground/pine_renderer.h"
+#include "render/ground/plant_renderer.h"
+#include "render/ground/river_renderer.h"
+#include "render/ground/riverbank_renderer.h"
+#include "render/ground/road_renderer.h"
+#include "render/ground/stone_renderer.h"
+#include "render/ground/terrain_renderer.h"
+#include "render/scene_renderer.h"
+
+auto RendererBootstrap::initialize_rendering() -> RenderingComponents {
+  RenderingComponents components;
+
+  components.renderer = std::make_unique<Render::GL::Renderer>();
+  components.camera = std::make_unique<Render::GL::Camera>();
+  components.ground = std::make_unique<Render::GL::GroundRenderer>();
+  components.terrain = std::make_unique<Render::GL::TerrainRenderer>();
+  components.biome = std::make_unique<Render::GL::BiomeRenderer>();
+  components.river = std::make_unique<Render::GL::RiverRenderer>();
+  components.road = std::make_unique<Render::GL::RoadRenderer>();
+  components.riverbank = std::make_unique<Render::GL::RiverbankRenderer>();
+  components.bridge = std::make_unique<Render::GL::BridgeRenderer>();
+  components.fog = std::make_unique<Render::GL::FogRenderer>();
+  components.stone = std::make_unique<Render::GL::StoneRenderer>();
+  components.plant = std::make_unique<Render::GL::PlantRenderer>();
+  components.pine = std::make_unique<Render::GL::PineRenderer>();
+  components.olive = std::make_unique<Render::GL::OliveRenderer>();
+  components.firecamp = std::make_unique<Render::GL::FireCampRenderer>();
+
+  components.passes = {components.ground.get(),    components.terrain.get(),
+                       components.river.get(),     components.road.get(),
+                       components.riverbank.get(), components.bridge.get(),
+                       components.biome.get(),     components.stone.get(),
+                       components.plant.get(),     components.pine.get(),
+                       components.olive.get(),     components.firecamp.get(),
+                       components.fog.get()};
+
+  return components;
+}
+
+void RendererBootstrap::initialize_world_systems(Engine::Core::World &world) {
+  world.add_system(std::make_unique<Game::Systems::ArrowSystem>());
+  world.add_system(std::make_unique<Game::Systems::ProjectileSystem>());
+  world.add_system(std::make_unique<Game::Systems::MovementSystem>());
+  world.add_system(std::make_unique<Game::Systems::PatrolSystem>());
+  world.add_system(std::make_unique<Game::Systems::CombatSystem>());
+  world.add_system(std::make_unique<Game::Systems::CatapultAttackSystem>());
+  world.add_system(std::make_unique<Game::Systems::BallistaAttackSystem>());
+  world.add_system(std::make_unique<Game::Systems::HealingBeamSystem>());
+  world.add_system(std::make_unique<Game::Systems::HealingSystem>());
+  world.add_system(std::make_unique<Game::Systems::CaptureSystem>());
+  world.add_system(std::make_unique<Game::Systems::AISystem>());
+  world.add_system(std::make_unique<Game::Systems::ProductionSystem>());
+  world.add_system(std::make_unique<Game::Systems::TerrainAlignmentSystem>());
+  world.add_system(std::make_unique<Game::Systems::CleanupSystem>());
+  world.add_system(std::make_unique<Game::Systems::SelectionSystem>());
+}

+ 52 - 0
app/core/renderer_bootstrap.h

@@ -0,0 +1,52 @@
+#pragma once
+
+#include <memory>
+#include <vector>
+
+namespace Render::GL {
+class Renderer;
+class Camera;
+class GroundRenderer;
+class TerrainRenderer;
+class BiomeRenderer;
+class RiverRenderer;
+class RoadRenderer;
+class RiverbankRenderer;
+class BridgeRenderer;
+class FogRenderer;
+class StoneRenderer;
+class PlantRenderer;
+class PineRenderer;
+class OliveRenderer;
+class FireCampRenderer;
+struct IRenderPass;
+} // namespace Render::GL
+
+namespace Engine::Core {
+class World;
+}
+
+class RendererBootstrap {
+public:
+  struct RenderingComponents {
+    std::unique_ptr<Render::GL::Renderer> renderer;
+    std::unique_ptr<Render::GL::Camera> camera;
+    std::unique_ptr<Render::GL::GroundRenderer> ground;
+    std::unique_ptr<Render::GL::TerrainRenderer> terrain;
+    std::unique_ptr<Render::GL::BiomeRenderer> biome;
+    std::unique_ptr<Render::GL::RiverRenderer> river;
+    std::unique_ptr<Render::GL::RoadRenderer> road;
+    std::unique_ptr<Render::GL::RiverbankRenderer> riverbank;
+    std::unique_ptr<Render::GL::BridgeRenderer> bridge;
+    std::unique_ptr<Render::GL::FogRenderer> fog;
+    std::unique_ptr<Render::GL::StoneRenderer> stone;
+    std::unique_ptr<Render::GL::PlantRenderer> plant;
+    std::unique_ptr<Render::GL::PineRenderer> pine;
+    std::unique_ptr<Render::GL::OliveRenderer> olive;
+    std::unique_ptr<Render::GL::FireCampRenderer> firecamp;
+    std::vector<Render::GL::IRenderPass *> passes;
+  };
+
+  static RenderingComponents initialize_rendering();
+  static void initialize_world_systems(Engine::Core::World &world);
+};

+ 48 - 0
app/models/map_preview_image_provider.cpp

@@ -0,0 +1,48 @@
+#include "map_preview_image_provider.h"
+
+#include <QMutexLocker>
+
+MapPreviewImageProvider::MapPreviewImageProvider()
+    : QQuickImageProvider(QQuickImageProvider::Image) {}
+
+QImage MapPreviewImageProvider::requestImage(const QString &id, QSize *size,
+                                             const QSize &requested_size) {
+  QImage image_copy;
+  {
+    QMutexLocker locker(&m_mutex);
+    if (m_preview_images.contains(id)) {
+      image_copy = m_preview_images[id];
+    }
+  }
+
+  if (image_copy.isNull()) {
+    QImage placeholder(200, 200, QImage::Format_RGBA8888);
+    placeholder.fill(QColor(40, 40, 40));
+    if (size) {
+      *size = placeholder.size();
+    }
+    return placeholder;
+  }
+
+  if (size) {
+    *size = image_copy.size();
+  }
+
+  if (requested_size.isValid() && !requested_size.isEmpty()) {
+    return image_copy.scaled(requested_size, Qt::KeepAspectRatio,
+                             Qt::SmoothTransformation);
+  }
+
+  return image_copy;
+}
+
+void MapPreviewImageProvider::set_preview_image(const QString &map_id,
+                                                const QImage &image) {
+  QMutexLocker locker(&m_mutex);
+  m_preview_images[map_id] = image;
+}
+
+void MapPreviewImageProvider::clear_preview(const QString &map_id) {
+  QMutexLocker locker(&m_mutex);
+  m_preview_images.remove(map_id);
+}

+ 25 - 0
app/models/map_preview_image_provider.h

@@ -0,0 +1,25 @@
+#pragma once
+
+#include <QImage>
+#include <QMap>
+#include <QMutex>
+#include <QQuickImageProvider>
+#include <QString>
+
+class MapPreviewImageProvider : public QQuickImageProvider {
+  Q_OBJECT
+
+public:
+  MapPreviewImageProvider();
+
+  QImage requestImage(const QString &id, QSize *size,
+                      const QSize &requested_size) override;
+
+  Q_INVOKABLE void set_preview_image(const QString &map_id,
+                                     const QImage &image);
+  Q_INVOKABLE void clear_preview(const QString &map_id);
+
+private:
+  QMap<QString, QImage> m_preview_images;
+  QMutex m_mutex;
+};

+ 13 - 5
app/models/minimap_image_provider.cpp

@@ -1,5 +1,6 @@
 #include "minimap_image_provider.h"
 
+#include <QMutexLocker>
 MinimapImageProvider::MinimapImageProvider()
     : QQuickImageProvider(QQuickImageProvider::Image) {}
 
@@ -7,7 +8,13 @@ QImage MinimapImageProvider::requestImage(const QString &id, QSize *size,
                                           const QSize &requested_size) {
   Q_UNUSED(id);
 
-  if (m_minimap_image.isNull()) {
+  QImage image_copy;
+  {
+    QMutexLocker locker(&m_mutex);
+    image_copy = m_minimap_image;
+  }
+
+  if (image_copy.isNull()) {
 
     QImage placeholder(64, 64, QImage::Format_RGBA8888);
     placeholder.fill(QColor(15, 26, 34));
@@ -18,17 +25,18 @@ QImage MinimapImageProvider::requestImage(const QString &id, QSize *size,
   }
 
   if (size) {
-    *size = m_minimap_image.size();
+    *size = image_copy.size();
   }
 
   if (requested_size.isValid() && !requested_size.isEmpty()) {
-    return m_minimap_image.scaled(requested_size, Qt::KeepAspectRatio,
-                                  Qt::SmoothTransformation);
+    return image_copy.scaled(requested_size, Qt::KeepAspectRatio,
+                             Qt::SmoothTransformation);
   }
 
-  return m_minimap_image;
+  return image_copy;
 }
 
 void MinimapImageProvider::set_minimap_image(const QImage &image) {
+  QMutexLocker locker(&m_mutex);
   m_minimap_image = image;
 }

+ 2 - 0
app/models/minimap_image_provider.h

@@ -1,6 +1,7 @@
 #pragma once
 
 #include <QImage>
+#include <QMutex>
 #include <QQuickImageProvider>
 
 class MinimapImageProvider : public QQuickImageProvider {
@@ -14,4 +15,5 @@ public:
 
 private:
   QImage m_minimap_image;
+  QMutex m_mutex;
 };

+ 1 - 0
game/CMakeLists.txt

@@ -72,6 +72,7 @@ add_library(game_systems STATIC
     map/map_catalog.cpp
     map/skirmish_loader.cpp
     map/minimap/minimap_generator.cpp
+    map/minimap/map_preview_generator.cpp
     map/minimap/minimap_texture_manager.cpp
     map/minimap/fog_of_war_mask.cpp
     map/minimap/minimap_manager.cpp

+ 118 - 24
game/core/serialization.cpp

@@ -35,7 +35,7 @@ namespace Engine::Core {
 
 namespace {
 
-auto combatModeToString(AttackComponent::CombatMode mode) -> QString {
+auto combat_mode_to_string(AttackComponent::CombatMode mode) -> QString {
   switch (mode) {
   case AttackComponent::CombatMode::Melee:
     return "melee";
@@ -47,7 +47,8 @@ auto combatModeToString(AttackComponent::CombatMode mode) -> QString {
   }
 }
 
-auto combatModeFromString(const QString &value) -> AttackComponent::CombatMode {
+auto combat_mode_from_string(const QString &value)
+    -> AttackComponent::CombatMode {
   if (value == "melee") {
     return AttackComponent::CombatMode::Melee;
   }
@@ -57,7 +58,7 @@ auto combatModeFromString(const QString &value) -> AttackComponent::CombatMode {
   return AttackComponent::CombatMode::Auto;
 }
 
-auto serializeColor(const std::array<float, 3> &color) -> QJsonArray {
+auto serialize_color(const std::array<float, 3> &color) -> QJsonArray {
   QJsonArray array;
   array.append(color[0]);
   array.append(color[1]);
@@ -65,7 +66,7 @@ auto serializeColor(const std::array<float, 3> &color) -> QJsonArray {
   return array;
 }
 
-void deserializeColor(const QJsonArray &array, std::array<float, 3> &color) {
+void deserialize_color(const QJsonArray &array, std::array<float, 3> &color) {
   if (array.size() >= 3) {
     color[0] = static_cast<float>(array.at(0).toDouble());
     color[1] = static_cast<float>(array.at(1).toDouble());
@@ -75,7 +76,7 @@ void deserializeColor(const QJsonArray &array, std::array<float, 3> &color) {
 
 } // namespace
 
-auto Serialization::serializeEntity(const Entity *entity) -> QJsonObject {
+auto Serialization::serialize_entity(const Entity *entity) -> QJsonObject {
   QJsonObject entity_obj;
   entity_obj["id"] = static_cast<qint64>(entity->get_id());
 
@@ -106,7 +107,7 @@ auto Serialization::serializeEntity(const Entity *entity) -> QJsonObject {
     }
     renderable_obj["visible"] = renderable->visible;
     renderable_obj["mesh"] = static_cast<int>(renderable->mesh);
-    renderable_obj["color"] = serializeColor(renderable->color);
+    renderable_obj["color"] = serialize_color(renderable->color);
     entity_obj["renderable"] = renderable_obj;
   }
 
@@ -161,8 +162,9 @@ auto Serialization::serializeEntity(const Entity *entity) -> QJsonObject {
     attack_obj["melee_range"] = attack->melee_range;
     attack_obj["melee_damage"] = attack->melee_damage;
     attack_obj["melee_cooldown"] = attack->melee_cooldown;
-    attack_obj["preferred_mode"] = combatModeToString(attack->preferred_mode);
-    attack_obj["current_mode"] = combatModeToString(attack->current_mode);
+    attack_obj["preferred_mode"] =
+        combat_mode_to_string(attack->preferred_mode);
+    attack_obj["current_mode"] = combat_mode_to_string(attack->current_mode);
     attack_obj["can_melee"] = attack->can_melee;
     attack_obj["can_ranged"] = attack->can_ranged;
     attack_obj["max_height_difference"] = attack->max_height_difference;
@@ -238,10 +240,53 @@ auto Serialization::serializeEntity(const Entity *entity) -> QJsonObject {
     entity_obj["capture"] = capture_obj;
   }
 
+  if (const auto *hold_mode = entity->get_component<HoldModeComponent>()) {
+    QJsonObject hold_mode_obj;
+    hold_mode_obj["active"] = hold_mode->active;
+    hold_mode_obj["exit_cooldown"] =
+        static_cast<double>(hold_mode->exit_cooldown);
+    hold_mode_obj["stand_up_duration"] =
+        static_cast<double>(hold_mode->stand_up_duration);
+    entity_obj["hold_mode"] = hold_mode_obj;
+  }
+
+  if (const auto *healer = entity->get_component<HealerComponent>()) {
+    QJsonObject healer_obj;
+    healer_obj["healing_range"] = static_cast<double>(healer->healing_range);
+    healer_obj["healing_amount"] = healer->healing_amount;
+    healer_obj["healing_cooldown"] =
+        static_cast<double>(healer->healing_cooldown);
+    healer_obj["time_since_last_heal"] =
+        static_cast<double>(healer->time_since_last_heal);
+    entity_obj["healer"] = healer_obj;
+  }
+
+  if (const auto *catapult =
+          entity->get_component<CatapultLoadingComponent>()) {
+    QJsonObject catapult_obj;
+    catapult_obj["state"] = static_cast<int>(catapult->state);
+    catapult_obj["loading_time"] = static_cast<double>(catapult->loading_time);
+    catapult_obj["loading_duration"] =
+        static_cast<double>(catapult->loading_duration);
+    catapult_obj["firing_time"] = static_cast<double>(catapult->firing_time);
+    catapult_obj["firing_duration"] =
+        static_cast<double>(catapult->firing_duration);
+    catapult_obj["target_id"] = static_cast<qint64>(catapult->target_id);
+    catapult_obj["target_locked_x"] =
+        static_cast<double>(catapult->target_locked_x);
+    catapult_obj["target_locked_y"] =
+        static_cast<double>(catapult->target_locked_y);
+    catapult_obj["target_locked_z"] =
+        static_cast<double>(catapult->target_locked_z);
+    catapult_obj["target_position_locked"] = catapult->target_position_locked;
+    entity_obj["catapult_loading"] = catapult_obj;
+  }
+
   return entity_obj;
 }
 
-void Serialization::deserializeEntity(Entity *entity, const QJsonObject &json) {
+void Serialization::deserialize_entity(Entity *entity,
+                                       const QJsonObject &json) {
   if (json.contains("transform")) {
     const auto transform_obj = json["transform"].toObject();
     auto *transform = entity->add_component<TransformComponent>();
@@ -282,7 +327,7 @@ void Serialization::deserializeEntity(Entity *entity, const QJsonObject &json) {
         static_cast<RenderableComponent::MeshKind>(renderable_obj["mesh"].toInt(
             static_cast<int>(RenderableComponent::MeshKind::Cube)));
     if (renderable_obj.contains("color")) {
-      deserializeColor(renderable_obj["color"].toArray(), renderable->color);
+      deserialize_color(renderable_obj["color"].toArray(), renderable->color);
     }
   }
 
@@ -369,9 +414,9 @@ void Serialization::deserializeEntity(Entity *entity, const QJsonObject &json) {
     attack->melee_cooldown =
         static_cast<float>(attack_obj["melee_cooldown"].toDouble());
     attack->preferred_mode =
-        combatModeFromString(attack_obj["preferred_mode"].toString());
+        combat_mode_from_string(attack_obj["preferred_mode"].toString());
     attack->current_mode =
-        combatModeFromString(attack_obj["current_mode"].toString());
+        combat_mode_from_string(attack_obj["current_mode"].toString());
     attack->can_melee = attack_obj["can_melee"].toBool(true);
     attack->can_ranged = attack_obj["can_ranged"].toBool(false);
     attack->max_height_difference =
@@ -456,9 +501,58 @@ void Serialization::deserializeEntity(Entity *entity, const QJsonObject &json) {
             static_cast<double>(Defaults::kCaptureRequiredTime)));
     capture->is_being_captured = capture_obj["is_being_captured"].toBool(false);
   }
+
+  if (json.contains("hold_mode")) {
+    const auto hold_mode_obj = json["hold_mode"].toObject();
+    auto *hold_mode = entity->add_component<HoldModeComponent>();
+    hold_mode->active = hold_mode_obj["active"].toBool(true);
+    hold_mode->exit_cooldown =
+        static_cast<float>(hold_mode_obj["exit_cooldown"].toDouble(0.0));
+    hold_mode->stand_up_duration =
+        static_cast<float>(hold_mode_obj["stand_up_duration"].toDouble(
+            static_cast<double>(Defaults::kHoldStandUpDuration)));
+  }
+
+  if (json.contains("healer")) {
+    const auto healer_obj = json["healer"].toObject();
+    auto *healer = entity->add_component<HealerComponent>();
+    healer->healing_range =
+        static_cast<float>(healer_obj["healing_range"].toDouble(8.0));
+    healer->healing_amount = healer_obj["healing_amount"].toInt(5);
+    healer->healing_cooldown =
+        static_cast<float>(healer_obj["healing_cooldown"].toDouble(2.0));
+    healer->time_since_last_heal =
+        static_cast<float>(healer_obj["time_since_last_heal"].toDouble(0.0));
+  }
+
+  if (json.contains("catapult_loading")) {
+    const auto catapult_obj = json["catapult_loading"].toObject();
+    auto *catapult = entity->add_component<CatapultLoadingComponent>();
+    catapult->state = static_cast<CatapultLoadingComponent::LoadingState>(
+        catapult_obj["state"].toInt(
+            static_cast<int>(CatapultLoadingComponent::LoadingState::Idle)));
+    catapult->loading_time =
+        static_cast<float>(catapult_obj["loading_time"].toDouble(0.0));
+    catapult->loading_duration =
+        static_cast<float>(catapult_obj["loading_duration"].toDouble(2.0));
+    catapult->firing_time =
+        static_cast<float>(catapult_obj["firing_time"].toDouble(0.0));
+    catapult->firing_duration =
+        static_cast<float>(catapult_obj["firing_duration"].toDouble(0.5));
+    catapult->target_id = static_cast<EntityID>(
+        catapult_obj["target_id"].toVariant().toULongLong());
+    catapult->target_locked_x =
+        static_cast<float>(catapult_obj["target_locked_x"].toDouble(0.0));
+    catapult->target_locked_y =
+        static_cast<float>(catapult_obj["target_locked_y"].toDouble(0.0));
+    catapult->target_locked_z =
+        static_cast<float>(catapult_obj["target_locked_z"].toDouble(0.0));
+    catapult->target_position_locked =
+        catapult_obj["target_position_locked"].toBool(false);
+  }
 }
 
-auto Serialization::serializeTerrain(
+auto Serialization::serialize_terrain(
     const Game::Map::TerrainHeightMap *height_map,
     const Game::Map::BiomeSettings &biome,
     const std::vector<Game::Map::RoadSegment> &roads) -> QJsonObject {
@@ -580,7 +674,7 @@ auto Serialization::serializeTerrain(
   return terrain_obj;
 }
 
-void Serialization::deserializeTerrain(
+void Serialization::deserialize_terrain(
     Game::Map::TerrainHeightMap *height_map, Game::Map::BiomeSettings &biome,
     std::vector<Game::Map::RoadSegment> &roads, const QJsonObject &json) {
   if ((height_map == nullptr) || json.isEmpty()) {
@@ -775,13 +869,13 @@ void Serialization::deserializeTerrain(
   height_map->restoreFromData(heights, terrain_types, rivers, bridges);
 }
 
-auto Serialization::serializeWorld(const World *world) -> QJsonDocument {
+auto Serialization::serialize_world(const World *world) -> QJsonDocument {
   QJsonObject world_obj;
   QJsonArray entities_array;
 
   const auto &entities = world->get_entities();
   for (const auto &[id, entity] : entities) {
-    QJsonObject const entity_obj = serializeEntity(entity.get());
+    QJsonObject const entity_obj = serialize_entity(entity.get());
     entities_array.append(entity_obj);
   }
 
@@ -794,15 +888,15 @@ auto Serialization::serializeWorld(const World *world) -> QJsonDocument {
   const auto &terrain_service = Game::Map::TerrainService::instance();
   if (terrain_service.is_initialized() &&
       (terrain_service.get_height_map() != nullptr)) {
-    world_obj["terrain"] = serializeTerrain(terrain_service.get_height_map(),
-                                            terrain_service.biome_settings(),
-                                            terrain_service.road_segments());
+    world_obj["terrain"] = serialize_terrain(terrain_service.get_height_map(),
+                                             terrain_service.biome_settings(),
+                                             terrain_service.road_segments());
   }
 
   return QJsonDocument(world_obj);
 }
 
-void Serialization::deserializeWorld(World *world, const QJsonDocument &doc) {
+void Serialization::deserialize_world(World *world, const QJsonDocument &doc) {
   auto world_obj = doc.object();
   auto entities_array = world_obj["entities"].toArray();
   for (const auto &value : entities_array) {
@@ -813,7 +907,7 @@ void Serialization::deserializeWorld(World *world, const QJsonDocument &doc) {
                        ? world->create_entity()
                        : world->create_entity_with_id(entity_id);
     if (entity != nullptr) {
-      deserializeEntity(entity, entity_obj);
+      deserialize_entity(entity, entity_obj);
     }
   }
 
@@ -844,7 +938,7 @@ void Serialization::deserializeWorld(World *world, const QJsonDocument &doc) {
 
     auto temp_height_map =
         std::make_unique<Game::Map::TerrainHeightMap>(width, height, tile_size);
-    deserializeTerrain(temp_height_map.get(), biome, roads, terrain_obj);
+    deserialize_terrain(temp_height_map.get(), biome, roads, terrain_obj);
 
     auto &terrain_service = Game::Map::TerrainService::instance();
     terrain_service.restore_from_serialized(
@@ -854,8 +948,8 @@ void Serialization::deserializeWorld(World *world, const QJsonDocument &doc) {
   }
 }
 
-auto Serialization::saveToFile(const QString &filename,
-                               const QJsonDocument &doc) -> bool {
+auto Serialization::save_to_file(const QString &filename,
+                                 const QJsonDocument &doc) -> bool {
   QFile file(filename);
   if (!file.open(QIODevice::WriteOnly)) {
     qWarning() << "Could not open file for writing:" << filename;

+ 17 - 17
game/core/serialization.h

@@ -15,23 +15,23 @@ namespace Engine::Core {
 
 class Serialization {
 public:
-  static auto serializeEntity(const class Entity *entity) -> QJsonObject;
-  static void deserializeEntity(class Entity *entity, const QJsonObject &json);
-
-  static auto serializeWorld(const class World *world) -> QJsonDocument;
-  static void deserializeWorld(class World *world, const QJsonDocument &doc);
-
-  static auto serializeTerrain(const Game::Map::TerrainHeightMap *height_map,
-                               const Game::Map::BiomeSettings &biome,
-                               const std::vector<Game::Map::RoadSegment> &roads)
-      -> QJsonObject;
-  static void deserializeTerrain(Game::Map::TerrainHeightMap *height_map,
-                                 Game::Map::BiomeSettings &biome,
-                                 std::vector<Game::Map::RoadSegment> &roads,
-                                 const QJsonObject &json);
-
-  static auto saveToFile(const QString &filename,
-                         const QJsonDocument &doc) -> bool;
+  static auto serialize_entity(const class Entity *entity) -> QJsonObject;
+  static void deserialize_entity(class Entity *entity, const QJsonObject &json);
+
+  static auto serialize_world(const class World *world) -> QJsonDocument;
+  static void deserialize_world(class World *world, const QJsonDocument &doc);
+
+  static auto serialize_terrain(
+      const Game::Map::TerrainHeightMap *height_map,
+      const Game::Map::BiomeSettings &biome,
+      const std::vector<Game::Map::RoadSegment> &roads) -> QJsonObject;
+  static void deserialize_terrain(Game::Map::TerrainHeightMap *height_map,
+                                  Game::Map::BiomeSettings &biome,
+                                  std::vector<Game::Map::RoadSegment> &roads,
+                                  const QJsonObject &json);
+
+  static auto save_to_file(const QString &filename,
+                           const QJsonDocument &doc) -> bool;
   static auto load_from_file(const QString &filename) -> QJsonDocument;
 };
 

+ 156 - 0
game/map/minimap/map_preview_generator.cpp

@@ -0,0 +1,156 @@
+#include "map_preview_generator.h"
+#include "../../units/spawn_type.h"
+#include "../map_loader.h"
+#include "minimap_generator.h"
+#include <QColor>
+#include <QFile>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QPainter>
+#include <QPen>
+#include <QVariantMap>
+#include <cmath>
+
+namespace Game::Map::Minimap {
+
+namespace {
+
+constexpr float k_camera_yaw_cos = -0.70710678118F;
+constexpr float k_camera_yaw_sin = -0.70710678118F;
+
+constexpr float BASE_SIZE = 16.0F;
+constexpr float INNER_SIZE_RATIO = 0.35F;
+constexpr float INNER_OFFSET_RATIO = 0.3F;
+
+} // namespace
+
+MapPreviewGenerator::MapPreviewGenerator()
+    : m_minimap_generator(std::make_unique<MinimapGenerator>()) {}
+
+MapPreviewGenerator::~MapPreviewGenerator() = default;
+
+auto MapPreviewGenerator::generate_preview(
+    const QString &map_path, const QVariantList &player_configs) -> QImage {
+
+  MapDefinition map_def;
+  QString error;
+  if (!MapLoader::loadFromJsonFile(map_path, map_def, &error)) {
+    QImage error_image(200, 200, QImage::Format_RGBA8888);
+    error_image.fill(QColor(40, 40, 40));
+    return error_image;
+  }
+
+  QImage preview = m_minimap_generator->generate(map_def);
+
+  std::vector<PlayerConfig> parsed_configs =
+      parse_player_configs(player_configs);
+  draw_player_bases(preview, map_def, parsed_configs);
+
+  return preview;
+}
+
+auto MapPreviewGenerator::parse_player_configs(
+    const QVariantList &configs) const -> std::vector<PlayerConfig> {
+  std::vector<PlayerConfig> result;
+
+  for (const QVariant &var : configs) {
+    if (!var.canConvert<QVariantMap>()) {
+      continue;
+    }
+
+    QVariantMap config = var.toMap();
+    PlayerConfig player_config;
+
+    if (config.contains("player_id")) {
+      player_config.player_id = config["player_id"].toInt();
+    }
+
+    if (config.contains("colorHex")) {
+      QString color_hex = config["colorHex"].toString();
+      player_config.color = QColor(color_hex);
+    }
+
+    if (player_config.player_id > 0 && player_config.color.isValid()) {
+      result.push_back(player_config);
+    }
+  }
+
+  return result;
+}
+
+void MapPreviewGenerator::draw_player_bases(
+    QImage &image, const MapDefinition &map_def,
+    const std::vector<PlayerConfig> &player_configs) {
+
+  if (player_configs.empty()) {
+    return;
+  }
+
+  QPainter painter(&image);
+  painter.setRenderHint(QPainter::Antialiasing, true);
+
+  constexpr float pixels_per_tile = 2.0F;
+
+  for (const auto &spawn : map_def.spawns) {
+    if (!Game::Units::is_building_spawn(spawn.type)) {
+      continue;
+    }
+
+    if (spawn.player_id <= 0) {
+      continue;
+    }
+
+    QColor player_color;
+    for (const auto &config : player_configs) {
+      if (config.player_id == spawn.player_id) {
+        player_color = config.color;
+        break;
+      }
+    }
+
+    if (!player_color.isValid()) {
+      continue;
+    }
+
+    const auto [px, py] =
+        world_to_pixel(spawn.x, spawn.z, map_def.grid, pixels_per_tile);
+
+    constexpr float HALF = BASE_SIZE * 0.5F;
+
+    QColor border_color = player_color.darker(150);
+
+    painter.setBrush(player_color);
+    painter.setPen(QPen(border_color, 2.5));
+    painter.drawEllipse(QPointF(px, py), HALF, HALF);
+
+    painter.setBrush(player_color.lighter(130));
+    painter.setPen(Qt::NoPen);
+    constexpr float INNER_SIZE = BASE_SIZE * INNER_SIZE_RATIO;
+    painter.drawEllipse(
+        QPointF(px - HALF * INNER_OFFSET_RATIO, py - HALF * INNER_OFFSET_RATIO),
+        INNER_SIZE * 0.5F, INNER_SIZE * 0.5F);
+  }
+}
+
+auto MapPreviewGenerator::world_to_pixel(
+    float world_x, float world_z, const GridDefinition &grid,
+    float pixels_per_tile) const -> std::pair<float, float> {
+
+  const float rotated_x =
+      world_x * k_camera_yaw_cos - world_z * k_camera_yaw_sin;
+  const float rotated_z =
+      world_x * k_camera_yaw_sin + world_z * k_camera_yaw_cos;
+
+  const float world_width = grid.width * grid.tile_size;
+  const float world_height = grid.height * grid.tile_size;
+  const float img_width = grid.width * pixels_per_tile;
+  const float img_height = grid.height * 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};
+}
+
+} // namespace Game::Map::Minimap

+ 40 - 0
game/map/minimap/map_preview_generator.h

@@ -0,0 +1,40 @@
+#pragma once
+
+#include "../map_definition.h"
+#include <QColor>
+#include <QImage>
+#include <QString>
+#include <QVariantList>
+#include <memory>
+
+namespace Game::Map::Minimap {
+
+class MinimapGenerator;
+
+class MapPreviewGenerator {
+public:
+  struct PlayerConfig {
+    int player_id = 0;
+    QColor color;
+  };
+
+  MapPreviewGenerator();
+  ~MapPreviewGenerator();
+
+  [[nodiscard]] auto
+  generate_preview(const QString &map_path,
+                   const QVariantList &player_configs) -> QImage;
+
+private:
+  std::unique_ptr<MinimapGenerator> m_minimap_generator;
+
+  void draw_player_bases(QImage &image, const MapDefinition &map_def,
+                         const std::vector<PlayerConfig> &player_configs);
+  [[nodiscard]] auto parse_player_configs(const QVariantList &configs) const
+      -> std::vector<PlayerConfig>;
+  [[nodiscard]] auto
+  world_to_pixel(float world_x, float world_z, const GridDefinition &grid,
+                 float pixels_per_tile) const -> std::pair<float, float>;
+};
+
+} // namespace Game::Map::Minimap

+ 17 - 13
game/map/minimap/minimap_generator.cpp

@@ -569,12 +569,14 @@ void MinimapGenerator::apply_vignette(QPainter &painter, int width,
 
 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));
+  const float min_dim = static_cast<float>(std::min(width, height));
+  const float margin = std::clamp(min_dim * 0.06F, 12.0F, 32.0F);
+  const float SIZE = std::clamp(min_dim * 0.08F, 14.0F, 42.0F);
+  const float cx = static_cast<float>(width) - margin;
+  const float cy = static_cast<float>(height) - margin;
+
+  const float stroke = std::max(1.2F, SIZE * 0.08F);
+  painter.setPen(QPen(Palette::INK_MEDIUM, stroke));
   painter.setBrush(Qt::NoBrush);
 
   QPainterPath north_arrow;
@@ -599,13 +601,15 @@ void MinimapGenerator::draw_compass_rose(QPainter &painter, int width,
                    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;
+  const float dot_radius = std::max(2.0F, SIZE * 0.2F);
+  painter.drawEllipse(QPointF(cx, cy), dot_radius, dot_radius);
+
+  painter.setPen(QPen(Palette::INK_DARK, stroke));
+  const float n_half_width = SIZE * 0.35F;
+  const float n_left = cx - n_half_width;
+  const float n_right = cx + n_half_width;
+  const float n_top = cy - SIZE - SIZE * 0.7F;
+  const float n_bottom = cy - SIZE - SIZE * 0.15F;
 
   QPainterPath n_path;
   n_path.moveTo(n_left, n_bottom);

+ 5 - 4
game/systems/camera_follow_system.cpp

@@ -26,14 +26,15 @@ void CameraFollowSystem::update(Engine::Core::World &world,
   }
   if (count > 0) {
     QVector3D const center = sum / float(count);
-    camera.setTarget(center);
+    // update_follow() smoothly lerps to the new center and sets the target internally.
+    // Calling setTarget() here would cause a snap before the lerp, creating flicker.
     camera.update_follow(center);
   }
 }
 
-void CameraFollowSystem::snapToSelection(Engine::Core::World &world,
-                                         SelectionSystem &selection,
-                                         Render::GL::Camera &camera) {
+void CameraFollowSystem::snap_to_selection(Engine::Core::World &world,
+                                           SelectionSystem &selection,
+                                           Render::GL::Camera &camera) {
   const auto &sel = selection.get_selected_units();
   if (sel.empty()) {
     return;

+ 3 - 3
game/systems/camera_follow_system.h

@@ -21,9 +21,9 @@ public:
   static void update(Engine::Core::World &world, SelectionSystem &selection,
                      Render::GL::Camera &camera);
 
-  static void snapToSelection(Engine::Core::World &world,
-                              SelectionSystem &selection,
-                              Render::GL::Camera &camera);
+  static void snap_to_selection(Engine::Core::World &world,
+                                SelectionSystem &selection,
+                                Render::GL::Camera &camera);
 };
 
 } // namespace Game::Systems

+ 1 - 1
game/systems/camera_service.cpp

@@ -68,7 +68,7 @@ void CameraService::follow_selection(Render::GL::Camera &camera,
 
   if (enable) {
     if (auto *selection_system = world.get_system<SelectionSystem>()) {
-      m_followSystem->snapToSelection(world, *selection_system, camera);
+      m_followSystem->snap_to_selection(world, *selection_system, camera);
     }
   } else {
     auto pos = camera.get_position();

+ 30 - 30
game/systems/save_load_service.cpp

@@ -28,7 +28,7 @@
 namespace Game::Systems {
 
 SaveLoadService::SaveLoadService() {
-  ensureSavesDirectoryExists();
+  ensure_saves_directory_exists();
   m_storage = std::make_unique<SaveStorage>(get_database_path());
   QString init_error;
   if (!m_storage->initialize(&init_error)) {
@@ -39,31 +39,31 @@ SaveLoadService::SaveLoadService() {
 
 SaveLoadService::~SaveLoadService() = default;
 
-auto SaveLoadService::getSavesDirectory() -> QString {
+auto SaveLoadService::get_saves_directory() -> QString {
   QString const saves_path =
       QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
   return saves_path + "/saves";
 }
 
 auto SaveLoadService::get_database_path() -> QString {
-  return getSavesDirectory() + QStringLiteral("/saves.sqlite");
+  return get_saves_directory() + QStringLiteral("/saves.sqlite");
 }
 
-void SaveLoadService::ensureSavesDirectoryExists() {
-  QString const saves_dir = getSavesDirectory();
+void SaveLoadService::ensure_saves_directory_exists() {
+  QString const saves_dir = get_saves_directory();
   QDir const dir;
   if (!dir.exists(saves_dir)) {
     dir.mkpath(saves_dir);
   }
 }
 
-auto SaveLoadService::saveGameToSlot(Engine::Core::World &world,
-                                     const QString &slotName,
-                                     const QString &title,
-                                     const QString &map_name,
-                                     const QJsonObject &metadata,
-                                     const QByteArray &screenshot) -> bool {
-  qInfo() << "Saving game to slot:" << slotName;
+auto SaveLoadService::save_game_to_slot(Engine::Core::World &world,
+                                        const QString &slot_name,
+                                        const QString &title,
+                                        const QString &map_name,
+                                        const QJsonObject &metadata,
+                                        const QByteArray &screenshot) -> bool {
+  qInfo() << "Saving game to slot:" << slot_name;
 
   try {
     if (!m_storage) {
@@ -73,11 +73,11 @@ auto SaveLoadService::saveGameToSlot(Engine::Core::World &world,
     }
 
     QJsonDocument const world_doc =
-        Engine::Core::Serialization::serializeWorld(&world);
+        Engine::Core::Serialization::serialize_world(&world);
     const QByteArray world_bytes = world_doc.toJson(QJsonDocument::Compact);
 
     QJsonObject combined_metadata = metadata;
-    combined_metadata["slotName"] = slotName;
+    combined_metadata["slotName"] = slot_name;
     combined_metadata["title"] = title;
     combined_metadata["timestamp"] =
         QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs);
@@ -88,15 +88,15 @@ auto SaveLoadService::saveGameToSlot(Engine::Core::World &world,
     combined_metadata["version"] = QStringLiteral("1.0");
 
     QString storage_error;
-    if (!m_storage->saveSlot(slotName, title, combined_metadata, world_bytes,
-                             screenshot, &storage_error)) {
+    if (!m_storage->save_slot(slot_name, title, combined_metadata, world_bytes,
+                              screenshot, &storage_error)) {
       m_last_error = storage_error;
       qWarning() << "SaveLoadService: failed to persist slot" << storage_error;
       return false;
     }
 
     m_last_metadata = combined_metadata;
-    m_lastTitle = title;
+    m_last_title = title;
     m_last_screenshot = screenshot;
     m_last_error.clear();
     return true;
@@ -109,8 +109,8 @@ auto SaveLoadService::saveGameToSlot(Engine::Core::World &world,
 }
 
 auto SaveLoadService::load_game_from_slot(Engine::Core::World &world,
-                                          const QString &slotName) -> bool {
-  qInfo() << "Loading game from slot:" << slotName;
+                                          const QString &slot_name) -> bool {
+  qInfo() << "Loading game from slot:" << slot_name;
 
   try {
     if (!m_storage) {
@@ -125,8 +125,8 @@ auto SaveLoadService::load_game_from_slot(Engine::Core::World &world,
     QString title;
 
     QString load_error;
-    if (!m_storage->loadSlot(slotName, world_bytes, metadata, screenshot, title,
-                             &load_error)) {
+    if (!m_storage->load_slot(slot_name, world_bytes, metadata, screenshot,
+                              title, &load_error)) {
       m_last_error = load_error;
       qWarning() << "SaveLoadService: failed to load slot" << load_error;
       return false;
@@ -137,16 +137,16 @@ auto SaveLoadService::load_game_from_slot(Engine::Core::World &world,
         QJsonDocument::fromJson(world_bytes, &parse_error);
     if (parse_error.error != QJsonParseError::NoError || doc.isNull()) {
       m_last_error = QStringLiteral("Corrupted save data for slot '%1': %2")
-                         .arg(slotName, parse_error.errorString());
+                         .arg(slot_name, parse_error.errorString());
       qWarning() << m_last_error;
       return false;
     }
 
     world.clear();
-    Engine::Core::Serialization::deserializeWorld(&world, doc);
+    Engine::Core::Serialization::deserialize_world(&world, doc);
 
     m_last_metadata = metadata;
-    m_lastTitle = title;
+    m_last_title = title;
     m_last_screenshot = screenshot;
     m_last_error.clear();
     return true;
@@ -164,7 +164,7 @@ auto SaveLoadService::get_save_slots() const -> QVariantList {
   }
 
   QString list_error;
-  QVariantList slot_list = m_storage->listSlots(&list_error);
+  QVariantList slot_list = m_storage->list_slots(&list_error);
   if (!list_error.isEmpty()) {
     m_last_error = list_error;
     qWarning() << "SaveLoadService: failed to enumerate slots" << list_error;
@@ -174,8 +174,8 @@ auto SaveLoadService::get_save_slots() const -> QVariantList {
   return slot_list;
 }
 
-auto SaveLoadService::deleteSaveSlot(const QString &slotName) -> bool {
-  qInfo() << "Deleting save slot:" << slotName;
+auto SaveLoadService::delete_save_slot(const QString &slot_name) -> bool {
+  qInfo() << "Deleting save slot:" << slot_name;
 
   if (!m_storage) {
     m_last_error = QStringLiteral("Save storage unavailable");
@@ -184,7 +184,7 @@ auto SaveLoadService::deleteSaveSlot(const QString &slotName) -> bool {
   }
 
   QString delete_error;
-  if (!m_storage->deleteSlot(slotName, &delete_error)) {
+  if (!m_storage->delete_slot(slot_name, &delete_error)) {
     m_last_error = delete_error;
     qWarning() << "SaveLoadService: failed to delete slot" << delete_error;
     return false;
@@ -226,9 +226,9 @@ auto SaveLoadService::mark_campaign_completed(const QString &campaign_id,
   return m_storage->mark_campaign_completed(campaign_id, out_error);
 }
 
-void SaveLoadService::openSettings() { qInfo() << "Open settings requested"; }
+void SaveLoadService::open_settings() { qInfo() << "Open settings requested"; }
 
-void SaveLoadService::exitGame() {
+void SaveLoadService::exit_game() {
   qInfo() << "Exit game requested";
 
   QCoreApplication::quit();

+ 16 - 16
game/systems/save_load_service.h

@@ -20,25 +20,25 @@ public:
   SaveLoadService();
   ~SaveLoadService();
 
-  auto saveGameToSlot(Engine::Core::World &world, const QString &slotName,
-                      const QString &title, const QString &map_name,
-                      const QJsonObject &metadata = {},
-                      const QByteArray &screenshot = QByteArray()) -> bool;
+  auto save_game_to_slot(Engine::Core::World &world, const QString &slot_name,
+                         const QString &title, const QString &map_name,
+                         const QJsonObject &metadata = {},
+                         const QByteArray &screenshot = QByteArray()) -> bool;
 
   auto load_game_from_slot(Engine::Core::World &world,
-                           const QString &slotName) -> bool;
+                           const QString &slot_name) -> bool;
 
   auto get_save_slots() const -> QVariantList;
 
-  auto deleteSaveSlot(const QString &slotName) -> bool;
+  auto delete_save_slot(const QString &slot_name) -> bool;
 
-  auto getLastError() const -> QString { return m_last_error; }
+  auto get_last_error() const -> QString { return m_last_error; }
 
-  void clearError() { m_last_error.clear(); }
+  void clear_error() { m_last_error.clear(); }
 
-  auto getLastMetadata() const -> QJsonObject { return m_last_metadata; }
-  auto getLastTitle() const -> QString { return m_lastTitle; }
-  auto getLastScreenshot() const -> QByteArray { return m_last_screenshot; }
+  auto get_last_metadata() const -> QJsonObject { return m_last_metadata; }
+  auto get_last_title() const -> QString { return m_last_title; }
+  auto get_last_screenshot() const -> QByteArray { return m_last_screenshot; }
 
   auto list_campaigns(QString *out_error = nullptr) const -> QVariantList;
   auto get_campaign_progress(const QString &campaign_id,
@@ -46,18 +46,18 @@ public:
   auto mark_campaign_completed(const QString &campaign_id,
                                QString *out_error = nullptr) -> bool;
 
-  static void openSettings();
+  static void open_settings();
 
-  static void exitGame();
+  static void exit_game();
 
 private:
-  static auto getSavesDirectory() -> QString;
+  static auto get_saves_directory() -> QString;
   static auto get_database_path() -> QString;
-  static void ensureSavesDirectoryExists();
+  static void ensure_saves_directory_exists();
 
   mutable QString m_last_error;
   QJsonObject m_last_metadata;
-  QString m_lastTitle;
+  QString m_last_title;
   QByteArray m_last_screenshot;
   std::unique_ptr<SaveStorage> m_storage;
 };

+ 48 - 47
game/systems/save_storage.cpp

@@ -25,12 +25,12 @@ namespace {
 constexpr const char *k_driver_name = "QSQLITE";
 constexpr int k_current_schema_version = 2;
 
-auto buildConnectionName(const SaveStorage *instance) -> QString {
+auto build_connection_name(const SaveStorage *instance) -> QString {
   return QStringLiteral("SaveStorage_%1")
       .arg(reinterpret_cast<quintptr>(instance), 0, 16);
 }
 
-auto lastErrorString(const QSqlError &error) -> QString {
+auto last_error_string(const QSqlError &error) -> QString {
   if (error.type() == QSqlError::NoError) {
     return {};
   }
@@ -45,7 +45,7 @@ public:
     if (!m_database.transaction()) {
       if (out_error != nullptr) {
         *out_error = QStringLiteral("Failed to begin transaction: %1")
-                         .arg(lastErrorString(m_database.lastError()));
+                         .arg(last_error_string(m_database.lastError()));
       }
       return false;
     }
@@ -61,7 +61,7 @@ public:
     if (!m_database.commit()) {
       if (out_error != nullptr) {
         *out_error = QStringLiteral("Failed to commit transaction: %1")
-                         .arg(lastErrorString(m_database.lastError()));
+                         .arg(last_error_string(m_database.lastError()));
       }
       rollback();
       return false;
@@ -88,7 +88,7 @@ private:
 
 SaveStorage::SaveStorage(QString database_path)
     : m_database_path(std::move(database_path)),
-      m_connection_name(buildConnectionName(this)) {}
+      m_connection_name(build_connection_name(this)) {}
 
 SaveStorage::~SaveStorage() {
   if (m_database.isValid()) {
@@ -108,18 +108,18 @@ auto SaveStorage::initialize(QString *out_error) -> bool {
   if (!open(out_error)) {
     return false;
   }
-  if (!ensureSchema(out_error)) {
+  if (!ensure_schema(out_error)) {
     return false;
   }
   m_initialized = true;
   return true;
 }
 
-auto SaveStorage::saveSlot(const QString &slotName, const QString &title,
-                           const QJsonObject &metadata,
-                           const QByteArray &worldState,
-                           const QByteArray &screenshot,
-                           QString *out_error) -> bool {
+auto SaveStorage::save_slot(const QString &slot_name, const QString &title,
+                            const QJsonObject &metadata,
+                            const QByteArray &world_state,
+                            const QByteArray &screenshot,
+                            QString *out_error) -> bool {
   if (!initialize(out_error)) {
     return false;
   }
@@ -147,7 +147,7 @@ auto SaveStorage::saveSlot(const QString &slotName, const QString &title,
   if (!query.prepare(insert_sql)) {
     if (out_error != nullptr) {
       *out_error = QStringLiteral("Failed to prepare save query: %1")
-                       .arg(lastErrorString(query.lastError()));
+                       .arg(last_error_string(query.lastError()));
     }
     return false;
   }
@@ -161,12 +161,12 @@ auto SaveStorage::saveSlot(const QString &slotName, const QString &title,
   const QByteArray metadata_bytes =
       QJsonDocument(metadata).toJson(QJsonDocument::Compact);
 
-  query.bindValue(QStringLiteral(":slot_name"), slotName);
+  query.bindValue(QStringLiteral(":slot_name"), slot_name);
   query.bindValue(QStringLiteral(":title"), title);
   query.bindValue(QStringLiteral(":map_name"), map_name);
   query.bindValue(QStringLiteral(":timestamp"), now_iso);
   query.bindValue(QStringLiteral(":metadata"), metadata_bytes);
-  query.bindValue(QStringLiteral(":world_state"), worldState);
+  query.bindValue(QStringLiteral(":world_state"), world_state);
   if (screenshot.isEmpty()) {
     query.bindValue(QStringLiteral(":screenshot"),
                     QVariant(QMetaType::fromType<QByteArray>()));
@@ -179,7 +179,7 @@ auto SaveStorage::saveSlot(const QString &slotName, const QString &title,
   if (!query.exec()) {
     if (out_error != nullptr) {
       *out_error = QStringLiteral("Failed to persist save slot: %1")
-                       .arg(lastErrorString(query.lastError()));
+                       .arg(last_error_string(query.lastError()));
     }
     transaction.rollback();
     return false;
@@ -192,9 +192,9 @@ auto SaveStorage::saveSlot(const QString &slotName, const QString &title,
   return true;
 }
 
-auto SaveStorage::loadSlot(const QString &slotName, QByteArray &worldState,
-                           QJsonObject &metadata, QByteArray &screenshot,
-                           QString &title, QString *out_error) -> bool {
+auto SaveStorage::load_slot(const QString &slot_name, QByteArray &world_state,
+                            QJsonObject &metadata, QByteArray &screenshot,
+                            QString &title, QString *out_error) -> bool {
   if (!initialize(out_error)) {
     return false;
   }
@@ -203,19 +203,19 @@ auto SaveStorage::loadSlot(const QString &slotName, QByteArray &worldState,
   query.prepare(QStringLiteral(
       "SELECT title, metadata, world_state, screenshot FROM saves "
       "WHERE slot_name = :slot_name"));
-  query.bindValue(QStringLiteral(":slot_name"), slotName);
+  query.bindValue(QStringLiteral(":slot_name"), slot_name);
 
   if (!query.exec()) {
     if (out_error != nullptr) {
       *out_error = QStringLiteral("Failed to read save slot: %1")
-                       .arg(lastErrorString(query.lastError()));
+                       .arg(last_error_string(query.lastError()));
     }
     return false;
   }
 
   if (!query.next()) {
     if (out_error != nullptr) {
-      *out_error = QStringLiteral("Save slot '%1' not found").arg(slotName);
+      *out_error = QStringLiteral("Save slot '%1' not found").arg(slot_name);
     }
     return false;
   }
@@ -223,12 +223,12 @@ auto SaveStorage::loadSlot(const QString &slotName, QByteArray &worldState,
   title = query.value(0).toString();
   const QByteArray metadata_bytes = query.value(1).toByteArray();
   metadata = QJsonDocument::fromJson(metadata_bytes).object();
-  worldState = query.value(2).toByteArray();
+  world_state = query.value(2).toByteArray();
   screenshot = query.value(3).toByteArray();
   return true;
 }
 
-auto SaveStorage::listSlots(QString *out_error) const -> QVariantList {
+auto SaveStorage::list_slots(QString *out_error) const -> QVariantList {
   QVariantList result;
   if (!const_cast<SaveStorage *>(this)->initialize(out_error)) {
     return result;
@@ -240,7 +240,7 @@ auto SaveStorage::listSlots(QString *out_error) const -> QVariantList {
           "FROM saves ORDER BY datetime(timestamp) DESC"))) {
     if (out_error != nullptr) {
       *out_error = QStringLiteral("Failed to enumerate save slots: %1")
-                       .arg(lastErrorString(query.lastError()));
+                       .arg(last_error_string(query.lastError()));
     }
     return result;
   }
@@ -295,7 +295,7 @@ auto SaveStorage::list_campaigns(QString *out_error) const -> QVariantList {
   if (!query.exec(sql)) {
     if (out_error != nullptr) {
       *out_error = QStringLiteral("Failed to list campaigns: %1")
-                       .arg(lastErrorString(query.lastError()));
+                       .arg(last_error_string(query.lastError()));
     }
     return result;
   }
@@ -332,7 +332,7 @@ auto SaveStorage::get_campaign_progress(
   if (!query.exec()) {
     if (out_error != nullptr) {
       *out_error = QStringLiteral("Failed to get campaign progress: %1")
-                       .arg(lastErrorString(query.lastError()));
+                       .arg(last_error_string(query.lastError()));
     }
     return result;
   }
@@ -373,7 +373,7 @@ auto SaveStorage::mark_campaign_completed(const QString &campaign_id,
   if (!query.exec()) {
     if (out_error != nullptr) {
       *out_error = QStringLiteral("Failed to mark campaign as completed: %1")
-                       .arg(lastErrorString(query.lastError()));
+                       .arg(last_error_string(query.lastError()));
     }
     transaction.rollback();
     return false;
@@ -386,8 +386,8 @@ auto SaveStorage::mark_campaign_completed(const QString &campaign_id,
   return true;
 }
 
-auto SaveStorage::deleteSlot(const QString &slotName,
-                             QString *out_error) -> bool {
+auto SaveStorage::delete_slot(const QString &slot_name,
+                              QString *out_error) -> bool {
   if (!initialize(out_error)) {
     return false;
   }
@@ -400,12 +400,12 @@ auto SaveStorage::deleteSlot(const QString &slotName,
   QSqlQuery query(m_database);
   query.prepare(
       QStringLiteral("DELETE FROM saves WHERE slot_name = :slot_name"));
-  query.bindValue(QStringLiteral(":slot_name"), slotName);
+  query.bindValue(QStringLiteral(":slot_name"), slot_name);
 
   if (!query.exec()) {
     if (out_error != nullptr) {
       *out_error = QStringLiteral("Failed to delete save slot: %1")
-                       .arg(lastErrorString(query.lastError()));
+                       .arg(last_error_string(query.lastError()));
     }
     transaction.rollback();
     return false;
@@ -413,7 +413,7 @@ auto SaveStorage::deleteSlot(const QString &slotName,
 
   if (query.numRowsAffected() == 0) {
     if (out_error != nullptr) {
-      *out_error = QStringLiteral("Save slot '%1' not found").arg(slotName);
+      *out_error = QStringLiteral("Save slot '%1' not found").arg(slot_name);
     }
     transaction.rollback();
     return false;
@@ -440,7 +440,7 @@ auto SaveStorage::open(QString *out_error) const -> bool {
   if (!m_database.open()) {
     if (out_error != nullptr) {
       *out_error = QStringLiteral("Failed to open save database: %1")
-                       .arg(lastErrorString(m_database.lastError()));
+                       .arg(last_error_string(m_database.lastError()));
     }
     return false;
   }
@@ -454,8 +454,8 @@ auto SaveStorage::open(QString *out_error) const -> bool {
   return true;
 }
 
-auto SaveStorage::ensureSchema(QString *out_error) const -> bool {
-  const int current_version = schemaVersion(out_error);
+auto SaveStorage::ensure_schema(QString *out_error) const -> bool {
+  const int current_version = schema_version(out_error);
   if (current_version < 0) {
     return false;
   }
@@ -497,12 +497,12 @@ auto SaveStorage::ensureSchema(QString *out_error) const -> bool {
   return true;
 }
 
-auto SaveStorage::schemaVersion(QString *out_error) const -> int {
+auto SaveStorage::schema_version(QString *out_error) const -> int {
   QSqlQuery pragma_query(m_database);
   if (!pragma_query.exec(QStringLiteral("PRAGMA user_version"))) {
     if (out_error != nullptr) {
       *out_error = QStringLiteral("Failed to read schema version: %1")
-                       .arg(lastErrorString(pragma_query.lastError()));
+                       .arg(last_error_string(pragma_query.lastError()));
     }
     return -1;
   }
@@ -521,14 +521,14 @@ auto SaveStorage::set_schema_version(int version,
           QStringLiteral("PRAGMA user_version = %1").arg(version))) {
     if (out_error != nullptr) {
       *out_error = QStringLiteral("Failed to update schema version: %1")
-                       .arg(lastErrorString(pragma_query.lastError()));
+                       .arg(last_error_string(pragma_query.lastError()));
     }
     return false;
   }
   return true;
 }
 
-auto SaveStorage::createBaseSchema(QString *out_error) const -> bool {
+auto SaveStorage::create_base_schema(QString *out_error) const -> bool {
   QSqlQuery query(m_database);
   const QString create_sql =
       QStringLiteral("CREATE TABLE IF NOT EXISTS saves ("
@@ -547,7 +547,7 @@ auto SaveStorage::createBaseSchema(QString *out_error) const -> bool {
   if (!query.exec(create_sql)) {
     if (out_error != nullptr) {
       *out_error = QStringLiteral("Failed to create save schema: %1")
-                       .arg(lastErrorString(query.lastError()));
+                       .arg(last_error_string(query.lastError()));
     }
     return false;
   }
@@ -558,7 +558,7 @@ auto SaveStorage::createBaseSchema(QString *out_error) const -> bool {
           "(updated_at DESC)"))) {
     if (out_error != nullptr) {
       *out_error = QStringLiteral("Failed to build save index: %1")
-                       .arg(lastErrorString(index_query.lastError()));
+                       .arg(last_error_string(index_query.lastError()));
     }
     return false;
   }
@@ -573,7 +573,7 @@ auto SaveStorage::migrate_schema(int fromVersion,
   while (version < k_current_schema_version) {
     switch (version) {
     case 0:
-      if (!createBaseSchema(out_error)) {
+      if (!create_base_schema(out_error)) {
         return false;
       }
       version = 1;
@@ -611,7 +611,7 @@ auto SaveStorage::migrate_to_2(QString *out_error) const -> bool {
   if (!query.exec(create_campaigns_sql)) {
     if (out_error != nullptr) {
       *out_error = QStringLiteral("Failed to create campaigns table: %1")
-                       .arg(lastErrorString(query.lastError()));
+                       .arg(last_error_string(query.lastError()));
     }
     return false;
   }
@@ -630,7 +630,7 @@ auto SaveStorage::migrate_to_2(QString *out_error) const -> bool {
     if (out_error != nullptr) {
       *out_error =
           QStringLiteral("Failed to create campaign_progress table: %1")
-              .arg(lastErrorString(progress_query.lastError()));
+              .arg(last_error_string(progress_query.lastError()));
     }
     return false;
   }
@@ -646,7 +646,7 @@ auto SaveStorage::migrate_to_2(QString *out_error) const -> bool {
   if (!insert_query.exec(insert_campaign_sql)) {
     if (out_error != nullptr) {
       *out_error = QStringLiteral("Failed to insert initial campaign: %1")
-                       .arg(lastErrorString(insert_query.lastError()));
+                       .arg(last_error_string(insert_query.lastError()));
     }
     return false;
   }
@@ -658,8 +658,9 @@ auto SaveStorage::migrate_to_2(QString *out_error) const -> bool {
 
   if (!progress_insert_query.exec(insert_progress_sql)) {
     if (out_error != nullptr) {
-      *out_error = QStringLiteral("Failed to initialize campaign progress: %1")
-                       .arg(lastErrorString(progress_insert_query.lastError()));
+      *out_error =
+          QStringLiteral("Failed to initialize campaign progress: %1")
+              .arg(last_error_string(progress_insert_query.lastError()));
     }
     return false;
   }

+ 14 - 14
game/systems/save_storage.h

@@ -17,19 +17,19 @@ public:
 
   auto initialize(QString *out_error = nullptr) -> bool;
 
-  auto saveSlot(const QString &slotName, const QString &title,
-                const QJsonObject &metadata, const QByteArray &worldState,
-                const QByteArray &screenshot,
-                QString *out_error = nullptr) -> bool;
+  auto save_slot(const QString &slot_name, const QString &title,
+                 const QJsonObject &metadata, const QByteArray &world_state,
+                 const QByteArray &screenshot,
+                 QString *out_error = nullptr) -> bool;
 
-  auto loadSlot(const QString &slotName, QByteArray &worldState,
-                QJsonObject &metadata, QByteArray &screenshot, QString &title,
-                QString *out_error = nullptr) -> bool;
+  auto load_slot(const QString &slot_name, QByteArray &world_state,
+                 QJsonObject &metadata, QByteArray &screenshot, QString &title,
+                 QString *out_error = nullptr) -> bool;
 
-  auto listSlots(QString *out_error = nullptr) const -> QVariantList;
+  auto list_slots(QString *out_error = nullptr) const -> QVariantList;
 
-  auto deleteSlot(const QString &slotName,
-                  QString *out_error = nullptr) -> bool;
+  auto delete_slot(const QString &slot_name,
+                   QString *out_error = nullptr) -> bool;
 
   auto list_campaigns(QString *out_error = nullptr) const -> QVariantList;
   auto get_campaign_progress(const QString &campaign_id,
@@ -39,11 +39,11 @@ public:
 
 private:
   auto open(QString *out_error = nullptr) const -> bool;
-  auto ensureSchema(QString *out_error = nullptr) const -> bool;
-  auto createBaseSchema(QString *out_error = nullptr) const -> bool;
-  auto migrate_schema(int fromVersion,
+  auto ensure_schema(QString *out_error = nullptr) const -> bool;
+  auto create_base_schema(QString *out_error = nullptr) const -> bool;
+  auto migrate_schema(int from_version,
                       QString *out_error = nullptr) const -> bool;
-  auto schemaVersion(QString *out_error = nullptr) const -> int;
+  auto schema_version(QString *out_error = nullptr) const -> int;
   auto set_schema_version(int version,
                           QString *out_error = nullptr) const -> bool;
   auto migrate_to_2(QString *out_error = nullptr) const -> bool;

+ 8 - 0
main.cpp

@@ -38,6 +38,7 @@
 #include "app/core/game_engine.h"
 #include "app/core/language_manager.h"
 #include "app/models/graphics_settings_proxy.h"
+#include "app/models/map_preview_image_provider.h"
 #include "app/models/minimap_image_provider.h"
 #include "ui/gl_view.h"
 #include "ui/theme.h"
@@ -295,10 +296,17 @@ auto main(int argc, char *argv[]) -> int {
   auto *minimap_provider = new MinimapImageProvider();
   engine->addImageProvider("minimap", minimap_provider);
 
+  // Register map preview image provider
+  qInfo() << "Registering map preview image provider...";
+  auto *map_preview_provider = new MapPreviewImageProvider();
+  engine->addImageProvider("mappreview", map_preview_provider);
+
   qInfo() << "Adding context properties...";
   engine->rootContext()->setContextProperty("languageManager",
                                             language_manager.get());
   engine->rootContext()->setContextProperty("game", game_engine.get());
+  engine->rootContext()->setContextProperty("mapPreviewProvider",
+                                            map_preview_provider);
   engine->rootContext()->setContextProperty("graphicsSettings",
                                             graphics_settings.get());
 

+ 1 - 0
qml_resources.qrc

@@ -3,6 +3,7 @@
         <file>ui/qml/Main.qml</file>
         <file>ui/qml/MainMenu.qml</file>
         <file>ui/qml/MapSelect.qml</file>
+        <file>ui/qml/MapPreview.qml</file>
         <file>ui/qml/CampaignMenu.qml</file>
         <file>ui/qml/MapListPanel.qml</file>
         <file>ui/qml/PlayerListItem.qml</file>

+ 1 - 1
render/entity/nations/carthage/barracks_renderer.cpp

@@ -254,7 +254,7 @@ void draw_phoenician_banner(
   out.mesh(get_unit_cylinder(),
            p.model * Render::Geom::cylinder_between(beam_end, connector_top,
                                                     pole_radius * 0.18F),
-           c.gold, white, 1.0F);
+           c.limestone, white, 1.0F);
 
   float const panel_x = beam_end.x() + (banner_width * 0.5F - beam_length);
 

+ 1 - 1
render/entity/nations/roman/barracks_renderer.cpp

@@ -214,7 +214,7 @@ void drawStandards(const DrawContext &p, ISubmitter &out, Mesh *unit,
   out.mesh(get_unit_cylinder(),
            p.model * Render::Geom::cylinder_between(beam_end, connector_top,
                                                     pole_radius * 0.18F),
-           c.iron, white, 1.0F);
+           c.stone_light, white, 1.0F);
 
   float const panel_x = beam_end.x() + (banner_width * 0.5F - beam_length);
 

+ 0 - 1
render/gl/backend.h

@@ -80,7 +80,6 @@ public:
   healer_aura_pipeline() -> BackendPipelines::HealerAuraPipeline * {
     return m_healerAuraPipeline.get();
   }
-
   void enable_depth_test(bool enable) {
     if (enable) {
       glEnable(GL_DEPTH_TEST);

+ 793 - 34
tests/core/serialization_test.cpp

@@ -3,6 +3,7 @@
 #include "core/serialization.h"
 #include "core/world.h"
 #include "systems/nation_id.h"
+#include "systems/owner_registry.h"
 #include "units/spawn_type.h"
 #include "units/troop_type.h"
 #include <QJsonArray>
@@ -28,7 +29,7 @@ TEST_F(SerializationTest, EntitySerializationBasic) {
 
   auto entity_id = entity->get_id();
 
-  QJsonObject json = Serialization::serializeEntity(entity);
+  QJsonObject json = Serialization::serialize_entity(entity);
 
   EXPECT_TRUE(json.contains("id"));
   EXPECT_EQ(json["id"].toVariant().toULongLong(),
@@ -51,7 +52,7 @@ TEST_F(SerializationTest, TransformComponentSerialization) {
   transform->has_desired_yaw = true;
   transform->desired_yaw = 45.0F;
 
-  QJsonObject json = Serialization::serializeEntity(entity);
+  QJsonObject json = Serialization::serialize_entity(entity);
 
   ASSERT_TRUE(json.contains("transform"));
   QJsonObject transform_obj = json["transform"].toObject();
@@ -69,6 +70,41 @@ TEST_F(SerializationTest, TransformComponentSerialization) {
   EXPECT_FLOAT_EQ(transform_obj["desired_yaw"].toDouble(), 45.0);
 }
 
+TEST_F(SerializationTest, TransformComponentRoundTrip) {
+  auto *original_entity = world->create_entity();
+  auto *transform = original_entity->add_component<TransformComponent>();
+  transform->position.x = 15.0F;
+  transform->position.y = 25.0F;
+  transform->position.z = 35.0F;
+  transform->rotation.x = 1.0F;
+  transform->rotation.y = 2.0F;
+  transform->rotation.z = 3.0F;
+  transform->scale.x = 1.5F;
+  transform->scale.y = 2.5F;
+  transform->scale.z = 3.5F;
+  transform->has_desired_yaw = true;
+  transform->desired_yaw = 90.0F;
+
+  QJsonObject json = Serialization::serialize_entity(original_entity);
+
+  auto *new_entity = world->create_entity();
+  Serialization::deserialize_entity(new_entity, json);
+
+  auto *deserialized = new_entity->get_component<TransformComponent>();
+  ASSERT_NE(deserialized, nullptr);
+  EXPECT_FLOAT_EQ(deserialized->position.x, 15.0F);
+  EXPECT_FLOAT_EQ(deserialized->position.y, 25.0F);
+  EXPECT_FLOAT_EQ(deserialized->position.z, 35.0F);
+  EXPECT_FLOAT_EQ(deserialized->rotation.x, 1.0F);
+  EXPECT_FLOAT_EQ(deserialized->rotation.y, 2.0F);
+  EXPECT_FLOAT_EQ(deserialized->rotation.z, 3.0F);
+  EXPECT_FLOAT_EQ(deserialized->scale.x, 1.5F);
+  EXPECT_FLOAT_EQ(deserialized->scale.y, 2.5F);
+  EXPECT_FLOAT_EQ(deserialized->scale.z, 3.5F);
+  EXPECT_TRUE(deserialized->has_desired_yaw);
+  EXPECT_FLOAT_EQ(deserialized->desired_yaw, 90.0F);
+}
+
 TEST_F(SerializationTest, UnitComponentSerialization) {
   auto *entity = world->create_entity();
   auto *unit = entity->add_component<UnitComponent>();
@@ -81,7 +117,7 @@ TEST_F(SerializationTest, UnitComponentSerialization) {
   unit->owner_id = 1;
   unit->nation_id = Game::Systems::NationID::RomanRepublic;
 
-  QJsonObject json = Serialization::serializeEntity(entity);
+  QJsonObject json = Serialization::serialize_entity(entity);
 
   ASSERT_TRUE(json.contains("unit"));
   QJsonObject unit_obj = json["unit"].toObject();
@@ -95,6 +131,33 @@ TEST_F(SerializationTest, UnitComponentSerialization) {
   EXPECT_EQ(unit_obj["nation_id"].toString(), QString("roman_republic"));
 }
 
+TEST_F(SerializationTest, UnitComponentRoundTrip) {
+  auto *original_entity = world->create_entity();
+  auto *unit = original_entity->add_component<UnitComponent>();
+  unit->health = 75;
+  unit->max_health = 150;
+  unit->speed = 7.5F;
+  unit->vision_range = 20.0F;
+  unit->spawn_type = Game::Units::SpawnType::Spearman;
+  unit->owner_id = 2;
+  unit->nation_id = Game::Systems::NationID::Carthage;
+
+  QJsonObject json = Serialization::serialize_entity(original_entity);
+
+  auto *new_entity = world->create_entity();
+  Serialization::deserialize_entity(new_entity, json);
+
+  auto *deserialized = new_entity->get_component<UnitComponent>();
+  ASSERT_NE(deserialized, nullptr);
+  EXPECT_EQ(deserialized->health, 75);
+  EXPECT_EQ(deserialized->max_health, 150);
+  EXPECT_FLOAT_EQ(deserialized->speed, 7.5F);
+  EXPECT_FLOAT_EQ(deserialized->vision_range, 20.0F);
+  EXPECT_EQ(deserialized->spawn_type, Game::Units::SpawnType::Spearman);
+  EXPECT_EQ(deserialized->owner_id, 2);
+  EXPECT_EQ(deserialized->nation_id, Game::Systems::NationID::Carthage);
+}
+
 TEST_F(SerializationTest, MovementComponentSerialization) {
   auto *entity = world->create_entity();
   auto *movement = entity->add_component<MovementComponent>();
@@ -116,7 +179,7 @@ TEST_F(SerializationTest, MovementComponentSerialization) {
   movement->path.emplace_back(10.0F, 20.0F);
   movement->path.emplace_back(30.0F, 40.0F);
 
-  QJsonObject json = Serialization::serializeEntity(entity);
+  QJsonObject json = Serialization::serialize_entity(entity);
 
   ASSERT_TRUE(json.contains("movement"));
   QJsonObject movement_obj = json["movement"].toObject();
@@ -164,7 +227,7 @@ TEST_F(SerializationTest, AttackComponentSerialization) {
   attack->in_melee_lock = false;
   attack->melee_lock_target_id = 0;
 
-  QJsonObject json = Serialization::serializeEntity(entity);
+  QJsonObject json = Serialization::serialize_entity(entity);
 
   ASSERT_TRUE(json.contains("attack"));
   QJsonObject attack_obj = json["attack"].toObject();
@@ -196,10 +259,10 @@ TEST_F(SerializationTest, EntityDeserializationRoundTrip) {
   unit->max_health = 100;
   unit->speed = 6.0F;
 
-  QJsonObject json = Serialization::serializeEntity(original_entity);
+  QJsonObject json = Serialization::serialize_entity(original_entity);
 
   auto *new_entity = world->create_entity();
-  Serialization::deserializeEntity(new_entity, json);
+  Serialization::deserialize_entity(new_entity, json);
 
   auto *deserialized_transform =
       new_entity->get_component<TransformComponent>();
@@ -224,7 +287,7 @@ TEST_F(SerializationTest, DeserializationWithMissingFields) {
   json["unit"] = unit_obj;
 
   auto *entity = world->create_entity();
-  Serialization::deserializeEntity(entity, json);
+  Serialization::deserialize_entity(entity, json);
 
   auto *unit = entity->get_component<UnitComponent>();
   ASSERT_NE(unit, nullptr);
@@ -242,7 +305,7 @@ TEST_F(SerializationTest, DeserializationWithMalformedJSON) {
 
   auto *entity = world->create_entity();
 
-  EXPECT_NO_THROW({ Serialization::deserializeEntity(entity, json); });
+  EXPECT_NO_THROW({ Serialization::deserialize_entity(entity, json); });
 
   auto *transform = entity->get_component<TransformComponent>();
   ASSERT_NE(transform, nullptr);
@@ -258,7 +321,7 @@ TEST_F(SerializationTest, WorldSerializationRoundTrip) {
   auto *transform2 = entity2->add_component<TransformComponent>();
   transform2->position.x = 20.0F;
 
-  QJsonDocument doc = Serialization::serializeWorld(world.get());
+  QJsonDocument doc = Serialization::serialize_world(world.get());
 
   ASSERT_TRUE(doc.isObject());
   QJsonObject world_obj = doc.object();
@@ -267,7 +330,7 @@ TEST_F(SerializationTest, WorldSerializationRoundTrip) {
   EXPECT_TRUE(world_obj.contains("schemaVersion"));
 
   auto new_world = std::make_unique<World>();
-  Serialization::deserializeWorld(new_world.get(), doc);
+  Serialization::deserialize_world(new_world.get(), doc);
 
   const auto &entities = new_world->get_entities();
   EXPECT_EQ(entities.size(), 2UL);
@@ -280,20 +343,20 @@ TEST_F(SerializationTest, SaveAndLoadFromFile) {
   transform->position.y = 43.0F;
   transform->position.z = 44.0F;
 
-  QJsonDocument doc = Serialization::serializeWorld(world.get());
+  QJsonDocument doc = Serialization::serialize_world(world.get());
 
   QTemporaryFile temp_file;
   ASSERT_TRUE(temp_file.open());
   QString filename = temp_file.fileName();
   temp_file.close();
 
-  EXPECT_TRUE(Serialization::saveToFile(filename, doc));
+  EXPECT_TRUE(Serialization::save_to_file(filename, doc));
 
   QJsonDocument loaded_doc = Serialization::load_from_file(filename);
   EXPECT_FALSE(loaded_doc.isNull());
 
   auto new_world = std::make_unique<World>();
-  Serialization::deserializeWorld(new_world.get(), loaded_doc);
+  Serialization::deserialize_world(new_world.get(), loaded_doc);
 
   const auto &entities = new_world->get_entities();
   EXPECT_EQ(entities.size(), 1UL);
@@ -325,7 +388,7 @@ TEST_F(SerializationTest, ProductionComponentSerialization) {
   production->production_queue.push_back(Game::Units::TroopType::Spearman);
   production->production_queue.push_back(Game::Units::TroopType::Archer);
 
-  QJsonObject json = Serialization::serializeEntity(entity);
+  QJsonObject json = Serialization::serialize_entity(entity);
 
   ASSERT_TRUE(json.contains("production"));
   QJsonObject prod_obj = json["production"].toObject();
@@ -358,7 +421,7 @@ TEST_F(SerializationTest, PatrolComponentSerialization) {
   patrol->waypoints.emplace_back(30.0F, 40.0F);
   patrol->waypoints.emplace_back(50.0F, 60.0F);
 
-  QJsonObject json = Serialization::serializeEntity(entity);
+  QJsonObject json = Serialization::serialize_entity(entity);
 
   ASSERT_TRUE(json.contains("patrol"));
   QJsonObject patrol_obj = json["patrol"].toObject();
@@ -375,6 +438,129 @@ TEST_F(SerializationTest, PatrolComponentSerialization) {
   EXPECT_FLOAT_EQ(wp0["y"].toDouble(), 20.0);
 }
 
+TEST_F(SerializationTest, PatrolComponentRoundTrip) {
+  auto *original_entity = world->create_entity();
+  auto *patrol = original_entity->add_component<PatrolComponent>();
+  patrol->current_waypoint = 2;
+  patrol->patrolling = true;
+  patrol->waypoints.emplace_back(15.0F, 25.0F);
+  patrol->waypoints.emplace_back(35.0F, 45.0F);
+
+  QJsonObject json = Serialization::serialize_entity(original_entity);
+
+  auto *new_entity = world->create_entity();
+  Serialization::deserialize_entity(new_entity, json);
+
+  auto *deserialized = new_entity->get_component<PatrolComponent>();
+  ASSERT_NE(deserialized, nullptr);
+  EXPECT_EQ(deserialized->current_waypoint, 2UL);
+  EXPECT_TRUE(deserialized->patrolling);
+  EXPECT_EQ(deserialized->waypoints.size(), 2UL);
+  EXPECT_FLOAT_EQ(deserialized->waypoints[0].first, 15.0F);
+  EXPECT_FLOAT_EQ(deserialized->waypoints[0].second, 25.0F);
+}
+
+TEST_F(SerializationTest, MovementComponentRoundTrip) {
+  auto *original_entity = world->create_entity();
+  auto *movement = original_entity->add_component<MovementComponent>();
+  movement->has_target = true;
+  movement->target_x = 100.0F;
+  movement->target_y = 200.0F;
+  movement->goal_x = 150.0F;
+  movement->goal_y = 250.0F;
+  movement->vx = 1.5F;
+  movement->vz = 2.5F;
+  movement->path.emplace_back(10.0F, 20.0F);
+  movement->path.emplace_back(30.0F, 40.0F);
+
+  QJsonObject json = Serialization::serialize_entity(original_entity);
+
+  auto *new_entity = world->create_entity();
+  Serialization::deserialize_entity(new_entity, json);
+
+  auto *deserialized = new_entity->get_component<MovementComponent>();
+  ASSERT_NE(deserialized, nullptr);
+  EXPECT_TRUE(deserialized->has_target);
+  EXPECT_FLOAT_EQ(deserialized->target_x, 100.0F);
+  EXPECT_FLOAT_EQ(deserialized->target_y, 200.0F);
+  EXPECT_FLOAT_EQ(deserialized->goal_x, 150.0F);
+  EXPECT_FLOAT_EQ(deserialized->goal_y, 250.0F);
+  EXPECT_FLOAT_EQ(deserialized->vx, 1.5F);
+  EXPECT_FLOAT_EQ(deserialized->vz, 2.5F);
+  EXPECT_EQ(deserialized->path.size(), 2UL);
+}
+
+TEST_F(SerializationTest, AttackComponentRoundTrip) {
+  auto *original_entity = world->create_entity();
+  auto *attack = original_entity->add_component<AttackComponent>();
+  attack->range = 15.0F;
+  attack->damage = 30;
+  attack->cooldown = 2.5F;
+  attack->melee_range = 3.0F;
+  attack->melee_damage = 20;
+  attack->preferred_mode = AttackComponent::CombatMode::Ranged;
+  attack->current_mode = AttackComponent::CombatMode::Melee;
+  attack->can_melee = true;
+  attack->can_ranged = true;
+  attack->in_melee_lock = true;
+  attack->melee_lock_target_id = 42;
+
+  QJsonObject json = Serialization::serialize_entity(original_entity);
+
+  auto *new_entity = world->create_entity();
+  Serialization::deserialize_entity(new_entity, json);
+
+  auto *deserialized = new_entity->get_component<AttackComponent>();
+  ASSERT_NE(deserialized, nullptr);
+  EXPECT_FLOAT_EQ(deserialized->range, 15.0F);
+  EXPECT_EQ(deserialized->damage, 30);
+  EXPECT_FLOAT_EQ(deserialized->cooldown, 2.5F);
+  EXPECT_FLOAT_EQ(deserialized->melee_range, 3.0F);
+  EXPECT_EQ(deserialized->melee_damage, 20);
+  EXPECT_EQ(deserialized->preferred_mode, AttackComponent::CombatMode::Ranged);
+  EXPECT_EQ(deserialized->current_mode, AttackComponent::CombatMode::Melee);
+  EXPECT_TRUE(deserialized->can_melee);
+  EXPECT_TRUE(deserialized->can_ranged);
+  EXPECT_TRUE(deserialized->in_melee_lock);
+  EXPECT_EQ(deserialized->melee_lock_target_id, 42U);
+}
+
+TEST_F(SerializationTest, ProductionComponentRoundTrip) {
+  auto *original_entity = world->create_entity();
+  auto *production = original_entity->add_component<ProductionComponent>();
+  production->in_progress = true;
+  production->build_time = 15.0F;
+  production->time_remaining = 7.5F;
+  production->produced_count = 5;
+  production->max_units = 20;
+  production->product_type = Game::Units::TroopType::Spearman;
+  production->rally_x = 150.0F;
+  production->rally_z = 250.0F;
+  production->rally_set = true;
+  production->villager_cost = 3;
+  production->production_queue.push_back(Game::Units::TroopType::Archer);
+
+  QJsonObject json = Serialization::serialize_entity(original_entity);
+
+  auto *new_entity = world->create_entity();
+  Serialization::deserialize_entity(new_entity, json);
+
+  auto *deserialized = new_entity->get_component<ProductionComponent>();
+  ASSERT_NE(deserialized, nullptr);
+  EXPECT_TRUE(deserialized->in_progress);
+  EXPECT_FLOAT_EQ(deserialized->build_time, 15.0F);
+  EXPECT_FLOAT_EQ(deserialized->time_remaining, 7.5F);
+  EXPECT_EQ(deserialized->produced_count, 5);
+  EXPECT_EQ(deserialized->max_units, 20);
+  EXPECT_EQ(deserialized->product_type, Game::Units::TroopType::Spearman);
+  EXPECT_FLOAT_EQ(deserialized->rally_x, 150.0F);
+  EXPECT_FLOAT_EQ(deserialized->rally_z, 250.0F);
+  EXPECT_TRUE(deserialized->rally_set);
+  EXPECT_EQ(deserialized->villager_cost, 3);
+  EXPECT_EQ(deserialized->production_queue.size(), 1UL);
+  EXPECT_EQ(deserialized->production_queue[0], Game::Units::TroopType::Archer);
+}
+
 TEST_F(SerializationTest, RenderableComponentSerialization) {
   auto *entity = world->create_entity();
   auto *renderable =
@@ -387,7 +573,7 @@ TEST_F(SerializationTest, RenderableComponentSerialization) {
   renderable->mesh = RenderableComponent::MeshKind::Capsule;
   renderable->color = {0.8F, 0.2F, 0.5F};
 
-  QJsonObject json = Serialization::serializeEntity(entity);
+  QJsonObject json = Serialization::serialize_entity(entity);
 
   ASSERT_TRUE(json.contains("renderable"));
   QJsonObject renderable_obj = json["renderable"].toObject();
@@ -420,10 +606,10 @@ TEST_F(SerializationTest, RenderableComponentRoundTrip) {
   renderable->mesh = RenderableComponent::MeshKind::Quad;
   renderable->color = {1.0F, 0.5F, 0.25F};
 
-  QJsonObject json = Serialization::serializeEntity(original_entity);
+  QJsonObject json = Serialization::serialize_entity(original_entity);
 
   auto *new_entity = world->create_entity();
-  Serialization::deserializeEntity(new_entity, json);
+  Serialization::deserialize_entity(new_entity, json);
 
   auto *deserialized = new_entity->get_component<RenderableComponent>();
   ASSERT_NE(deserialized, nullptr);
@@ -443,7 +629,7 @@ TEST_F(SerializationTest, AttackTargetComponentSerialization) {
   attack_target->target_id = 42;
   attack_target->should_chase = true;
 
-  QJsonObject json = Serialization::serializeEntity(entity);
+  QJsonObject json = Serialization::serialize_entity(entity);
 
   ASSERT_TRUE(json.contains("attack_target"));
   QJsonObject attack_target_obj = json["attack_target"].toObject();
@@ -458,10 +644,10 @@ TEST_F(SerializationTest, AttackTargetComponentRoundTrip) {
   attack_target->target_id = 123;
   attack_target->should_chase = false;
 
-  QJsonObject json = Serialization::serializeEntity(original_entity);
+  QJsonObject json = Serialization::serialize_entity(original_entity);
 
   auto *new_entity = world->create_entity();
-  Serialization::deserializeEntity(new_entity, json);
+  Serialization::deserialize_entity(new_entity, json);
 
   auto *deserialized = new_entity->get_component<AttackTargetComponent>();
   ASSERT_NE(deserialized, nullptr);
@@ -473,7 +659,7 @@ TEST_F(SerializationTest, BuildingComponentSerialization) {
   auto *entity = world->create_entity();
   entity->add_component<BuildingComponent>();
 
-  QJsonObject json = Serialization::serializeEntity(entity);
+  QJsonObject json = Serialization::serialize_entity(entity);
 
   ASSERT_TRUE(json.contains("building"));
   EXPECT_TRUE(json["building"].toBool());
@@ -483,10 +669,10 @@ TEST_F(SerializationTest, BuildingComponentRoundTrip) {
   auto *original_entity = world->create_entity();
   original_entity->add_component<BuildingComponent>();
 
-  QJsonObject json = Serialization::serializeEntity(original_entity);
+  QJsonObject json = Serialization::serialize_entity(original_entity);
 
   auto *new_entity = world->create_entity();
-  Serialization::deserializeEntity(new_entity, json);
+  Serialization::deserialize_entity(new_entity, json);
 
   auto *deserialized = new_entity->get_component<BuildingComponent>();
   ASSERT_NE(deserialized, nullptr);
@@ -496,7 +682,7 @@ TEST_F(SerializationTest, AIControlledComponentSerialization) {
   auto *entity = world->create_entity();
   entity->add_component<AIControlledComponent>();
 
-  QJsonObject json = Serialization::serializeEntity(entity);
+  QJsonObject json = Serialization::serialize_entity(entity);
 
   ASSERT_TRUE(json.contains("aiControlled"));
   EXPECT_TRUE(json["aiControlled"].toBool());
@@ -506,10 +692,10 @@ TEST_F(SerializationTest, AIControlledComponentRoundTrip) {
   auto *original_entity = world->create_entity();
   original_entity->add_component<AIControlledComponent>();
 
-  QJsonObject json = Serialization::serializeEntity(original_entity);
+  QJsonObject json = Serialization::serialize_entity(original_entity);
 
   auto *new_entity = world->create_entity();
-  Serialization::deserializeEntity(new_entity, json);
+  Serialization::deserialize_entity(new_entity, json);
 
   auto *deserialized = new_entity->get_component<AIControlledComponent>();
   ASSERT_NE(deserialized, nullptr);
@@ -524,7 +710,7 @@ TEST_F(SerializationTest, CaptureComponentSerialization) {
   capture->required_time = 15.0F;
   capture->is_being_captured = true;
 
-  QJsonObject json = Serialization::serializeEntity(entity);
+  QJsonObject json = Serialization::serialize_entity(entity);
 
   ASSERT_TRUE(json.contains("capture"));
   QJsonObject capture_obj = json["capture"].toObject();
@@ -543,10 +729,10 @@ TEST_F(SerializationTest, CaptureComponentRoundTrip) {
   capture->required_time = 20.0F;
   capture->is_being_captured = false;
 
-  QJsonObject json = Serialization::serializeEntity(original_entity);
+  QJsonObject json = Serialization::serialize_entity(original_entity);
 
   auto *new_entity = world->create_entity();
-  Serialization::deserializeEntity(new_entity, json);
+  Serialization::deserialize_entity(new_entity, json);
 
   auto *deserialized = new_entity->get_component<CaptureComponent>();
   ASSERT_NE(deserialized, nullptr);
@@ -592,7 +778,16 @@ TEST_F(SerializationTest, CompleteEntityWithAllComponents) {
   auto *capture = entity->add_component<CaptureComponent>();
   capture->is_being_captured = true;
 
-  QJsonObject json = Serialization::serializeEntity(entity);
+  auto *hold_mode = entity->add_component<HoldModeComponent>();
+  hold_mode->active = true;
+
+  auto *healer = entity->add_component<HealerComponent>();
+  healer->healing_amount = 10;
+
+  auto *catapult = entity->add_component<CatapultLoadingComponent>();
+  catapult->state = CatapultLoadingComponent::LoadingState::Idle;
+
+  QJsonObject json = Serialization::serialize_entity(entity);
 
   EXPECT_TRUE(json.contains("transform"));
   EXPECT_TRUE(json.contains("renderable"));
@@ -604,9 +799,12 @@ TEST_F(SerializationTest, CompleteEntityWithAllComponents) {
   EXPECT_TRUE(json.contains("production"));
   EXPECT_TRUE(json.contains("aiControlled"));
   EXPECT_TRUE(json.contains("capture"));
+  EXPECT_TRUE(json.contains("hold_mode"));
+  EXPECT_TRUE(json.contains("healer"));
+  EXPECT_TRUE(json.contains("catapult_loading"));
 
   auto *new_entity = world->create_entity();
-  Serialization::deserializeEntity(new_entity, json);
+  Serialization::deserialize_entity(new_entity, json);
 
   EXPECT_NE(new_entity->get_component<TransformComponent>(), nullptr);
   EXPECT_NE(new_entity->get_component<RenderableComponent>(), nullptr);
@@ -618,10 +816,13 @@ TEST_F(SerializationTest, CompleteEntityWithAllComponents) {
   EXPECT_NE(new_entity->get_component<ProductionComponent>(), nullptr);
   EXPECT_NE(new_entity->get_component<AIControlledComponent>(), nullptr);
   EXPECT_NE(new_entity->get_component<CaptureComponent>(), nullptr);
+  EXPECT_NE(new_entity->get_component<HoldModeComponent>(), nullptr);
+  EXPECT_NE(new_entity->get_component<HealerComponent>(), nullptr);
+  EXPECT_NE(new_entity->get_component<CatapultLoadingComponent>(), nullptr);
 }
 
 TEST_F(SerializationTest, EmptyWorldSerialization) {
-  QJsonDocument doc = Serialization::serializeWorld(world.get());
+  QJsonDocument doc = Serialization::serialize_world(world.get());
 
   ASSERT_TRUE(doc.isObject());
   QJsonObject world_obj = doc.object();
@@ -630,3 +831,561 @@ TEST_F(SerializationTest, EmptyWorldSerialization) {
   QJsonArray entities = world_obj["entities"].toArray();
   EXPECT_EQ(entities.size(), 0);
 }
+
+TEST_F(SerializationTest, HoldModeComponentSerialization) {
+  auto *entity = world->create_entity();
+  auto *hold_mode = entity->add_component<HoldModeComponent>();
+
+  hold_mode->active = false;
+  hold_mode->exit_cooldown = 1.5F;
+  hold_mode->stand_up_duration = 3.0F;
+
+  QJsonObject json = Serialization::serialize_entity(entity);
+
+  ASSERT_TRUE(json.contains("hold_mode"));
+  QJsonObject hold_mode_obj = json["hold_mode"].toObject();
+
+  EXPECT_FALSE(hold_mode_obj["active"].toBool());
+  EXPECT_FLOAT_EQ(hold_mode_obj["exit_cooldown"].toDouble(), 1.5);
+  EXPECT_FLOAT_EQ(hold_mode_obj["stand_up_duration"].toDouble(), 3.0);
+}
+
+TEST_F(SerializationTest, HoldModeComponentRoundTrip) {
+  auto *original_entity = world->create_entity();
+  auto *hold_mode = original_entity->add_component<HoldModeComponent>();
+  hold_mode->active = true;
+  hold_mode->exit_cooldown = 2.5F;
+  hold_mode->stand_up_duration = 4.0F;
+
+  QJsonObject json = Serialization::serialize_entity(original_entity);
+
+  auto *new_entity = world->create_entity();
+  Serialization::deserialize_entity(new_entity, json);
+
+  auto *deserialized = new_entity->get_component<HoldModeComponent>();
+  ASSERT_NE(deserialized, nullptr);
+  EXPECT_TRUE(deserialized->active);
+  EXPECT_FLOAT_EQ(deserialized->exit_cooldown, 2.5F);
+  EXPECT_FLOAT_EQ(deserialized->stand_up_duration, 4.0F);
+}
+
+TEST_F(SerializationTest, HealerComponentSerialization) {
+  auto *entity = world->create_entity();
+  auto *healer = entity->add_component<HealerComponent>();
+
+  healer->healing_range = 12.0F;
+  healer->healing_amount = 10;
+  healer->healing_cooldown = 3.0F;
+  healer->time_since_last_heal = 1.0F;
+
+  QJsonObject json = Serialization::serialize_entity(entity);
+
+  ASSERT_TRUE(json.contains("healer"));
+  QJsonObject healer_obj = json["healer"].toObject();
+
+  EXPECT_FLOAT_EQ(healer_obj["healing_range"].toDouble(), 12.0);
+  EXPECT_EQ(healer_obj["healing_amount"].toInt(), 10);
+  EXPECT_FLOAT_EQ(healer_obj["healing_cooldown"].toDouble(), 3.0);
+  EXPECT_FLOAT_EQ(healer_obj["time_since_last_heal"].toDouble(), 1.0);
+}
+
+TEST_F(SerializationTest, HealerComponentRoundTrip) {
+  auto *original_entity = world->create_entity();
+  auto *healer = original_entity->add_component<HealerComponent>();
+  healer->healing_range = 15.0F;
+  healer->healing_amount = 8;
+  healer->healing_cooldown = 4.0F;
+  healer->time_since_last_heal = 2.0F;
+
+  QJsonObject json = Serialization::serialize_entity(original_entity);
+
+  auto *new_entity = world->create_entity();
+  Serialization::deserialize_entity(new_entity, json);
+
+  auto *deserialized = new_entity->get_component<HealerComponent>();
+  ASSERT_NE(deserialized, nullptr);
+  EXPECT_FLOAT_EQ(deserialized->healing_range, 15.0F);
+  EXPECT_EQ(deserialized->healing_amount, 8);
+  EXPECT_FLOAT_EQ(deserialized->healing_cooldown, 4.0F);
+  EXPECT_FLOAT_EQ(deserialized->time_since_last_heal, 2.0F);
+}
+
+TEST_F(SerializationTest, CatapultLoadingComponentSerialization) {
+  auto *entity = world->create_entity();
+  auto *catapult = entity->add_component<CatapultLoadingComponent>();
+
+  catapult->state = CatapultLoadingComponent::LoadingState::Loading;
+  catapult->loading_time = 1.0F;
+  catapult->loading_duration = 3.0F;
+  catapult->firing_time = 0.0F;
+  catapult->firing_duration = 1.0F;
+  catapult->target_id = 42;
+  catapult->target_locked_x = 100.0F;
+  catapult->target_locked_y = 50.0F;
+  catapult->target_locked_z = 200.0F;
+  catapult->target_position_locked = true;
+
+  QJsonObject json = Serialization::serialize_entity(entity);
+
+  ASSERT_TRUE(json.contains("catapult_loading"));
+  QJsonObject catapult_obj = json["catapult_loading"].toObject();
+
+  EXPECT_EQ(catapult_obj["state"].toInt(),
+            static_cast<int>(CatapultLoadingComponent::LoadingState::Loading));
+  EXPECT_FLOAT_EQ(catapult_obj["loading_time"].toDouble(), 1.0);
+  EXPECT_FLOAT_EQ(catapult_obj["loading_duration"].toDouble(), 3.0);
+  EXPECT_FLOAT_EQ(catapult_obj["firing_time"].toDouble(), 0.0);
+  EXPECT_FLOAT_EQ(catapult_obj["firing_duration"].toDouble(), 1.0);
+  EXPECT_EQ(catapult_obj["target_id"].toVariant().toULongLong(), 42ULL);
+  EXPECT_FLOAT_EQ(catapult_obj["target_locked_x"].toDouble(), 100.0);
+  EXPECT_FLOAT_EQ(catapult_obj["target_locked_y"].toDouble(), 50.0);
+  EXPECT_FLOAT_EQ(catapult_obj["target_locked_z"].toDouble(), 200.0);
+  EXPECT_TRUE(catapult_obj["target_position_locked"].toBool());
+}
+
+TEST_F(SerializationTest, CatapultLoadingComponentRoundTrip) {
+  auto *original_entity = world->create_entity();
+  auto *catapult = original_entity->add_component<CatapultLoadingComponent>();
+  catapult->state = CatapultLoadingComponent::LoadingState::ReadyToFire;
+  catapult->loading_time = 2.0F;
+  catapult->loading_duration = 4.0F;
+  catapult->firing_time = 0.25F;
+  catapult->firing_duration = 0.75F;
+  catapult->target_id = 99;
+  catapult->target_locked_x = 150.0F;
+  catapult->target_locked_y = 75.0F;
+  catapult->target_locked_z = 250.0F;
+  catapult->target_position_locked = false;
+
+  QJsonObject json = Serialization::serialize_entity(original_entity);
+
+  auto *new_entity = world->create_entity();
+  Serialization::deserialize_entity(new_entity, json);
+
+  auto *deserialized = new_entity->get_component<CatapultLoadingComponent>();
+  ASSERT_NE(deserialized, nullptr);
+  EXPECT_EQ(deserialized->state,
+            CatapultLoadingComponent::LoadingState::ReadyToFire);
+  EXPECT_FLOAT_EQ(deserialized->loading_time, 2.0F);
+  EXPECT_FLOAT_EQ(deserialized->loading_duration, 4.0F);
+  EXPECT_FLOAT_EQ(deserialized->firing_time, 0.25F);
+  EXPECT_FLOAT_EQ(deserialized->firing_duration, 0.75F);
+  EXPECT_EQ(deserialized->target_id, 99U);
+  EXPECT_FLOAT_EQ(deserialized->target_locked_x, 150.0F);
+  EXPECT_FLOAT_EQ(deserialized->target_locked_y, 75.0F);
+  EXPECT_FLOAT_EQ(deserialized->target_locked_z, 250.0F);
+  EXPECT_FALSE(deserialized->target_position_locked);
+}
+
+// ============================================================================
+// Integration Tests: Multi-Unit Battlefield State Preservation
+// ============================================================================
+
+TEST_F(SerializationTest, MultipleUnitsPositionsAndHealthPreserved) {
+  // Create a battlefield with multiple units at different positions
+  struct UnitData {
+    float x, y, z;
+    int health;
+    int max_health;
+    int owner_id;
+    Game::Units::SpawnType spawn_type;
+  };
+
+  std::vector<UnitData> original_units = {
+      {10.0F, 0.0F, 20.0F, 80, 100, 1, Game::Units::SpawnType::Archer},
+      {15.5F, 1.0F, 25.5F, 45, 100, 1, Game::Units::SpawnType::Spearman},
+      {30.0F, 0.0F, 40.0F, 100, 100, 2, Game::Units::SpawnType::Knight},
+      {35.5F, 2.0F, 45.5F, 60, 150, 2, Game::Units::SpawnType::HorseArcher},
+      {50.0F, 0.5F, 60.0F, 25, 80, 1, Game::Units::SpawnType::Catapult},
+  };
+
+  std::vector<EntityID> entity_ids;
+  for (const auto &unit_data : original_units) {
+    auto *entity = world->create_entity();
+    entity_ids.push_back(entity->get_id());
+
+    auto *transform = entity->add_component<TransformComponent>();
+    transform->position.x = unit_data.x;
+    transform->position.y = unit_data.y;
+    transform->position.z = unit_data.z;
+
+    auto *unit = entity->add_component<UnitComponent>();
+    unit->health = unit_data.health;
+    unit->max_health = unit_data.max_health;
+    unit->owner_id = unit_data.owner_id;
+    unit->spawn_type = unit_data.spawn_type;
+  }
+
+  // Serialize and deserialize the world
+  QJsonDocument doc = Serialization::serialize_world(world.get());
+  auto restored_world = std::make_unique<World>();
+  Serialization::deserialize_world(restored_world.get(), doc);
+
+  // Verify all units are restored with exact positions and health
+  const auto &entities = restored_world->get_entities();
+  EXPECT_EQ(entities.size(), original_units.size());
+
+  for (size_t i = 0; i < entity_ids.size(); ++i) {
+    auto *entity = restored_world->get_entity(entity_ids[i]);
+    ASSERT_NE(entity, nullptr) << "Entity " << i << " not found";
+
+    auto *transform = entity->get_component<TransformComponent>();
+    ASSERT_NE(transform, nullptr);
+    EXPECT_FLOAT_EQ(transform->position.x, original_units[i].x)
+        << "Unit " << i << " X position mismatch";
+    EXPECT_FLOAT_EQ(transform->position.y, original_units[i].y)
+        << "Unit " << i << " Y position mismatch";
+    EXPECT_FLOAT_EQ(transform->position.z, original_units[i].z)
+        << "Unit " << i << " Z position mismatch";
+
+    auto *unit = entity->get_component<UnitComponent>();
+    ASSERT_NE(unit, nullptr);
+    EXPECT_EQ(unit->health, original_units[i].health)
+        << "Unit " << i << " health mismatch";
+    EXPECT_EQ(unit->max_health, original_units[i].max_health)
+        << "Unit " << i << " max_health mismatch";
+    EXPECT_EQ(unit->owner_id, original_units[i].owner_id)
+        << "Unit " << i << " owner_id mismatch";
+    EXPECT_EQ(unit->spawn_type, original_units[i].spawn_type)
+        << "Unit " << i << " spawn_type mismatch";
+  }
+}
+
+TEST_F(SerializationTest, OwnerRegistryTeamsAndColorsPreserved) {
+  // Setup owner registry with teams and custom colors
+  auto &registry = Game::Systems::OwnerRegistry::instance();
+  registry.clear();
+
+  // Register players with specific teams and colors
+  int player1 =
+      registry.register_owner(Game::Systems::OwnerType::Player, "Blue Kingdom");
+  int player2 =
+      registry.register_owner(Game::Systems::OwnerType::AI, "Red Empire");
+  int player3 = registry.register_owner(Game::Systems::OwnerType::Player,
+                                        "Green Alliance");
+
+  // Set teams (player1 and player3 are allies)
+  registry.set_owner_team(player1, 1);
+  registry.set_owner_team(player2, 2);
+  registry.set_owner_team(player3, 1);
+
+  // Set custom colors
+  registry.set_owner_color(player1, 0.1F, 0.2F, 0.9F);
+  registry.set_owner_color(player2, 0.9F, 0.1F, 0.1F);
+  registry.set_owner_color(player3, 0.1F, 0.9F, 0.2F);
+
+  registry.set_local_player_id(player1);
+
+  // Create some entities owned by these players
+  for (int i = 0; i < 3; ++i) {
+    auto *entity = world->create_entity();
+    auto *unit = entity->add_component<UnitComponent>();
+    unit->owner_id = player1;
+  }
+  for (int i = 0; i < 2; ++i) {
+    auto *entity = world->create_entity();
+    auto *unit = entity->add_component<UnitComponent>();
+    unit->owner_id = player2;
+  }
+
+  // Serialize world (includes owner_registry)
+  QJsonDocument doc = Serialization::serialize_world(world.get());
+
+  // Clear registry and restore
+  registry.clear();
+  auto restored_world = std::make_unique<World>();
+  Serialization::deserialize_world(restored_world.get(), doc);
+
+  // Verify owner registry state is preserved
+  EXPECT_EQ(registry.get_local_player_id(), player1);
+
+  // Verify teams are preserved
+  EXPECT_EQ(registry.get_owner_team(player1), 1);
+  EXPECT_EQ(registry.get_owner_team(player2), 2);
+  EXPECT_EQ(registry.get_owner_team(player3), 1);
+
+  // Verify alliances are preserved
+  EXPECT_TRUE(registry.are_allies(player1, player3));
+  EXPECT_TRUE(registry.are_enemies(player1, player2));
+  EXPECT_TRUE(registry.are_enemies(player2, player3));
+
+  // Verify colors are preserved
+  auto color1 = registry.get_owner_color(player1);
+  EXPECT_FLOAT_EQ(color1[0], 0.1F);
+  EXPECT_FLOAT_EQ(color1[1], 0.2F);
+  EXPECT_FLOAT_EQ(color1[2], 0.9F);
+
+  auto color2 = registry.get_owner_color(player2);
+  EXPECT_FLOAT_EQ(color2[0], 0.9F);
+  EXPECT_FLOAT_EQ(color2[1], 0.1F);
+  EXPECT_FLOAT_EQ(color2[2], 0.1F);
+
+  auto color3 = registry.get_owner_color(player3);
+  EXPECT_FLOAT_EQ(color3[0], 0.1F);
+  EXPECT_FLOAT_EQ(color3[1], 0.9F);
+  EXPECT_FLOAT_EQ(color3[2], 0.2F);
+
+  // Verify owner names are preserved
+  EXPECT_EQ(registry.get_owner_name(player1), "Blue Kingdom");
+  EXPECT_EQ(registry.get_owner_name(player2), "Red Empire");
+  EXPECT_EQ(registry.get_owner_name(player3), "Green Alliance");
+
+  // Verify owner types are preserved
+  EXPECT_TRUE(registry.is_player(player1));
+  EXPECT_TRUE(registry.is_ai(player2));
+  EXPECT_TRUE(registry.is_player(player3));
+
+  // Clean up
+  registry.clear();
+}
+
+TEST_F(SerializationTest, BuildingOwnershipAndCaptureStatePreserved) {
+  // Create buildings (barracks/villages) with different ownership and capture
+  // states
+  struct BuildingData {
+    float x, z;
+    int owner_id;
+    int capturing_player_id;
+    float capture_progress;
+    bool is_being_captured;
+  };
+
+  std::vector<BuildingData> buildings = {
+      {100.0F, 100.0F, 1, -1, 0.0F,
+       false}, // Owned by player 1, not being captured
+      {200.0F, 200.0F, 2, 1, 7.5F,
+       true}, // Owned by player 2, being captured by player 1
+      {300.0F, 300.0F, 1, 2, 15.0F,
+       true}, // Owned by player 1, being captured by player 2
+      {400.0F, 400.0F, -1, -1, 0.0F, false}, // Neutral building
+  };
+
+  std::vector<EntityID> building_ids;
+  for (const auto &bldg : buildings) {
+    auto *entity = world->create_entity();
+    building_ids.push_back(entity->get_id());
+
+    auto *transform = entity->add_component<TransformComponent>();
+    transform->position.x = bldg.x;
+    transform->position.z = bldg.z;
+
+    entity->add_component<BuildingComponent>();
+
+    auto *unit = entity->add_component<UnitComponent>();
+    unit->owner_id = bldg.owner_id;
+
+    auto *capture = entity->add_component<CaptureComponent>();
+    capture->capturing_player_id = bldg.capturing_player_id;
+    capture->capture_progress = bldg.capture_progress;
+    capture->is_being_captured = bldg.is_being_captured;
+  }
+
+  // Serialize and restore
+  QJsonDocument doc = Serialization::serialize_world(world.get());
+  auto restored_world = std::make_unique<World>();
+  Serialization::deserialize_world(restored_world.get(), doc);
+
+  // Verify all buildings are restored with correct ownership and capture state
+  for (size_t i = 0; i < building_ids.size(); ++i) {
+    auto *entity = restored_world->get_entity(building_ids[i]);
+    ASSERT_NE(entity, nullptr) << "Building " << i << " not found";
+
+    auto *transform = entity->get_component<TransformComponent>();
+    ASSERT_NE(transform, nullptr);
+    EXPECT_FLOAT_EQ(transform->position.x, buildings[i].x)
+        << "Building " << i << " X position mismatch";
+    EXPECT_FLOAT_EQ(transform->position.z, buildings[i].z)
+        << "Building " << i << " Z position mismatch";
+
+    EXPECT_NE(entity->get_component<BuildingComponent>(), nullptr);
+
+    auto *unit = entity->get_component<UnitComponent>();
+    ASSERT_NE(unit, nullptr);
+    EXPECT_EQ(unit->owner_id, buildings[i].owner_id)
+        << "Building " << i << " owner mismatch";
+
+    auto *capture = entity->get_component<CaptureComponent>();
+    ASSERT_NE(capture, nullptr);
+    EXPECT_EQ(capture->capturing_player_id, buildings[i].capturing_player_id)
+        << "Building " << i << " capturing_player_id mismatch";
+    EXPECT_FLOAT_EQ(capture->capture_progress, buildings[i].capture_progress)
+        << "Building " << i << " capture_progress mismatch";
+    EXPECT_EQ(capture->is_being_captured, buildings[i].is_being_captured)
+        << "Building " << i << " is_being_captured mismatch";
+  }
+}
+
+TEST_F(SerializationTest, UnitMovementStatePreserved) {
+  // Create units with active movement paths
+  auto *entity = world->create_entity();
+  auto entity_id = entity->get_id();
+
+  auto *transform = entity->add_component<TransformComponent>();
+  transform->position.x = 10.0F;
+  transform->position.y = 0.0F;
+  transform->position.z = 20.0F;
+
+  auto *unit = entity->add_component<UnitComponent>();
+  unit->owner_id = 1;
+  unit->health = 85;
+
+  auto *movement = entity->add_component<MovementComponent>();
+  movement->has_target = true;
+  movement->target_x = 50.0F;
+  movement->target_y = 60.0F;
+  movement->goal_x = 55.0F;
+  movement->goal_y = 65.0F;
+  movement->vx = 2.5F;
+  movement->vz = 3.0F;
+  // Add path waypoints
+  movement->path.emplace_back(20.0F, 30.0F);
+  movement->path.emplace_back(35.0F, 45.0F);
+  movement->path.emplace_back(50.0F, 60.0F);
+  const size_t expected_path_size = movement->path.size();
+
+  // Serialize and restore
+  QJsonDocument doc = Serialization::serialize_world(world.get());
+  auto restored_world = std::make_unique<World>();
+  Serialization::deserialize_world(restored_world.get(), doc);
+
+  // Verify movement state is preserved
+  auto *restored_entity = restored_world->get_entity(entity_id);
+  ASSERT_NE(restored_entity, nullptr);
+
+  auto *restored_movement = restored_entity->get_component<MovementComponent>();
+  ASSERT_NE(restored_movement, nullptr);
+
+  EXPECT_TRUE(restored_movement->has_target);
+  EXPECT_FLOAT_EQ(restored_movement->target_x, 50.0F);
+  EXPECT_FLOAT_EQ(restored_movement->target_y, 60.0F);
+  EXPECT_FLOAT_EQ(restored_movement->goal_x, 55.0F);
+  EXPECT_FLOAT_EQ(restored_movement->goal_y, 65.0F);
+  EXPECT_FLOAT_EQ(restored_movement->vx, 2.5F);
+  EXPECT_FLOAT_EQ(restored_movement->vz, 3.0F);
+
+  // Verify path is preserved
+  ASSERT_EQ(restored_movement->path.size(), expected_path_size);
+  EXPECT_FLOAT_EQ(restored_movement->path[0].first, 20.0F);
+  EXPECT_FLOAT_EQ(restored_movement->path[0].second, 30.0F);
+  EXPECT_FLOAT_EQ(restored_movement->path[1].first, 35.0F);
+  EXPECT_FLOAT_EQ(restored_movement->path[1].second, 45.0F);
+  EXPECT_FLOAT_EQ(restored_movement->path[2].first, 50.0F);
+  EXPECT_FLOAT_EQ(restored_movement->path[2].second, 60.0F);
+}
+
+TEST_F(SerializationTest, CombatStatePreserved) {
+  // Create units engaged in combat
+  auto *attacker = world->create_entity();
+  auto *defender = world->create_entity();
+  auto attacker_id = attacker->get_id();
+  auto defender_id = defender->get_id();
+
+  // Setup attacker
+  auto *attacker_transform = attacker->add_component<TransformComponent>();
+  attacker_transform->position.x = 10.0F;
+  attacker_transform->position.z = 10.0F;
+
+  auto *attacker_unit = attacker->add_component<UnitComponent>();
+  attacker_unit->owner_id = 1;
+  attacker_unit->health = 90;
+
+  auto *attacker_attack = attacker->add_component<AttackComponent>();
+  attacker_attack->damage = 25;
+  attacker_attack->range = 15.0F;
+  attacker_attack->current_mode = AttackComponent::CombatMode::Melee;
+  attacker_attack->in_melee_lock = true;
+  attacker_attack->melee_lock_target_id = defender_id;
+
+  auto *attacker_target = attacker->add_component<AttackTargetComponent>();
+  attacker_target->target_id = defender_id;
+  attacker_target->should_chase = true;
+
+  // Setup defender
+  auto *defender_transform = defender->add_component<TransformComponent>();
+  defender_transform->position.x = 12.0F;
+  defender_transform->position.z = 12.0F;
+
+  auto *defender_unit = defender->add_component<UnitComponent>();
+  defender_unit->owner_id = 2;
+  defender_unit->health = 60;
+
+  auto *defender_attack = defender->add_component<AttackComponent>();
+  defender_attack->damage = 20;
+  defender_attack->in_melee_lock = true;
+  defender_attack->melee_lock_target_id = attacker_id;
+
+  // Serialize and restore
+  QJsonDocument doc = Serialization::serialize_world(world.get());
+  auto restored_world = std::make_unique<World>();
+  Serialization::deserialize_world(restored_world.get(), doc);
+
+  // Verify combat state is preserved
+  auto *restored_attacker = restored_world->get_entity(attacker_id);
+  auto *restored_defender = restored_world->get_entity(defender_id);
+  ASSERT_NE(restored_attacker, nullptr);
+  ASSERT_NE(restored_defender, nullptr);
+
+  auto *restored_attacker_attack =
+      restored_attacker->get_component<AttackComponent>();
+  ASSERT_NE(restored_attacker_attack, nullptr);
+  EXPECT_TRUE(restored_attacker_attack->in_melee_lock);
+  EXPECT_EQ(restored_attacker_attack->melee_lock_target_id, defender_id);
+  EXPECT_EQ(restored_attacker_attack->current_mode,
+            AttackComponent::CombatMode::Melee);
+
+  auto *restored_attacker_target =
+      restored_attacker->get_component<AttackTargetComponent>();
+  ASSERT_NE(restored_attacker_target, nullptr);
+  EXPECT_EQ(restored_attacker_target->target_id, defender_id);
+  EXPECT_TRUE(restored_attacker_target->should_chase);
+
+  auto *restored_defender_attack =
+      restored_defender->get_component<AttackComponent>();
+  ASSERT_NE(restored_defender_attack, nullptr);
+  EXPECT_TRUE(restored_defender_attack->in_melee_lock);
+  EXPECT_EQ(restored_defender_attack->melee_lock_target_id, attacker_id);
+
+  // Verify health is preserved
+  auto *restored_attacker_unit =
+      restored_attacker->get_component<UnitComponent>();
+  auto *restored_defender_unit =
+      restored_defender->get_component<UnitComponent>();
+  EXPECT_EQ(restored_attacker_unit->health, 90);
+  EXPECT_EQ(restored_defender_unit->health, 60);
+}
+
+TEST_F(SerializationTest, NationIdentityPreserved) {
+  // Create units from different nations
+  auto *roman_unit = world->create_entity();
+  auto *carthage_unit = world->create_entity();
+  auto roman_id = roman_unit->get_id();
+  auto carthage_id = carthage_unit->get_id();
+
+  auto *roman_unit_comp = roman_unit->add_component<UnitComponent>();
+  roman_unit_comp->nation_id = Game::Systems::NationID::RomanRepublic;
+  roman_unit_comp->spawn_type = Game::Units::SpawnType::Spearman;
+
+  auto *carthage_unit_comp = carthage_unit->add_component<UnitComponent>();
+  carthage_unit_comp->nation_id = Game::Systems::NationID::Carthage;
+  carthage_unit_comp->spawn_type = Game::Units::SpawnType::Archer;
+
+  // Serialize and restore
+  QJsonDocument doc = Serialization::serialize_world(world.get());
+  auto restored_world = std::make_unique<World>();
+  Serialization::deserialize_world(restored_world.get(), doc);
+
+  // Verify nation IDs are preserved
+  auto *restored_roman = restored_world->get_entity(roman_id);
+  auto *restored_carthage = restored_world->get_entity(carthage_id);
+
+  auto *restored_roman_comp = restored_roman->get_component<UnitComponent>();
+  EXPECT_EQ(restored_roman_comp->nation_id,
+            Game::Systems::NationID::RomanRepublic);
+  EXPECT_EQ(restored_roman_comp->spawn_type, Game::Units::SpawnType::Spearman);
+
+  auto *restored_carthage_comp =
+      restored_carthage->get_component<UnitComponent>();
+  EXPECT_EQ(restored_carthage_comp->nation_id,
+            Game::Systems::NationID::Carthage);
+  EXPECT_EQ(restored_carthage_comp->spawn_type, Game::Units::SpawnType::Archer);
+}

+ 52 - 52
tests/db/save_storage_test.cpp

@@ -35,8 +35,8 @@ TEST_F(SaveStorageTest, SaveSlotBasic) {
   QByteArray screenshot("screenshot_data");
 
   QString error;
-  bool saved = storage->saveSlot(slot_name, title, metadata, world_state,
-                                 screenshot, &error);
+  bool saved = storage->save_slot(slot_name, title, metadata, world_state,
+                                  screenshot, &error);
 
   EXPECT_TRUE(saved) << "Failed to save: " << error.toStdString();
 }
@@ -55,8 +55,8 @@ TEST_F(SaveStorageTest, SaveAndLoadSlot) {
 
   QString error;
   bool saved =
-      storage->saveSlot(slot_name, original_title, original_metadata,
-                        original_world_state, original_screenshot, &error);
+      storage->save_slot(slot_name, original_title, original_metadata,
+                         original_world_state, original_screenshot, &error);
   ASSERT_TRUE(saved) << "Save failed: " << error.toStdString();
 
   QByteArray loaded_world_state;
@@ -65,8 +65,8 @@ TEST_F(SaveStorageTest, SaveAndLoadSlot) {
   QString loaded_title;
 
   bool loaded =
-      storage->loadSlot(slot_name, loaded_world_state, loaded_metadata,
-                        loaded_screenshot, loaded_title, &error);
+      storage->load_slot(slot_name, loaded_world_state, loaded_metadata,
+                         loaded_screenshot, loaded_title, &error);
 
   ASSERT_TRUE(loaded) << "Load failed: " << error.toStdString();
 
@@ -95,12 +95,12 @@ TEST_F(SaveStorageTest, OverwriteExistingSlot) {
 
   QString error;
 
-  bool saved1 = storage->saveSlot(slot_name, title1, metadata1, world_state1,
-                                  QByteArray(), &error);
+  bool saved1 = storage->save_slot(slot_name, title1, metadata1, world_state1,
+                                   QByteArray(), &error);
   ASSERT_TRUE(saved1) << "First save failed: " << error.toStdString();
 
-  bool saved2 = storage->saveSlot(slot_name, title2, metadata2, world_state2,
-                                  QByteArray(), &error);
+  bool saved2 = storage->save_slot(slot_name, title2, metadata2, world_state2,
+                                   QByteArray(), &error);
   ASSERT_TRUE(saved2) << "Second save failed: " << error.toStdString();
 
   QByteArray loaded_world_state;
@@ -109,8 +109,8 @@ TEST_F(SaveStorageTest, OverwriteExistingSlot) {
   QString loaded_title;
 
   bool loaded =
-      storage->loadSlot(slot_name, loaded_world_state, loaded_metadata,
-                        loaded_screenshot, loaded_title, &error);
+      storage->load_slot(slot_name, loaded_world_state, loaded_metadata,
+                         loaded_screenshot, loaded_title, &error);
 
   ASSERT_TRUE(loaded) << "Load failed: " << error.toStdString();
 
@@ -129,8 +129,8 @@ TEST_F(SaveStorageTest, LoadNonExistentSlot) {
   QString error;
 
   bool loaded =
-      storage->loadSlot(slot_name, loaded_world_state, loaded_metadata,
-                        loaded_screenshot, loaded_title, &error);
+      storage->load_slot(slot_name, loaded_world_state, loaded_metadata,
+                         loaded_screenshot, loaded_title, &error);
 
   EXPECT_FALSE(loaded);
   EXPECT_FALSE(error.isEmpty());
@@ -140,14 +140,14 @@ TEST_F(SaveStorageTest, ListSlots) {
   QString error;
 
   QByteArray non_empty_data("test_data");
-  storage->saveSlot("slot1", "Title 1", QJsonObject(), non_empty_data,
-                    QByteArray(), &error);
-  storage->saveSlot("slot2", "Title 2", QJsonObject(), non_empty_data,
-                    QByteArray(), &error);
-  storage->saveSlot("slot3", "Title 3", QJsonObject(), non_empty_data,
-                    QByteArray(), &error);
+  storage->save_slot("slot1", "Title 1", QJsonObject(), non_empty_data,
+                     QByteArray(), &error);
+  storage->save_slot("slot2", "Title 2", QJsonObject(), non_empty_data,
+                     QByteArray(), &error);
+  storage->save_slot("slot3", "Title 3", QJsonObject(), non_empty_data,
+                     QByteArray(), &error);
 
-  QVariantList slot_list = storage->listSlots(&error);
+  QVariantList slot_list = storage->list_slots(&error);
 
   EXPECT_TRUE(error.isEmpty()) << "List failed: " << error.toStdString();
   EXPECT_EQ(slot_list.size(), 3);
@@ -182,16 +182,16 @@ TEST_F(SaveStorageTest, DeleteSlot) {
   QString error;
 
   QByteArray non_empty_data("test_data");
-  storage->saveSlot(slot_name, "Title", QJsonObject(), non_empty_data,
-                    QByteArray(), &error);
+  storage->save_slot(slot_name, "Title", QJsonObject(), non_empty_data,
+                     QByteArray(), &error);
 
-  QVariantList slots_before = storage->listSlots(&error);
+  QVariantList slots_before = storage->list_slots(&error);
   EXPECT_EQ(slots_before.size(), 1);
 
-  bool deleted = storage->deleteSlot(slot_name, &error);
+  bool deleted = storage->delete_slot(slot_name, &error);
   EXPECT_TRUE(deleted) << "Delete failed: " << error.toStdString();
 
-  QVariantList slots_after = storage->listSlots(&error);
+  QVariantList slots_after = storage->list_slots(&error);
   EXPECT_EQ(slots_after.size(), 0);
 }
 
@@ -199,7 +199,7 @@ TEST_F(SaveStorageTest, DeleteNonExistentSlot) {
   QString slot_name = "nonexistent_delete";
   QString error;
 
-  bool deleted = storage->deleteSlot(slot_name, &error);
+  bool deleted = storage->delete_slot(slot_name, &error);
 
   EXPECT_FALSE(deleted);
   EXPECT_FALSE(error.isEmpty());
@@ -210,8 +210,8 @@ TEST_F(SaveStorageTest, EmptyMetadataSave) {
   QJsonObject empty_metadata;
 
   QString error;
-  bool saved = storage->saveSlot(slot_name, "Title", empty_metadata,
-                                 QByteArray("data"), QByteArray(), &error);
+  bool saved = storage->save_slot(slot_name, "Title", empty_metadata,
+                                  QByteArray("data"), QByteArray(), &error);
 
   EXPECT_TRUE(saved) << "Failed to save: " << error.toStdString();
 
@@ -221,8 +221,8 @@ TEST_F(SaveStorageTest, EmptyMetadataSave) {
   QString loaded_title;
 
   bool loaded =
-      storage->loadSlot(slot_name, loaded_world_state, loaded_metadata,
-                        loaded_screenshot, loaded_title, &error);
+      storage->load_slot(slot_name, loaded_world_state, loaded_metadata,
+                         loaded_screenshot, loaded_title, &error);
 
   EXPECT_TRUE(loaded) << "Failed to load: " << error.toStdString();
 }
@@ -232,8 +232,8 @@ TEST_F(SaveStorageTest, EmptyWorldStateSave) {
   QByteArray minimal_world_state(" ");
 
   QString error;
-  bool saved = storage->saveSlot(slot_name, "Title", QJsonObject(),
-                                 minimal_world_state, QByteArray(), &error);
+  bool saved = storage->save_slot(slot_name, "Title", QJsonObject(),
+                                  minimal_world_state, QByteArray(), &error);
 
   EXPECT_TRUE(saved) << "Failed to save: " << error.toStdString();
 
@@ -243,8 +243,8 @@ TEST_F(SaveStorageTest, EmptyWorldStateSave) {
   QString loaded_title;
 
   bool loaded =
-      storage->loadSlot(slot_name, loaded_world_state, loaded_metadata,
-                        loaded_screenshot, loaded_title, &error);
+      storage->load_slot(slot_name, loaded_world_state, loaded_metadata,
+                         loaded_screenshot, loaded_title, &error);
 
   EXPECT_TRUE(loaded) << "Failed to load: " << error.toStdString();
   EXPECT_EQ(loaded_world_state, minimal_world_state);
@@ -260,8 +260,8 @@ TEST_F(SaveStorageTest, LargeDataSave) {
   metadata["size"] = "large";
 
   QString error;
-  bool saved = storage->saveSlot(slot_name, "Large Data Test", metadata,
-                                 large_world_state, large_screenshot, &error);
+  bool saved = storage->save_slot(slot_name, "Large Data Test", metadata,
+                                  large_world_state, large_screenshot, &error);
 
   EXPECT_TRUE(saved) << "Failed to save large data: " << error.toStdString();
 
@@ -271,8 +271,8 @@ TEST_F(SaveStorageTest, LargeDataSave) {
   QString loaded_title;
 
   bool loaded =
-      storage->loadSlot(slot_name, loaded_world_state, loaded_metadata,
-                        loaded_screenshot, loaded_title, &error);
+      storage->load_slot(slot_name, loaded_world_state, loaded_metadata,
+                         loaded_screenshot, loaded_title, &error);
 
   EXPECT_TRUE(loaded) << "Failed to load large data: " << error.toStdString();
   EXPECT_EQ(loaded_world_state.size(), 1024 * 1024);
@@ -287,8 +287,8 @@ TEST_F(SaveStorageTest, SpecialCharactersInSlotName) {
   metadata["description"] = "Test with special characters: <>&\"'";
 
   QString error;
-  bool saved = storage->saveSlot(slot_name, title, metadata, QByteArray("data"),
-                                 QByteArray(), &error);
+  bool saved = storage->save_slot(slot_name, title, metadata,
+                                  QByteArray("data"), QByteArray(), &error);
 
   EXPECT_TRUE(saved) << "Failed to save: " << error.toStdString();
 
@@ -298,8 +298,8 @@ TEST_F(SaveStorageTest, SpecialCharactersInSlotName) {
   QString loaded_title;
 
   bool loaded =
-      storage->loadSlot(slot_name, loaded_world_state, loaded_metadata,
-                        loaded_screenshot, loaded_title, &error);
+      storage->load_slot(slot_name, loaded_world_state, loaded_metadata,
+                         loaded_screenshot, loaded_title, &error);
 
   EXPECT_TRUE(loaded) << "Failed to load: " << error.toStdString();
   EXPECT_EQ(loaded_title, title);
@@ -325,8 +325,8 @@ TEST_F(SaveStorageTest, ComplexMetadataSave) {
   metadata["array"] = array;
 
   QString error;
-  bool saved = storage->saveSlot(slot_name, "Complex Metadata Test", metadata,
-                                 QByteArray("data"), QByteArray(), &error);
+  bool saved = storage->save_slot(slot_name, "Complex Metadata Test", metadata,
+                                  QByteArray("data"), QByteArray(), &error);
 
   EXPECT_TRUE(saved) << "Failed to save: " << error.toStdString();
 
@@ -336,8 +336,8 @@ TEST_F(SaveStorageTest, ComplexMetadataSave) {
   QString loaded_title;
 
   bool loaded =
-      storage->loadSlot(slot_name, loaded_world_state, loaded_metadata,
-                        loaded_screenshot, loaded_title, &error);
+      storage->load_slot(slot_name, loaded_world_state, loaded_metadata,
+                         loaded_screenshot, loaded_title, &error);
 
   EXPECT_TRUE(loaded) << "Failed to load: " << error.toStdString();
 
@@ -361,19 +361,19 @@ TEST_F(SaveStorageTest, MultipleSavesAndDeletes) {
 
   for (int i = 0; i < 10; i++) {
     QString slot_name = QString("slot_%1").arg(i);
-    storage->saveSlot(slot_name, QString("Title %1").arg(i), QJsonObject(),
-                      QByteArray("data"), QByteArray(), &error);
+    storage->save_slot(slot_name, QString("Title %1").arg(i), QJsonObject(),
+                       QByteArray("data"), QByteArray(), &error);
   }
 
-  QVariantList slot_list = storage->listSlots(&error);
+  QVariantList slot_list = storage->list_slots(&error);
   EXPECT_EQ(slot_list.size(), 10);
 
   for (int i = 0; i < 5; i++) {
     QString slot_name = QString("slot_%1").arg(i);
-    storage->deleteSlot(slot_name, &error);
+    storage->delete_slot(slot_name, &error);
   }
 
-  slot_list = storage->listSlots(&error);
+  slot_list = storage->list_slots(&error);
   EXPECT_EQ(slot_list.size(), 5);
 
   for (const QVariant &slot_variant : slot_list) {

+ 174 - 0
ui/qml/MapPreview.qml

@@ -0,0 +1,174 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import StandardOfIron 1.0
+
+Rectangle {
+    id: root
+
+    property var mapPath: ""
+    property var playerConfigs: []
+    property bool loading: false
+    property string previewId: ""
+
+    function refreshPreview() {
+        if (!mapPath || mapPath === "" || !playerConfigs || playerConfigs.length === 0) {
+            previewImage.source = "";
+            previewId = "";
+            return ;
+        }
+        if (typeof game === "undefined" || !game.generate_map_preview)
+            return ;
+
+        loading = true;
+        try {
+            var configStr = JSON.stringify(playerConfigs);
+            var hash = 0;
+            for (var i = 0; i < configStr.length; i++) {
+                var codePoint = configStr.charCodeAt(i);
+                hash = ((hash << 5) - hash) + codePoint;
+                hash = hash & hash;
+            }
+            var newId = mapPath + "_" + hash + "_" + Date.now();
+            var preview = game.generate_map_preview(mapPath, playerConfigs);
+            if (typeof mapPreviewProvider !== "undefined") {
+                mapPreviewProvider.set_preview_image(newId, preview);
+                previewId = newId;
+                previewImage.source = "image://mappreview/" + newId;
+            }
+            loading = false;
+        } catch (e) {
+            console.error("MapPreview: Failed to generate preview:", e);
+            loading = false;
+        }
+    }
+
+    radius: Theme.radiusLarge
+    color: Theme.cardBase
+    border.color: Theme.panelBr
+    border.width: 1
+    clip: true
+    onMapPathChanged: refreshPreview()
+    onPlayerConfigsChanged: refreshPreview()
+
+    Text {
+        id: titleText
+
+        text: qsTr("Map Preview")
+        color: Theme.textMain
+        font.pixelSize: 16
+        font.bold: true
+
+        anchors {
+            top: parent.top
+            left: parent.left
+            margins: Theme.spacingMedium
+        }
+
+    }
+
+    Rectangle {
+        id: previewContainer
+
+        color: Theme.cardBaseB
+        radius: Theme.radiusMedium
+        border.color: Theme.thumbBr
+        border.width: 1
+
+        anchors {
+            top: titleText.bottom
+            left: parent.left
+            right: parent.right
+            bottom: parent.bottom
+            margins: Theme.spacingMedium
+        }
+
+        RowLayout {
+            id: previewRow
+
+            anchors.fill: parent
+            anchors.margins: Theme.spacingSmall
+            spacing: Theme.spacingMedium
+            visible: !loading
+
+            Item {
+                id: imageWrapper
+
+                Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
+                Layout.preferredWidth: Math.min(previewContainer.width * 0.6, previewContainer.height - Theme.spacingLarge)
+                Layout.preferredHeight: Layout.preferredWidth
+
+                Image {
+                    id: previewImage
+
+                    anchors.fill: parent
+                    fillMode: Image.PreserveAspectFit
+                    smooth: true
+                    cache: false
+                    visible: status === Image.Ready
+                }
+
+            }
+
+            Text {
+                id: legendText
+
+                text: qsTr("Player bases shown as colored circles")
+                color: Theme.textSubLite
+                font.pixelSize: 12
+                font.italic: true
+                wrapMode: Text.WordWrap
+                horizontalAlignment: Text.AlignLeft
+                verticalAlignment: Text.AlignVCenter
+                Layout.fillWidth: true
+                Layout.alignment: Qt.AlignVCenter
+                visible: mapPath !== ""
+            }
+
+        }
+
+        Text {
+            anchors.centerIn: parent
+            text: qsTr("Select a map\nto see preview")
+            color: Theme.textHint
+            font.pixelSize: 13
+            horizontalAlignment: Text.AlignHCenter
+            visible: !loading && previewImage.status !== Image.Ready && mapPath === ""
+        }
+
+        Text {
+            anchors.centerIn: parent
+            text: qsTr("No preview available")
+            color: Theme.textHint
+            font.pixelSize: 13
+            horizontalAlignment: Text.AlignHCenter
+            visible: !loading && previewImage.status !== Image.Ready && mapPath !== ""
+        }
+
+        Item {
+            anchors.centerIn: parent
+            visible: loading
+            width: 60
+            height: 60
+
+            Text {
+                anchors.centerIn: parent
+                text: "⟳"
+                font.pixelSize: 36
+                color: Theme.accent
+
+                RotationAnimator on rotation {
+                    from: 0
+                    to: 360
+                    duration: 1500
+                    loops: Animation.Infinite
+                    running: loading
+                }
+
+            }
+
+        }
+
+    }
+
+}

+ 193 - 186
ui/qml/MapSelect.qml

@@ -352,207 +352,212 @@ Item {
                 bottomMargin: Theme.spacingMedium
             }
 
-            Text {
-                id: leftTitle
+            ColumnLayout {
+                anchors.fill: parent
+                anchors.margins: 0
+                spacing: Theme.spacingMedium
 
-                text: qsTr("Maps")
-                color: Theme.textMain
-                font.pixelSize: 20
-                font.bold: true
+                Text {
+                    id: leftTitle
 
-                anchors {
-                    top: parent.top
-                    left: parent.left
-                    right: parent.right
+                    text: qsTr("Maps")
+                    color: Theme.textMain
+                    font.pixelSize: 20
+                    font.bold: true
+                    Layout.fillWidth: true
                 }
 
-            }
+                MapPreview {
+                    id: mapPreviewLeft
 
-            Rectangle {
-                id: listFrame
-
-                color: Qt.rgba(0, 0, 0, 0)
-                radius: Theme.radiusLarge
-                border.color: Theme.panelBr
-                border.width: 1
-                clip: true
-
-                anchors {
-                    top: leftTitle.bottom
-                    left: parent.left
-                    right: parent.right
-                    bottom: parent.bottom
-                    topMargin: Theme.spacingMedium
+                    Layout.fillWidth: true
+                    Layout.preferredHeight: visible ? 240 : 0
+                    visible: selectedMapData !== null
+                    mapPath: selectedMapPath
+                    playerConfigs: getPlayerConfigs()
                 }
 
-                ListView {
-                    id: list
+                Rectangle {
+                    id: listFrame
 
-                    anchors.fill: parent
-                    anchors.margins: Theme.spacingSmall
-                    model: mapsModel
-                    spacing: Theme.spacingSmall
-                    currentIndex: (count > 0 ? 0 : -1)
-                    keyNavigationWraps: false
-                    boundsBehavior: Flickable.StopAtBounds
-                    onCurrentIndexChanged: {
-                        if (currentIndex < 0) {
-                            selectedMapIndex = -1;
-                            selectedMapData = null;
-                            selectedMapPath = "";
-                            playersModel.clear();
-                            return ;
-                        }
-                        selectedMapIndex = currentIndex;
-                        selectedMapData = getMapData(currentIndex);
-                        selectedMapPath = selectedMapData ? (selectedMapData.path || selectedMapData.file || "") : "";
-                        initializePlayers(selectedMapData);
-                    }
+                    color: Qt.rgba(0, 0, 0, 0)
+                    radius: Theme.radiusLarge
+                    border.color: Theme.panelBr
+                    border.width: 1
+                    clip: true
+                    Layout.fillWidth: true
+                    Layout.fillHeight: (!mapsLoading && list.count > 0)
 
-                    delegate: Item {
-                        id: row
+                    ListView {
+                        id: list
 
-                        width: list.width
-                        height: 72
+                        anchors.fill: parent
+                        anchors.margins: Theme.spacingSmall
+                        model: mapsModel
+                        spacing: Theme.spacingSmall
+                        currentIndex: (count > 0 ? 0 : -1)
+                        keyNavigationWraps: false
+                        boundsBehavior: Flickable.StopAtBounds
+                        onCurrentIndexChanged: {
+                            if (currentIndex < 0) {
+                                selectedMapIndex = -1;
+                                selectedMapData = null;
+                                selectedMapPath = "";
+                                playersModel.clear();
+                                return ;
+                            }
+                            selectedMapIndex = currentIndex;
+                            selectedMapData = getMapData(currentIndex);
+                            selectedMapPath = selectedMapData ? (selectedMapData.path || selectedMapData.file || "") : "";
+                            initializePlayers(selectedMapData);
+                        }
 
-                        MouseArea {
-                            id: rowMouse
+                        delegate: Item {
+                            id: row
 
-                            anchors.fill: parent
-                            hoverEnabled: true
-                            acceptedButtons: Qt.LeftButton
-                            cursorShape: Qt.PointingHandCursor
-                            onClicked: list.currentIndex = index
-                            onDoubleClicked: acceptSelection()
-                        }
+                            width: list.width
+                            height: 72
 
-                        Rectangle {
-                            id: card
+                            MouseArea {
+                                id: rowMouse
 
-                            anchors.fill: parent
-                            radius: Theme.radiusLarge
-                            clip: true
-                            color: rowMouse.containsPress ? Theme.hoverBg : (index === list.currentIndex ? Theme.selectedBg : (rowMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.03) : Qt.rgba(0, 0, 0, 0)))
-                            border.width: 1
-                            border.color: (index === list.currentIndex) ? Theme.selectedBr : (rowMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.15) : Theme.thumbBr)
+                                anchors.fill: parent
+                                hoverEnabled: true
+                                acceptedButtons: Qt.LeftButton
+                                cursorShape: Qt.PointingHandCursor
+                                onClicked: list.currentIndex = index
+                                onDoubleClicked: acceptSelection()
+                            }
 
                             Rectangle {
-                                id: thumbWrap
+                                id: card
 
-                                width: 76
-                                height: 54
-                                radius: Theme.radiusMedium
-                                color: Theme.cardBase
-                                border.color: Theme.thumbBr
-                                border.width: 1
+                                anchors.fill: parent
+                                radius: Theme.radiusLarge
                                 clip: true
+                                color: rowMouse.containsPress ? Theme.hoverBg : (index === list.currentIndex ? Theme.selectedBg : (rowMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.03) : Qt.rgba(0, 0, 0, 0)))
+                                border.width: 1
+                                border.color: (index === list.currentIndex) ? Theme.selectedBr : (rowMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.15) : Theme.thumbBr)
 
-                                anchors {
-                                    left: parent.left
-                                    leftMargin: Theme.spacingSmall
-                                    verticalCenter: parent.verticalCenter
-                                }
+                                Rectangle {
+                                    id: thumbWrap
 
-                                Image {
-                                    id: thumbImage
+                                    width: 76
+                                    height: 54
+                                    radius: Theme.radiusMedium
+                                    color: Theme.cardBase
+                                    border.color: Theme.thumbBr
+                                    border.width: 1
+                                    clip: true
 
-                                    anchors.fill: parent
-                                    source: (typeof thumbnail !== "undefined" && thumbnail !== "") ? thumbnail : ""
-                                    asynchronous: true
-                                    fillMode: Image.PreserveAspectCrop
-                                    visible: status === Image.Ready
-                                }
+                                    anchors {
+                                        left: parent.left
+                                        leftMargin: Theme.spacingSmall
+                                        verticalCenter: parent.verticalCenter
+                                    }
 
-                                Rectangle {
-                                    anchors.fill: parent
-                                    color: Theme.cardBase
-                                    visible: !thumbImage.visible
+                                    Image {
+                                        id: thumbImage
 
-                                    Text {
-                                        anchors.centerIn: parent
-                                        text: "🗺"
-                                        font.pixelSize: 24
-                                        color: Theme.textDim
+                                        anchors.fill: parent
+                                        source: (typeof thumbnail !== "undefined" && thumbnail !== "") ? thumbnail : ""
+                                        asynchronous: true
+                                        fillMode: Image.PreserveAspectCrop
+                                        visible: status === Image.Ready
                                     }
 
-                                }
+                                    Rectangle {
+                                        anchors.fill: parent
+                                        color: Theme.cardBase
+                                        visible: !thumbImage.visible
 
-                            }
+                                        Text {
+                                            anchors.centerIn: parent
+                                            text: "🗺"
+                                            font.pixelSize: 24
+                                            color: Theme.textDim
+                                        }
 
-                            Item {
-                                height: 54
+                                    }
 
-                                anchors {
-                                    left: thumbWrap.right
-                                    right: parent.right
-                                    leftMargin: Theme.spacingSmall
-                                    rightMargin: Theme.spacingSmall
-                                    verticalCenter: parent.verticalCenter
                                 }
 
-                                Text {
-                                    id: map_name
-
-                                    text: (typeof name !== "undefined") ? String(name) : (typeof modelData === "string" ? modelData : (modelData && modelData.name ? String(modelData.name) : ""))
-                                    color: (index === list.currentIndex) ? Theme.textMain : Theme.textBright
-                                    font.pixelSize: (index === list.currentIndex) ? 17 : 15
-                                    font.bold: (index === list.currentIndex)
-                                    elide: Text.ElideRight
+                                Item {
+                                    height: 54
 
                                     anchors {
-                                        top: parent.top
-                                        left: parent.left
+                                        left: thumbWrap.right
                                         right: parent.right
+                                        leftMargin: Theme.spacingSmall
+                                        rightMargin: Theme.spacingSmall
+                                        verticalCenter: parent.verticalCenter
                                     }
 
-                                    Behavior on font.pixelSize {
-                                        NumberAnimation {
-                                            duration: Theme.animNormal
+                                    Text {
+                                        id: map_name
+
+                                        text: (typeof name !== "undefined") ? String(name) : (typeof modelData === "string" ? modelData : (modelData && modelData.name ? String(modelData.name) : ""))
+                                        color: (index === list.currentIndex) ? Theme.textMain : Theme.textBright
+                                        font.pixelSize: (index === list.currentIndex) ? 17 : 15
+                                        font.bold: (index === list.currentIndex)
+                                        elide: Text.ElideRight
+
+                                        anchors {
+                                            top: parent.top
+                                            left: parent.left
+                                            right: parent.right
                                         }
 
-                                    }
-
-                                }
+                                        Behavior on font.pixelSize {
+                                            NumberAnimation {
+                                                duration: Theme.animNormal
+                                            }
 
-                                Text {
-                                    text: (typeof description !== "undefined") ? String(description) : (modelData && modelData.description ? String(modelData.description) : "")
-                                    color: (index === list.currentIndex) ? Theme.accentBright : Theme.textSub
-                                    font.pixelSize: 12
-                                    elide: Text.ElideRight
+                                        }
 
-                                    anchors {
-                                        left: parent.left
-                                        right: parent.right
-                                        bottom: parent.bottom
                                     }
 
-                                }
+                                    Text {
+                                        text: (typeof description !== "undefined") ? String(description) : (modelData && modelData.description ? String(modelData.description) : "")
+                                        color: (index === list.currentIndex) ? Theme.accentBright : Theme.textSub
+                                        font.pixelSize: 12
+                                        elide: Text.ElideRight
+
+                                        anchors {
+                                            left: parent.left
+                                            right: parent.right
+                                            bottom: parent.bottom
+                                        }
 
-                                Text {
-                                    text: "›"
-                                    font.pointSize: 18
-                                    color: (index === list.currentIndex) ? Theme.textMain : Theme.textHint
+                                    }
+
+                                    Text {
+                                        text: "›"
+                                        font.pointSize: 18
+                                        color: (index === list.currentIndex) ? Theme.textMain : Theme.textHint
+
+                                        anchors {
+                                            right: parent.right
+                                            rightMargin: 0
+                                            verticalCenter: parent.verticalCenter
+                                        }
 
-                                    anchors {
-                                        right: parent.right
-                                        rightMargin: 0
-                                        verticalCenter: parent.verticalCenter
                                     }
 
                                 }
 
-                            }
+                                Behavior on color {
+                                    ColorAnimation {
+                                        duration: Theme.animNormal
+                                    }
 
-                            Behavior on color {
-                                ColorAnimation {
-                                    duration: Theme.animNormal
                                 }
 
-                            }
+                                Behavior on border.color {
+                                    ColorAnimation {
+                                        duration: Theme.animNormal
+                                    }
 
-                            Behavior on border.color {
-                                ColorAnimation {
-                                    duration: Theme.animNormal
                                 }
 
                             }
@@ -563,50 +568,52 @@ Item {
 
                 }
 
-            }
+                Item {
+                    Layout.fillWidth: true
+                    Layout.fillHeight: visible
+                    visible: list.count === 0 && !mapsLoading
 
-            Item {
-                anchors.fill: parent
-                visible: list.count === 0 && !mapsLoading
+                    Text {
+                        text: qsTr("No maps available")
+                        color: Theme.textSub
+                        font.pixelSize: 14
+                        anchors.centerIn: parent
+                    }
 
-                Text {
-                    text: qsTr("No maps available")
-                    color: Theme.textSub
-                    font.pixelSize: 14
-                    anchors.centerIn: parent
                 }
 
-            }
-
-            Item {
-                anchors.fill: parent
-                visible: mapsLoading
+                Item {
+                    Layout.fillWidth: true
+                    Layout.fillHeight: visible
+                    visible: mapsLoading
 
-                Column {
-                    anchors.centerIn: parent
-                    spacing: Theme.spacingSmall
+                    Column {
+                        anchors.centerIn: parent
+                        spacing: Theme.spacingSmall
 
-                    Text {
-                        text: "⟳"
-                        font.pixelSize: 24
-                        color: Theme.accent
-                        anchors.horizontalCenter: parent.horizontalCenter
+                        Text {
+                            text: "⟳"
+                            font.pixelSize: 24
+                            color: Theme.accent
+                            anchors.horizontalCenter: parent.horizontalCenter
+
+                            RotationAnimator on rotation {
+                                from: 0
+                                to: 360
+                                duration: 1500
+                                loops: Animation.Infinite
+                                running: mapsLoading
+                            }
 
-                        RotationAnimator on rotation {
-                            from: 0
-                            to: 360
-                            duration: 1500
-                            loops: Animation.Infinite
-                            running: mapsLoading
                         }
 
-                    }
+                        Text {
+                            text: qsTr("Loading maps...")
+                            color: Theme.textSub
+                            font.pixelSize: 12
+                            anchors.horizontalCenter: parent.horizontalCenter
+                        }
 
-                    Text {
-                        text: qsTr("Loading maps...")
-                        color: Theme.textSub
-                        font.pixelSize: 12
-                        anchors.horizontalCenter: parent.horizontalCenter
                     }
 
                 }
@@ -844,7 +851,7 @@ Item {
             Rectangle {
                 id: playerConfigPanel
 
-                height: Math.min(280, (playersModel.count * 60) + 90)
+                height: Math.min(240, (playersModel.count * 60) + 90)
                 radius: Theme.radiusLarge
                 color: Theme.cardBaseA
                 border.color: Theme.panelBr
@@ -855,7 +862,7 @@ Item {
                     top: descr.bottom
                     left: parent.left
                     right: parent.right
-                    topMargin: Theme.spacingLarge + 4
+                    topMargin: Theme.spacingMedium
                 }
 
                 Column {

+ 1 - 0
ui/qml/qmldir

@@ -3,3 +3,4 @@ StyledButton 1.0 StyledButton.qml
 ProductionPanel 1.0 ProductionPanel.qml
 SaveGamePanel 1.0 SaveGamePanel.qml
 LoadGamePanel 1.0 LoadGamePanel.qml
+MapPreview 1.0 MapPreview.qml