Browse Source

Add Roman Roads as map asset type analogous to rivers

Co-authored-by: djeada <[email protected]>
copilot-swe-agent[bot] 2 weeks ago
parent
commit
bf7396bcd4

+ 11 - 3
app/core/game_engine.cpp

@@ -96,6 +96,7 @@
 #include "render/ground/pine_renderer.h"
 #include "render/ground/pine_renderer.h"
 #include "render/ground/plant_renderer.h"
 #include "render/ground/plant_renderer.h"
 #include "render/ground/river_renderer.h"
 #include "render/ground/river_renderer.h"
+#include "render/ground/road_renderer.h"
 #include "render/ground/riverbank_renderer.h"
 #include "render/ground/riverbank_renderer.h"
 #include "render/ground/stone_renderer.h"
 #include "render/ground/stone_renderer.h"
 #include "render/ground/terrain_renderer.h"
 #include "render/ground/terrain_renderer.h"
@@ -126,6 +127,7 @@ GameEngine::GameEngine(QObject *parent)
   m_terrain = std::make_unique<Render::GL::TerrainRenderer>();
   m_terrain = std::make_unique<Render::GL::TerrainRenderer>();
   m_biome = std::make_unique<Render::GL::BiomeRenderer>();
   m_biome = std::make_unique<Render::GL::BiomeRenderer>();
   m_river = std::make_unique<Render::GL::RiverRenderer>();
   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_riverbank = std::make_unique<Render::GL::RiverbankRenderer>();
   m_bridge = std::make_unique<Render::GL::BridgeRenderer>();
   m_bridge = std::make_unique<Render::GL::BridgeRenderer>();
   m_fog = std::make_unique<Render::GL::FogRenderer>();
   m_fog = std::make_unique<Render::GL::FogRenderer>();
@@ -135,9 +137,9 @@ GameEngine::GameEngine(QObject *parent)
   m_firecamp = std::make_unique<Render::GL::FireCampRenderer>();
   m_firecamp = std::make_unique<Render::GL::FireCampRenderer>();
 
 
   m_passes = {m_ground.get(),    m_terrain.get(), m_river.get(),
   m_passes = {m_ground.get(),    m_terrain.get(), m_river.get(),
-              m_riverbank.get(), m_bridge.get(),  m_biome.get(),
-              m_stone.get(),     m_plant.get(),   m_pine.get(),
-              m_firecamp.get(),  m_fog.get()};
+              m_road.get(),      m_riverbank.get(), m_bridge.get(),
+              m_biome.get(),     m_stone.get(),   m_plant.get(),
+              m_pine.get(),      m_firecamp.get(), m_fog.get()};
 
 
   std::unique_ptr<Engine::Core::System> arrow_sys =
   std::unique_ptr<Engine::Core::System> arrow_sys =
       std::make_unique<Game::Systems::ArrowSystem>();
       std::make_unique<Game::Systems::ArrowSystem>();
@@ -325,6 +327,7 @@ void GameEngine::cleanupOpenGLResources() {
   m_terrain.reset();
   m_terrain.reset();
   m_biome.reset();
   m_biome.reset();
   m_river.reset();
   m_river.reset();
+  m_road.reset();
   m_riverbank.reset();
   m_riverbank.reset();
   m_bridge.reset();
   m_bridge.reset();
   m_fog.reset();
   m_fog.reset();
@@ -1221,6 +1224,7 @@ void GameEngine::start_skirmish(const QString &map_path,
     loader.setTerrainRenderer(m_terrain.get());
     loader.setTerrainRenderer(m_terrain.get());
     loader.setBiomeRenderer(m_biome.get());
     loader.setBiomeRenderer(m_biome.get());
     loader.setRiverRenderer(m_river.get());
     loader.setRiverRenderer(m_river.get());
+    loader.setRoadRenderer(m_road.get());
     loader.setRiverbankRenderer(m_riverbank.get());
     loader.setRiverbankRenderer(m_riverbank.get());
     loader.setBridgeRenderer(m_bridge.get());
     loader.setBridgeRenderer(m_bridge.get());
     loader.setFogRenderer(m_fog.get());
     loader.setFogRenderer(m_fog.get());
@@ -1815,6 +1819,10 @@ void GameEngine::restoreEnvironmentFromMetadata(const QJsonObject &metadata) {
         m_river->configure(height_map->getRiverSegments(),
         m_river->configure(height_map->getRiverSegments(),
                            height_map->getTileSize());
                            height_map->getTileSize());
       }
       }
+      if (m_road) {
+        m_road->configure(terrain_service.roadSegments(),
+                          height_map->getTileSize());
+      }
       if (m_riverbank) {
       if (m_riverbank) {
         m_riverbank->configure(height_map->getRiverSegments(), *height_map);
         m_riverbank->configure(height_map->getRiverSegments(), *height_map);
       }
       }

+ 2 - 0
app/core/game_engine.h

@@ -39,6 +39,7 @@ class GroundRenderer;
 class TerrainRenderer;
 class TerrainRenderer;
 class BiomeRenderer;
 class BiomeRenderer;
 class RiverRenderer;
 class RiverRenderer;
+class RoadRenderer;
 class RiverbankRenderer;
 class RiverbankRenderer;
 class BridgeRenderer;
 class BridgeRenderer;
 class FogRenderer;
 class FogRenderer;
@@ -278,6 +279,7 @@ private:
   std::unique_ptr<Render::GL::TerrainRenderer> m_terrain;
   std::unique_ptr<Render::GL::TerrainRenderer> m_terrain;
   std::unique_ptr<Render::GL::BiomeRenderer> m_biome;
   std::unique_ptr<Render::GL::BiomeRenderer> m_biome;
   std::unique_ptr<Render::GL::RiverRenderer> m_river;
   std::unique_ptr<Render::GL::RiverRenderer> m_river;
+  std::unique_ptr<Render::GL::RoadRenderer> m_road;
   std::unique_ptr<Render::GL::RiverbankRenderer> m_riverbank;
   std::unique_ptr<Render::GL::RiverbankRenderer> m_riverbank;
   std::unique_ptr<Render::GL::BridgeRenderer> m_bridge;
   std::unique_ptr<Render::GL::BridgeRenderer> m_bridge;
   std::unique_ptr<Render::GL::FogRenderer> m_fog;
   std::unique_ptr<Render::GL::FogRenderer> m_fog;

+ 46 - 4
game/core/serialization.cpp

@@ -457,7 +457,8 @@ void Serialization::deserializeEntity(Entity *entity, const QJsonObject &json) {
 
 
 auto Serialization::serializeTerrain(
 auto Serialization::serializeTerrain(
     const Game::Map::TerrainHeightMap *height_map,
     const Game::Map::TerrainHeightMap *height_map,
-    const Game::Map::BiomeSettings &biome) -> QJsonObject {
+    const Game::Map::BiomeSettings &biome,
+    const std::vector<Game::Map::RoadSegment> &roads) -> QJsonObject {
   QJsonObject terrain_obj;
   QJsonObject terrain_obj;
 
 
   if (height_map == nullptr) {
   if (height_map == nullptr) {
@@ -513,6 +514,21 @@ auto Serialization::serializeTerrain(
   }
   }
   terrain_obj["bridges"] = bridges_array;
   terrain_obj["bridges"] = bridges_array;
 
 
+  QJsonArray roads_array;
+  for (const auto &road : roads) {
+    QJsonObject road_obj;
+    road_obj["startX"] = road.start.x();
+    road_obj["startY"] = road.start.y();
+    road_obj["startZ"] = road.start.z();
+    road_obj["endX"] = road.end.x();
+    road_obj["endY"] = road.end.y();
+    road_obj["endZ"] = road.end.z();
+    road_obj["width"] = road.width;
+    road_obj["style"] = road.style;
+    roads_array.append(road_obj);
+  }
+  terrain_obj["roads"] = roads_array;
+
   QJsonObject biome_obj;
   QJsonObject biome_obj;
   biome_obj["grassPrimaryR"] = biome.grassPrimary.x();
   biome_obj["grassPrimaryR"] = biome.grassPrimary.x();
   biome_obj["grassPrimaryG"] = biome.grassPrimary.y();
   biome_obj["grassPrimaryG"] = biome.grassPrimary.y();
@@ -563,6 +579,7 @@ auto Serialization::serializeTerrain(
 
 
 void Serialization::deserializeTerrain(Game::Map::TerrainHeightMap *height_map,
 void Serialization::deserializeTerrain(Game::Map::TerrainHeightMap *height_map,
                                        Game::Map::BiomeSettings &biome,
                                        Game::Map::BiomeSettings &biome,
+                                       std::vector<Game::Map::RoadSegment> &roads,
                                        const QJsonObject &json) {
                                        const QJsonObject &json) {
   if ((height_map == nullptr) || json.isEmpty()) {
   if ((height_map == nullptr) || json.isEmpty()) {
     return;
     return;
@@ -730,6 +747,29 @@ void Serialization::deserializeTerrain(Game::Map::TerrainHeightMap *height_map,
     }
     }
   }
   }
 
 
+  roads.clear();
+  if (json.contains("roads")) {
+    const auto roads_array = json["roads"].toArray();
+    roads.reserve(roads_array.size());
+    const Game::Map::RoadSegment default_road{};
+    for (const auto &val : roads_array) {
+      const auto road_obj = val.toObject();
+      Game::Map::RoadSegment road;
+      road.start =
+          QVector3D(static_cast<float>(road_obj["startX"].toDouble(0.0)),
+                    static_cast<float>(road_obj["startY"].toDouble(0.0)),
+                    static_cast<float>(road_obj["startZ"].toDouble(0.0)));
+      road.end =
+          QVector3D(static_cast<float>(road_obj["endX"].toDouble(0.0)),
+                    static_cast<float>(road_obj["endY"].toDouble(0.0)),
+                    static_cast<float>(road_obj["endZ"].toDouble(0.0)));
+      road.width = static_cast<float>(road_obj["width"].toDouble(
+          static_cast<double>(default_road.width)));
+      road.style = road_obj["style"].toString(default_road.style);
+      roads.push_back(road);
+    }
+  }
+
   height_map->restoreFromData(heights, terrain_types, rivers, bridges);
   height_map->restoreFromData(heights, terrain_types, rivers, bridges);
 }
 }
 
 
@@ -753,7 +793,8 @@ auto Serialization::serializeWorld(const World *world) -> QJsonDocument {
   if (terrain_service.isInitialized() &&
   if (terrain_service.isInitialized() &&
       (terrain_service.getHeightMap() != nullptr)) {
       (terrain_service.getHeightMap() != nullptr)) {
     world_obj["terrain"] = serializeTerrain(terrain_service.getHeightMap(),
     world_obj["terrain"] = serializeTerrain(terrain_service.getHeightMap(),
-                                            terrain_service.biomeSettings());
+                                            terrain_service.biomeSettings(),
+                                            terrain_service.roadSegments());
   }
   }
 
 
   return QJsonDocument(world_obj);
   return QJsonDocument(world_obj);
@@ -793,6 +834,7 @@ void Serialization::deserializeWorld(World *world, const QJsonDocument &doc) {
         static_cast<float>(terrain_obj["tile_size"].toDouble(1.0));
         static_cast<float>(terrain_obj["tile_size"].toDouble(1.0));
 
 
     Game::Map::BiomeSettings biome;
     Game::Map::BiomeSettings biome;
+    std::vector<Game::Map::RoadSegment> roads;
     std::vector<float> const heights;
     std::vector<float> const heights;
     std::vector<Game::Map::TerrainType> const terrain_types;
     std::vector<Game::Map::TerrainType> const terrain_types;
     std::vector<Game::Map::RiverSegment> const rivers;
     std::vector<Game::Map::RiverSegment> const rivers;
@@ -800,13 +842,13 @@ void Serialization::deserializeWorld(World *world, const QJsonDocument &doc) {
 
 
     auto temp_height_map =
     auto temp_height_map =
         std::make_unique<Game::Map::TerrainHeightMap>(width, height, tile_size);
         std::make_unique<Game::Map::TerrainHeightMap>(width, height, tile_size);
-    deserializeTerrain(temp_height_map.get(), biome, terrain_obj);
+    deserializeTerrain(temp_height_map.get(), biome, roads, terrain_obj);
 
 
     auto &terrain_service = Game::Map::TerrainService::instance();
     auto &terrain_service = Game::Map::TerrainService::instance();
     terrain_service.restoreFromSerialized(
     terrain_service.restoreFromSerialized(
         width, height, tile_size, temp_height_map->getHeightData(),
         width, height, tile_size, temp_height_map->getHeightData(),
         temp_height_map->getTerrainTypes(), temp_height_map->getRiverSegments(),
         temp_height_map->getTerrainTypes(), temp_height_map->getRiverSegments(),
-        temp_height_map->getBridges(), biome);
+        roads, temp_height_map->getBridges(), biome);
   }
   }
 }
 }
 
 

+ 10 - 4
game/core/serialization.h

@@ -3,10 +3,12 @@
 #include <QJsonDocument>
 #include <QJsonDocument>
 #include <QJsonObject>
 #include <QJsonObject>
 #include <QString>
 #include <QString>
+#include <vector>
 
 
 namespace Game::Map {
 namespace Game::Map {
 class TerrainHeightMap;
 class TerrainHeightMap;
 struct BiomeSettings;
 struct BiomeSettings;
+struct RoadSegment;
 } // namespace Game::Map
 } // namespace Game::Map
 
 
 namespace Engine::Core {
 namespace Engine::Core {
@@ -21,10 +23,14 @@ public:
 
 
   static auto
   static auto
   serializeTerrain(const Game::Map::TerrainHeightMap *height_map,
   serializeTerrain(const Game::Map::TerrainHeightMap *height_map,
-                   const Game::Map::BiomeSettings &biome) -> QJsonObject;
-  static void deserializeTerrain(Game::Map::TerrainHeightMap *height_map,
-                                 Game::Map::BiomeSettings &biome,
-                                 const QJsonObject &json);
+                   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,
   static auto saveToFile(const QString &filename,
                          const QJsonDocument &doc) -> bool;
                          const QJsonDocument &doc) -> bool;

+ 3 - 0
game/map/json_keys.h

@@ -13,6 +13,7 @@ inline constexpr const char *SPAWNS = "spawns";
 inline constexpr const char *FIRECAMPS = "firecamps";
 inline constexpr const char *FIRECAMPS = "firecamps";
 inline constexpr const char *TERRAIN = "terrain";
 inline constexpr const char *TERRAIN = "terrain";
 inline constexpr const char *RIVERS = "rivers";
 inline constexpr const char *RIVERS = "rivers";
+inline constexpr const char *ROADS = "roads";
 inline constexpr const char *BRIDGES = "bridges";
 inline constexpr const char *BRIDGES = "bridges";
 inline constexpr const char *VICTORY = "victory";
 inline constexpr const char *VICTORY = "victory";
 inline constexpr const char *THUMBNAIL = "thumbnail";
 inline constexpr const char *THUMBNAIL = "thumbnail";
@@ -95,4 +96,6 @@ inline constexpr const char *START = "start";
 inline constexpr const char *END = "end";
 inline constexpr const char *END = "end";
 inline constexpr const char *BRIDGE_WIDTH = "width";
 inline constexpr const char *BRIDGE_WIDTH = "width";
 
 
+inline constexpr const char *ROAD_STYLE = "style";
+
 } // namespace Game::Map::JsonKeys
 } // namespace Game::Map::JsonKeys

+ 1 - 0
game/map/map_definition.h

@@ -62,6 +62,7 @@ struct MapDefinition {
   std::vector<UnitSpawn> spawns;
   std::vector<UnitSpawn> spawns;
   std::vector<TerrainFeature> terrain;
   std::vector<TerrainFeature> terrain;
   std::vector<RiverSegment> rivers;
   std::vector<RiverSegment> rivers;
+  std::vector<RoadSegment> roads;
   std::vector<Bridge> bridges;
   std::vector<Bridge> bridges;
   std::vector<FireCamp> firecamps;
   std::vector<FireCamp> firecamps;
   BiomeSettings biome;
   BiomeSettings biome;

+ 73 - 0
game/map/map_loader.cpp

@@ -430,6 +430,74 @@ void readRivers(const QJsonArray &arr, std::vector<RiverSegment> &out,
   }
   }
 }
 }
 
 
+void readRoads(const QJsonArray &arr, std::vector<RoadSegment> &out,
+               const GridDefinition &grid, CoordSystem coordSys) {
+  out.clear();
+  out.reserve(arr.size());
+
+  constexpr float grid_center_offset = 0.5F;
+  constexpr float min_tile_size = 0.0001F;
+  constexpr double default_road_width = 3.0;
+
+  for (const auto &road_val : arr) {
+    auto road_obj = road_val.toObject();
+    RoadSegment segment;
+
+    if (road_obj.contains("start") && road_obj.value("start").isArray()) {
+      auto start_arr = road_obj.value("start").toArray();
+      if (start_arr.size() >= 2) {
+        const float start_x = float(start_arr[0].toDouble(0.0));
+        const float start_z = float(start_arr[1].toDouble(0.0));
+
+        if (coordSys == CoordSystem::Grid) {
+          const float tile = std::max(min_tile_size, grid.tile_size);
+          segment.start.setX((start_x - (grid.width * grid_center_offset -
+                                         grid_center_offset)) *
+                             tile);
+          segment.start.setY(0.0F);
+          segment.start.setZ((start_z - (grid.height * grid_center_offset -
+                                         grid_center_offset)) *
+                             tile);
+        } else {
+          segment.start = QVector3D(start_x, 0.0F, start_z);
+        }
+      }
+    }
+
+    if (road_obj.contains("end") && road_obj.value("end").isArray()) {
+      auto end_arr = road_obj.value("end").toArray();
+      if (end_arr.size() >= 2) {
+        const float end_x = float(end_arr[0].toDouble(0.0));
+        const float end_z = float(end_arr[1].toDouble(0.0));
+
+        if (coordSys == CoordSystem::Grid) {
+          const float tile = std::max(min_tile_size, grid.tile_size);
+          segment.end.setX(
+              (end_x - (grid.width * grid_center_offset - grid_center_offset)) *
+              tile);
+          segment.end.setY(0.0F);
+          segment.end.setZ((end_z - (grid.height * grid_center_offset -
+                                     grid_center_offset)) *
+                           tile);
+        } else {
+          segment.end = QVector3D(end_x, 0.0F, end_z);
+        }
+      }
+    }
+
+    if (road_obj.contains("width")) {
+      segment.width =
+          float(road_obj.value("width").toDouble(default_road_width));
+    }
+
+    if (road_obj.contains("style")) {
+      segment.style = road_obj.value("style").toString("default");
+    }
+
+    out.push_back(segment);
+  }
+}
+
 void readBridges(const QJsonArray &arr, std::vector<Bridge> &out,
 void readBridges(const QJsonArray &arr, std::vector<Bridge> &out,
                  const GridDefinition &grid, CoordSystem coordSys) {
                  const GridDefinition &grid, CoordSystem coordSys) {
   out.clear();
   out.clear();
@@ -582,6 +650,11 @@ auto MapLoader::loadFromJsonFile(const QString &path, MapDefinition &outMap,
                outMap.coordSystem);
                outMap.coordSystem);
   }
   }
 
 
+  if (root.contains(ROADS) && root.value(ROADS).isArray()) {
+    readRoads(root.value(ROADS).toArray(), outMap.roads, outMap.grid,
+              outMap.coordSystem);
+  }
+
   if (root.contains(BRIDGES) && root.value(BRIDGES).isArray()) {
   if (root.contains(BRIDGES) && root.value(BRIDGES).isArray()) {
     readBridges(root.value(BRIDGES).toArray(), outMap.bridges, outMap.grid,
     readBridges(root.value(BRIDGES).toArray(), outMap.bridges, outMap.grid,
                 outMap.coordSystem);
                 outMap.coordSystem);

+ 9 - 0
game/map/skirmish_loader.cpp

@@ -23,6 +23,7 @@
 #include "render/ground/pine_renderer.h"
 #include "render/ground/pine_renderer.h"
 #include "render/ground/plant_renderer.h"
 #include "render/ground/plant_renderer.h"
 #include "render/ground/river_renderer.h"
 #include "render/ground/river_renderer.h"
+#include "render/ground/road_renderer.h"
 #include "render/ground/riverbank_renderer.h"
 #include "render/ground/riverbank_renderer.h"
 #include "render/ground/stone_renderer.h"
 #include "render/ground/stone_renderer.h"
 #include "render/ground/terrain_renderer.h"
 #include "render/ground/terrain_renderer.h"
@@ -334,6 +335,14 @@ auto SkirmishLoader::start(const QString &map_path,
     }
     }
   }
   }
 
 
+  if (m_road != nullptr) {
+    if (terrain_service.isInitialized() &&
+        (terrain_service.getHeightMap() != nullptr)) {
+      m_road->configure(terrain_service.roadSegments(),
+                        terrain_service.getHeightMap()->getTileSize());
+    }
+  }
+
   if (m_riverbank != nullptr) {
   if (m_riverbank != nullptr) {
     if (terrain_service.isInitialized() &&
     if (terrain_service.isInitialized() &&
         (terrain_service.getHeightMap() != nullptr)) {
         (terrain_service.getHeightMap() != nullptr)) {

+ 3 - 0
game/map/skirmish_loader.h

@@ -25,6 +25,7 @@ class PlantRenderer;
 class PineRenderer;
 class PineRenderer;
 class FireCampRenderer;
 class FireCampRenderer;
 class RiverRenderer;
 class RiverRenderer;
+class RoadRenderer;
 class RiverbankRenderer;
 class RiverbankRenderer;
 class BridgeRenderer;
 class BridgeRenderer;
 } // namespace Render::GL
 } // namespace Render::GL
@@ -64,6 +65,7 @@ public:
   }
   }
   void setBiomeRenderer(Render::GL::BiomeRenderer *biome) { m_biome = biome; }
   void setBiomeRenderer(Render::GL::BiomeRenderer *biome) { m_biome = biome; }
   void setRiverRenderer(Render::GL::RiverRenderer *river) { m_river = river; }
   void setRiverRenderer(Render::GL::RiverRenderer *river) { m_river = river; }
+  void setRoadRenderer(Render::GL::RoadRenderer *road) { m_road = road; }
   void setRiverbankRenderer(Render::GL::RiverbankRenderer *riverbank) {
   void setRiverbankRenderer(Render::GL::RiverbankRenderer *riverbank) {
     m_riverbank = riverbank;
     m_riverbank = riverbank;
   }
   }
@@ -99,6 +101,7 @@ private:
   Render::GL::TerrainRenderer *m_terrain = nullptr;
   Render::GL::TerrainRenderer *m_terrain = nullptr;
   Render::GL::BiomeRenderer *m_biome = nullptr;
   Render::GL::BiomeRenderer *m_biome = nullptr;
   Render::GL::RiverRenderer *m_river = nullptr;
   Render::GL::RiverRenderer *m_river = nullptr;
+  Render::GL::RoadRenderer *m_road = nullptr;
   Render::GL::RiverbankRenderer *m_riverbank = nullptr;
   Render::GL::RiverbankRenderer *m_riverbank = nullptr;
   Render::GL::BridgeRenderer *m_bridge = nullptr;
   Render::GL::BridgeRenderer *m_bridge = nullptr;
   Render::GL::FogRenderer *m_fog = nullptr;
   Render::GL::FogRenderer *m_fog = nullptr;

+ 7 - 0
game/map/terrain.h

@@ -117,6 +117,13 @@ struct RiverSegment {
   float width = 2.0F;
   float width = 2.0F;
 };
 };
 
 
+struct RoadSegment {
+  QVector3D start;
+  QVector3D end;
+  float width = 3.0F;
+  QString style = QStringLiteral("default");
+};
+
 struct Bridge {
 struct Bridge {
   QVector3D start;
   QVector3D start;
   QVector3D end;
   QVector3D end;

+ 5 - 1
game/map/terrain_service.cpp

@@ -25,12 +25,14 @@ void TerrainService::initialize(const MapDefinition &mapDef) {
   m_biomeSettings = mapDef.biome;
   m_biomeSettings = mapDef.biome;
   m_height_map->applyBiomeVariation(m_biomeSettings);
   m_height_map->applyBiomeVariation(m_biomeSettings);
   m_fire_camps = mapDef.firecamps;
   m_fire_camps = mapDef.firecamps;
+  m_roadSegments = mapDef.roads;
 }
 }
 
 
 void TerrainService::clear() {
 void TerrainService::clear() {
   m_height_map.reset();
   m_height_map.reset();
   m_biomeSettings = BiomeSettings();
   m_biomeSettings = BiomeSettings();
   m_fire_camps.clear();
   m_fire_camps.clear();
+  m_roadSegments.clear();
 }
 }
 
 
 auto TerrainService::getTerrainHeight(float world_x,
 auto TerrainService::getTerrainHeight(float world_x,
@@ -124,11 +126,13 @@ auto TerrainService::getTerrainType(int grid_x,
 void TerrainService::restoreFromSerialized(
 void TerrainService::restoreFromSerialized(
     int width, int height, float tile_size, const std::vector<float> &heights,
     int width, int height, float tile_size, const std::vector<float> &heights,
     const std::vector<TerrainType> &terrain_types,
     const std::vector<TerrainType> &terrain_types,
-    const std::vector<RiverSegment> &rivers, const std::vector<Bridge> &bridges,
+    const std::vector<RiverSegment> &rivers,
+    const std::vector<RoadSegment> &roads, const std::vector<Bridge> &bridges,
     const BiomeSettings &biome) {
     const BiomeSettings &biome) {
   m_height_map = std::make_unique<TerrainHeightMap>(width, height, tile_size);
   m_height_map = std::make_unique<TerrainHeightMap>(width, height, tile_size);
   m_height_map->restoreFromData(heights, terrain_types, rivers, bridges);
   m_height_map->restoreFromData(heights, terrain_types, rivers, bridges);
   m_biomeSettings = biome;
   m_biomeSettings = biome;
+  m_roadSegments = roads;
 }
 }
 
 
 } // namespace Game::Map
 } // namespace Game::Map

+ 6 - 0
game/map/terrain_service.h

@@ -47,6 +47,10 @@ public:
     return m_fire_camps;
     return m_fire_camps;
   }
   }
 
 
+  [[nodiscard]] auto roadSegments() const -> const std::vector<RoadSegment> & {
+    return m_roadSegments;
+  }
+
   [[nodiscard]] auto isInitialized() const -> bool {
   [[nodiscard]] auto isInitialized() const -> bool {
     return m_height_map != nullptr;
     return m_height_map != nullptr;
   }
   }
@@ -55,6 +59,7 @@ public:
                              const std::vector<float> &heights,
                              const std::vector<float> &heights,
                              const std::vector<TerrainType> &terrain_types,
                              const std::vector<TerrainType> &terrain_types,
                              const std::vector<RiverSegment> &rivers,
                              const std::vector<RiverSegment> &rivers,
+                             const std::vector<RoadSegment> &roads,
                              const std::vector<Bridge> &bridges,
                              const std::vector<Bridge> &bridges,
                              const BiomeSettings &biome);
                              const BiomeSettings &biome);
 
 
@@ -68,6 +73,7 @@ private:
   std::unique_ptr<TerrainHeightMap> m_height_map;
   std::unique_ptr<TerrainHeightMap> m_height_map;
   BiomeSettings m_biomeSettings;
   BiomeSettings m_biomeSettings;
   std::vector<FireCamp> m_fire_camps;
   std::vector<FireCamp> m_fire_camps;
+  std::vector<RoadSegment> m_roadSegments;
 };
 };
 
 
 } // namespace Game::Map
 } // namespace Game::Map

+ 1 - 0
render/CMakeLists.txt

@@ -22,6 +22,7 @@ add_library(render_gl STATIC
     ground/fog_renderer.cpp
     ground/fog_renderer.cpp
     ground/terrain_renderer.cpp
     ground/terrain_renderer.cpp
     ground/river_renderer.cpp
     ground/river_renderer.cpp
+    ground/road_renderer.cpp
     ground/riverbank_renderer.cpp
     ground/riverbank_renderer.cpp
     ground/riverbank_asset_renderer.cpp
     ground/riverbank_asset_renderer.cpp
     ground/bridge_renderer.cpp
     ground/bridge_renderer.cpp

+ 235 - 0
render/ground/road_renderer.cpp

@@ -0,0 +1,235 @@
+#include "road_renderer.h"
+#include "../../game/map/visibility_service.h"
+#include "../gl/mesh.h"
+#include "../gl/resources.h"
+#include "../scene_renderer.h"
+#include "ground_utils.h"
+#include "map/terrain.h"
+#include <QVector2D>
+#include <QVector3D>
+#include <algorithm>
+#include <cmath>
+#include <cstddef>
+#include <memory>
+#include <qglobal.h>
+#include <qmatrix4x4.h>
+#include <qvectornd.h>
+#include <vector>
+
+namespace Render::GL {
+
+RoadRenderer::RoadRenderer() = default;
+RoadRenderer::~RoadRenderer() = default;
+
+void RoadRenderer::configure(
+    const std::vector<Game::Map::RoadSegment> &roadSegments, float tile_size) {
+  m_roadSegments = roadSegments;
+  m_tile_size = tile_size;
+  buildMeshes();
+}
+
+void RoadRenderer::buildMeshes() {
+  m_meshes.clear();
+
+  if (m_roadSegments.empty()) {
+    return;
+  }
+
+  auto noise = [](float x, float y) -> float {
+    float const ix = std::floor(x);
+    float const iy = std::floor(y);
+    float fx = x - ix;
+    float fy = y - iy;
+
+    fx = fx * fx * (3.0F - 2.0F * fx);
+    fy = fy * fy * (3.0F - 2.0F * fy);
+
+    float const a = Ground::noise_hash(ix, iy);
+    float const b = Ground::noise_hash(ix + 1.0F, iy);
+    float const c = Ground::noise_hash(ix, iy + 1.0F);
+    float const d = Ground::noise_hash(ix + 1.0F, iy + 1.0F);
+
+    return a * (1.0F - fx) * (1.0F - fy) + b * fx * (1.0F - fy) +
+           c * (1.0F - fx) * fy + d * fx * fy;
+  };
+
+  for (const auto &segment : m_roadSegments) {
+    QVector3D dir = segment.end - segment.start;
+    float const length = dir.length();
+    if (length < 0.01F) {
+      m_meshes.push_back(nullptr);
+      continue;
+    }
+
+    dir.normalize();
+    QVector3D const perpendicular(-dir.z(), 0.0F, dir.x());
+    float const half_width = segment.width * 0.5F;
+
+    int length_steps =
+        static_cast<int>(std::ceil(length / (m_tile_size * 0.5F))) + 1;
+    length_steps = std::max(length_steps, 8);
+
+    std::vector<Vertex> vertices;
+    std::vector<unsigned int> indices;
+
+    for (int i = 0; i < length_steps; ++i) {
+      float const t =
+          static_cast<float>(i) / static_cast<float>(length_steps - 1);
+      QVector3D center_pos = segment.start + dir * (length * t);
+
+      // Roads have subtle edge variation (less than rivers)
+      constexpr float k_edge_noise_freq_1 = 1.5F;
+      constexpr float k_edge_noise_freq_2 = 4.0F;
+
+      float const edge_noise1 = noise(center_pos.x() * k_edge_noise_freq_1,
+                                      center_pos.z() * k_edge_noise_freq_1);
+      float const edge_noise2 = noise(center_pos.x() * k_edge_noise_freq_2,
+                                      center_pos.z() * k_edge_noise_freq_2);
+
+      float combined_noise = edge_noise1 * 0.6F + edge_noise2 * 0.4F;
+      combined_noise = (combined_noise - 0.5F) * 2.0F;
+
+      // Roads have less width variation than rivers
+      float const width_variation = combined_noise * half_width * 0.15F;
+
+      // Slight Y offset for road surface above ground
+      constexpr float road_y_offset = 0.02F;
+
+      QVector3D const left =
+          center_pos - perpendicular * (half_width + width_variation);
+      QVector3D const right =
+          center_pos + perpendicular * (half_width + width_variation);
+
+      float const normal[3] = {0.0F, 1.0F, 0.0F};
+
+      Vertex left_vertex;
+      left_vertex.position[0] = left.x();
+      left_vertex.position[1] = left.y() + road_y_offset;
+      left_vertex.position[2] = left.z();
+      left_vertex.normal[0] = normal[0];
+      left_vertex.normal[1] = normal[1];
+      left_vertex.normal[2] = normal[2];
+      left_vertex.tex_coord[0] = 0.0F;
+      left_vertex.tex_coord[1] = t;
+      vertices.push_back(left_vertex);
+
+      Vertex right_vertex;
+      right_vertex.position[0] = right.x();
+      right_vertex.position[1] = right.y() + road_y_offset;
+      right_vertex.position[2] = right.z();
+      right_vertex.normal[0] = normal[0];
+      right_vertex.normal[1] = normal[1];
+      right_vertex.normal[2] = normal[2];
+      right_vertex.tex_coord[0] = 1.0F;
+      right_vertex.tex_coord[1] = t;
+      vertices.push_back(right_vertex);
+
+      if (i < length_steps - 1) {
+        unsigned int const idx0 = i * 2;
+        unsigned int const idx1 = idx0 + 1;
+        unsigned int const idx2 = idx0 + 2;
+        unsigned int const idx3 = idx0 + 3;
+
+        indices.push_back(idx0);
+        indices.push_back(idx2);
+        indices.push_back(idx1);
+
+        indices.push_back(idx1);
+        indices.push_back(idx2);
+        indices.push_back(idx3);
+      }
+    }
+
+    if (!vertices.empty() && !indices.empty()) {
+      m_meshes.push_back(std::make_unique<Mesh>(vertices, indices));
+    } else {
+      m_meshes.push_back(nullptr);
+    }
+  }
+}
+
+void RoadRenderer::submit(Renderer &renderer, ResourceManager *resources) {
+  if (m_meshes.empty() || m_roadSegments.empty()) {
+    return;
+  }
+
+  Q_UNUSED(resources);
+
+  auto &visibility = Game::Map::VisibilityService::instance();
+  const bool use_visibility = visibility.isInitialized();
+
+  auto *shader = renderer.getShader("road");
+  if (shader == nullptr) {
+    // Fallback to a basic shader if road shader not found
+    shader = renderer.getShader("terrain");
+    if (shader == nullptr) {
+      return;
+    }
+  }
+
+  renderer.setCurrentShader(shader);
+
+  QMatrix4x4 model;
+  model.setToIdentity();
+
+  // Road stone/gravel color (brownish-gray like Roman roads)
+  QVector3D const road_base_color(0.45F, 0.42F, 0.38F);
+
+  size_t mesh_index = 0;
+  for (const auto &segment : m_roadSegments) {
+    if (mesh_index >= m_meshes.size()) {
+      break;
+    }
+
+    auto *mesh = m_meshes[mesh_index].get();
+    ++mesh_index;
+
+    if (mesh == nullptr) {
+      continue;
+    }
+
+    QVector3D dir = segment.end - segment.start;
+    float const length = dir.length();
+
+    float alpha = 1.0F;
+    QVector3D color_multiplier(1.0F, 1.0F, 1.0F);
+
+    if (use_visibility) {
+      int max_visibility_state = 0;
+      dir.normalize();
+
+      int const samples_per_segment = 5;
+      for (int i = 0; i < samples_per_segment; ++i) {
+        float const t =
+            static_cast<float>(i) / static_cast<float>(samples_per_segment - 1);
+        QVector3D const pos = segment.start + dir * (length * t);
+
+        if (visibility.isVisibleWorld(pos.x(), pos.z())) {
+          max_visibility_state = 2;
+          break;
+        }
+        if (visibility.isExploredWorld(pos.x(), pos.z())) {
+          max_visibility_state = std::max(max_visibility_state, 1);
+        }
+      }
+
+      if (max_visibility_state == 0) {
+        continue;
+      }
+      if (max_visibility_state == 1) {
+        alpha = 0.5F;
+        color_multiplier = QVector3D(0.4F, 0.4F, 0.45F);
+      }
+    }
+
+    QVector3D const final_color(road_base_color.x() * color_multiplier.x(),
+                                road_base_color.y() * color_multiplier.y(),
+                                road_base_color.z() * color_multiplier.z());
+
+    renderer.mesh(mesh, model, final_color, nullptr, alpha);
+  }
+
+  renderer.setCurrentShader(nullptr);
+}
+
+} // namespace Render::GL

+ 32 - 0
render/ground/road_renderer.h

@@ -0,0 +1,32 @@
+#pragma once
+
+#include "../../game/map/terrain.h"
+#include "../i_render_pass.h"
+#include <QMatrix4x4>
+#include <memory>
+#include <vector>
+
+namespace Render::GL {
+class Mesh;
+class Renderer;
+class ResourceManager;
+
+class RoadRenderer : public IRenderPass {
+public:
+  RoadRenderer();
+  ~RoadRenderer() override;
+
+  void configure(const std::vector<Game::Map::RoadSegment> &roadSegments,
+                 float tile_size);
+
+  void submit(Renderer &renderer, ResourceManager *resources) override;
+
+private:
+  void buildMeshes();
+
+  std::vector<Game::Map::RoadSegment> m_roadSegments;
+  float m_tile_size = 1.0F;
+  std::vector<std::unique_ptr<Mesh>> m_meshes;
+};
+
+} // namespace Render::GL