minimap_generator.cpp 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  1. #include "minimap_generator.h"
  2. #include "minimap_utils.h"
  3. #include <QColor>
  4. #include <QLinearGradient>
  5. #include <QPainter>
  6. #include <QPainterPath>
  7. #include <QPen>
  8. #include <QRadialGradient>
  9. #include <algorithm>
  10. #include <cmath>
  11. #include <random>
  12. namespace Game::Map::Minimap {
  13. namespace {
  14. namespace Palette {
  15. constexpr QColor PARCHMENT_BASE{235, 220, 190};
  16. constexpr QColor PARCHMENT_LIGHT{245, 235, 215};
  17. constexpr QColor PARCHMENT_DARK{200, 180, 150};
  18. constexpr QColor PARCHMENT_STAIN{180, 160, 130, 40};
  19. constexpr QColor INK_DARK{45, 35, 25};
  20. constexpr QColor INK_MEDIUM{80, 65, 50};
  21. constexpr QColor INK_LIGHT{120, 100, 80};
  22. constexpr QColor MOUNTAIN_SHADOW{95, 80, 65};
  23. constexpr QColor MOUNTAIN_FACE{140, 125, 105};
  24. constexpr QColor MOUNTAIN_HIGHLIGHT{180, 165, 145};
  25. constexpr QColor HILL_BASE{160, 145, 120};
  26. constexpr QColor WATER_DARK{55, 95, 130};
  27. constexpr QColor WATER_MAIN{75, 120, 160};
  28. constexpr QColor WATER_LIGHT{100, 145, 180};
  29. constexpr QColor FOREST_BASE{100, 130, 90};
  30. constexpr QColor ROAD_MAIN{130, 105, 75};
  31. constexpr QColor ROAD_HIGHLIGHT{165, 140, 110};
  32. constexpr QColor STRUCTURE_STONE{160, 150, 135};
  33. constexpr QColor STRUCTURE_SHADOW{100, 85, 70};
  34. constexpr QColor TEAM_BLUE{65, 105, 165};
  35. constexpr QColor TEAM_BLUE_DARK{40, 65, 100};
  36. constexpr QColor TEAM_RED{175, 65, 55};
  37. constexpr QColor TEAM_RED_DARK{110, 40, 35};
  38. } // namespace Palette
  39. auto hash_coords(int x, int y, int seed = 0) -> float {
  40. const int n = x + y * 57 + seed * 131;
  41. const int shifted = (n << 13) ^ n;
  42. return 1.0F -
  43. static_cast<float>(
  44. (shifted * (shifted * shifted * 15731 + 789221) + 1376312589) &
  45. 0x7fffffff) /
  46. 1073741824.0F;
  47. }
  48. } // namespace
  49. MinimapGenerator::MinimapGenerator() : m_config() {}
  50. MinimapGenerator::MinimapGenerator(const Config &config) : m_config(config) {}
  51. auto MinimapGenerator::generate(const MapDefinition &map_def) -> QImage {
  52. const int img_width =
  53. static_cast<int>(map_def.grid.width * m_config.pixels_per_tile);
  54. const int img_height =
  55. static_cast<int>(map_def.grid.height * m_config.pixels_per_tile);
  56. QImage image(img_width, img_height, QImage::Format_RGBA8888);
  57. image.fill(Palette::PARCHMENT_BASE);
  58. render_parchment_background(image);
  59. render_terrain_base(image, map_def);
  60. render_terrain_features(image, map_def);
  61. render_rivers(image, map_def);
  62. render_roads(image, map_def);
  63. render_bridges(image, map_def);
  64. render_structures(image, map_def);
  65. apply_historical_styling(image);
  66. return image;
  67. }
  68. auto MinimapGenerator::world_to_pixel(float world_x, float world_z,
  69. const GridDefinition &grid) const
  70. -> std::pair<float, float> {
  71. const auto &orient = MinimapOrientation::instance();
  72. const float rotated_x =
  73. world_x * orient.cos_yaw() - world_z * orient.sin_yaw();
  74. const float rotated_z =
  75. world_x * orient.sin_yaw() + world_z * orient.cos_yaw();
  76. const float world_width = grid.width * grid.tile_size;
  77. const float world_height = grid.height * grid.tile_size;
  78. const float img_width = grid.width * m_config.pixels_per_tile;
  79. const float img_height = grid.height * m_config.pixels_per_tile;
  80. const float px = (rotated_x + world_width * 0.5F) * (img_width / world_width);
  81. const float py =
  82. (rotated_z + world_height * 0.5F) * (img_height / world_height);
  83. return {px, py};
  84. }
  85. auto MinimapGenerator::world_to_pixel_size(
  86. float world_size, const GridDefinition &grid) const -> float {
  87. return (world_size / grid.tile_size) * m_config.pixels_per_tile;
  88. }
  89. void MinimapGenerator::render_parchment_background(QImage &image) {
  90. const int BASE_R = Palette::PARCHMENT_BASE.red();
  91. const int BASE_G = Palette::PARCHMENT_BASE.green();
  92. const int BASE_B = Palette::PARCHMENT_BASE.blue();
  93. for (int y = 0; y < image.height(); ++y) {
  94. auto *scanline = reinterpret_cast<uint32_t *>(image.scanLine(y));
  95. for (int x = 0; x < image.width(); ++x) {
  96. const float noise = hash_coords(x / 3, y / 3, 42) * 0.08F;
  97. const int r = std::clamp(BASE_R + static_cast<int>(noise * 20), 0, 255);
  98. const int g = std::clamp(BASE_G + static_cast<int>(noise * 18), 0, 255);
  99. const int b = std::clamp(BASE_B + static_cast<int>(noise * 15), 0, 255);
  100. scanline[x] = qRgba(r, g, b, 255);
  101. }
  102. }
  103. QPainter painter(&image);
  104. painter.setRenderHint(QPainter::Antialiasing, true);
  105. std::mt19937 rng(12345);
  106. std::uniform_real_distribution<float> dist_x(
  107. 0.0F, static_cast<float>(image.width()));
  108. std::uniform_real_distribution<float> dist_y(
  109. 0.0F, static_cast<float>(image.height()));
  110. std::uniform_real_distribution<float> dist_size(5.0F, 25.0F);
  111. std::uniform_real_distribution<float> dist_alpha(0.02F, 0.06F);
  112. const int num_stains = (image.width() * image.height()) / 8000;
  113. for (int i = 0; i < num_stains; ++i) {
  114. const float cx = dist_x(rng);
  115. const float cy = dist_y(rng);
  116. const float radius = dist_size(rng);
  117. const float alpha = dist_alpha(rng);
  118. QRadialGradient stain(cx, cy, radius);
  119. QColor stain_color = Palette::PARCHMENT_STAIN;
  120. stain_color.setAlphaF(static_cast<double>(alpha));
  121. stain.setColorAt(0, stain_color);
  122. stain.setColorAt(1, Qt::transparent);
  123. painter.setBrush(stain);
  124. painter.setPen(Qt::NoPen);
  125. painter.drawEllipse(QPointF(cx, cy), radius, radius);
  126. }
  127. }
  128. void MinimapGenerator::render_terrain_base(QImage &image,
  129. const MapDefinition &map_def) {
  130. QPainter painter(&image);
  131. painter.setRenderHint(QPainter::Antialiasing, true);
  132. const QColor biome_color = biome_to_base_color(map_def.biome);
  133. painter.setCompositionMode(QPainter::CompositionMode_Multiply);
  134. painter.setOpacity(0.15);
  135. painter.fillRect(image.rect(), biome_color);
  136. painter.setOpacity(1.0);
  137. painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
  138. }
  139. void MinimapGenerator::render_terrain_features(QImage &image,
  140. const MapDefinition &map_def) {
  141. QPainter painter(&image);
  142. painter.setRenderHint(QPainter::Antialiasing, true);
  143. auto grid_to_world = [&](float grid_x,
  144. float grid_z) -> std::pair<float, float> {
  145. const float half_w = map_def.grid.width * 0.5F - 0.5F;
  146. const float half_h = map_def.grid.height * 0.5F - 0.5F;
  147. const float world_x = (grid_x - half_w) * map_def.grid.tile_size;
  148. const float world_z = (grid_z - half_h) * map_def.grid.tile_size;
  149. return {world_x, world_z};
  150. };
  151. for (const auto &feature : map_def.terrain) {
  152. const auto [px, py] =
  153. world_to_pixel(feature.center_x, feature.center_z, map_def.grid);
  154. float pixel_width = world_to_pixel_size(feature.width, map_def.grid);
  155. float pixel_depth = world_to_pixel_size(feature.depth, map_def.grid);
  156. constexpr float MIN_FEATURE_SIZE = 4.0F;
  157. pixel_width = std::max(pixel_width, MIN_FEATURE_SIZE);
  158. pixel_depth = std::max(pixel_depth, MIN_FEATURE_SIZE);
  159. if (feature.type == TerrainType::Mountain) {
  160. draw_mountain_symbol(painter, px, py, pixel_width, pixel_depth);
  161. } else if (feature.type == TerrainType::Hill) {
  162. draw_hill_symbol(painter, px, py, pixel_width, pixel_depth);
  163. if (!feature.entrances.empty()) {
  164. painter.setBrush(QColor(200, 40, 40));
  165. painter.setPen(QPen(QColor(80, 15, 15), 1.0));
  166. const float radius = std::max(2.0F, m_config.pixels_per_tile * 0.6F);
  167. for (const auto &entrance : feature.entrances) {
  168. float ex = entrance.x();
  169. float ez = entrance.z();
  170. if (map_def.coordSystem == CoordSystem::Grid) {
  171. const auto [wx, wz] = grid_to_world(ex, ez);
  172. ex = wx;
  173. ez = wz;
  174. }
  175. const auto [epx, epy] = world_to_pixel(ex, ez, map_def.grid);
  176. painter.drawEllipse(QPointF(epx, epy), radius, radius);
  177. }
  178. }
  179. } else if (feature.type == TerrainType::Forest) {
  180. draw_forest_symbol(painter, px, py, pixel_width, pixel_depth);
  181. } else if (feature.type == TerrainType::River) {
  182. constexpr QColor WATER_DARK{55, 95, 130};
  183. constexpr QColor WATER_MAIN{75, 120, 160};
  184. painter.setBrush(WATER_MAIN);
  185. painter.setPen(QPen(WATER_DARK, 1.0));
  186. const float half_w = pixel_width * 0.5F;
  187. const float half_h = pixel_depth * 0.5F;
  188. painter.drawEllipse(QPointF(px, py), half_w, half_h);
  189. }
  190. }
  191. }
  192. void MinimapGenerator::draw_mountain_symbol(QPainter &painter, float cx,
  193. float cy, float width,
  194. float height) {
  195. const float peak_height = height * 0.6F;
  196. const float base_width = width * 0.5F;
  197. QPainterPath shadow_path;
  198. shadow_path.moveTo(cx, cy - peak_height);
  199. shadow_path.lineTo(cx - base_width, cy + height * 0.3F);
  200. shadow_path.lineTo(cx, cy + height * 0.1F);
  201. shadow_path.closeSubpath();
  202. painter.setBrush(Palette::MOUNTAIN_SHADOW);
  203. painter.setPen(Qt::NoPen);
  204. painter.drawPath(shadow_path);
  205. QPainterPath lit_path;
  206. lit_path.moveTo(cx, cy - peak_height);
  207. lit_path.lineTo(cx + base_width, cy + height * 0.3F);
  208. lit_path.lineTo(cx, cy + height * 0.1F);
  209. lit_path.closeSubpath();
  210. painter.setBrush(Palette::MOUNTAIN_FACE);
  211. painter.drawPath(lit_path);
  212. QPainterPath snow_path;
  213. snow_path.moveTo(cx, cy - peak_height);
  214. snow_path.lineTo(cx - base_width * 0.3F, cy - peak_height * 0.5F);
  215. snow_path.lineTo(cx + base_width * 0.2F, cy - peak_height * 0.6F);
  216. snow_path.closeSubpath();
  217. painter.setBrush(Palette::MOUNTAIN_HIGHLIGHT);
  218. painter.drawPath(snow_path);
  219. painter.setBrush(Qt::NoBrush);
  220. painter.setPen(QPen(Palette::INK_MEDIUM, 0.8));
  221. QPainterPath outline;
  222. outline.moveTo(cx - base_width, cy + height * 0.3F);
  223. outline.lineTo(cx, cy - peak_height);
  224. outline.lineTo(cx + base_width, cy + height * 0.3F);
  225. painter.drawPath(outline);
  226. }
  227. void MinimapGenerator::draw_hill_symbol(QPainter &painter, float cx, float cy,
  228. float width, float height) {
  229. const float hill_height = height * 0.35F;
  230. const float base_width = width * 0.6F;
  231. QPainterPath hill_path;
  232. hill_path.moveTo(cx - base_width, cy + hill_height * 0.2F);
  233. hill_path.quadTo(cx - base_width * 0.3F, cy - hill_height, cx,
  234. cy - hill_height);
  235. hill_path.quadTo(cx + base_width * 0.3F, cy - hill_height, cx + base_width,
  236. cy + hill_height * 0.2F);
  237. hill_path.closeSubpath();
  238. QLinearGradient gradient(cx - base_width, cy, cx + base_width, cy);
  239. gradient.setColorAt(0.0, Palette::MOUNTAIN_SHADOW);
  240. gradient.setColorAt(0.4, Palette::HILL_BASE);
  241. gradient.setColorAt(1.0, Palette::MOUNTAIN_FACE);
  242. painter.setBrush(gradient);
  243. painter.setPen(QPen(Palette::INK_LIGHT, 0.6));
  244. painter.drawPath(hill_path);
  245. }
  246. void MinimapGenerator::draw_forest_symbol(QPainter &painter, float cx, float cy,
  247. float width, float height) {
  248. constexpr int JITTER_SEED_X = 123;
  249. constexpr int JITTER_SEED_Y = 456;
  250. const float tree_size = std::min(width, height) * 0.35F;
  251. const float spacing = tree_size * 1.2F;
  252. const int cols = std::max(2, static_cast<int>(width / spacing));
  253. const int rows = std::max(2, static_cast<int>(height / spacing));
  254. const float start_x = cx - (cols - 1) * spacing * 0.5F;
  255. const float start_y = cy - (rows - 1) * spacing * 0.5F;
  256. painter.setBrush(Palette::FOREST_BASE);
  257. painter.setPen(QPen(Palette::INK_LIGHT, 0.5));
  258. for (int row = 0; row < rows; ++row) {
  259. for (int col = 0; col < cols; ++col) {
  260. const float jitter_x =
  261. (hash_coords(col + static_cast<int>(cx), row + static_cast<int>(cy),
  262. JITTER_SEED_X) *
  263. 0.3F) *
  264. tree_size;
  265. const float jitter_y =
  266. (hash_coords(row + static_cast<int>(cx), col + static_cast<int>(cy),
  267. JITTER_SEED_Y) *
  268. 0.3F) *
  269. tree_size;
  270. const float tx = start_x + col * spacing + jitter_x;
  271. const float ty = start_y + row * spacing + jitter_y;
  272. const float tree_h = tree_size * 0.8F;
  273. const float tree_w = tree_size * 0.5F;
  274. QPainterPath tree_path;
  275. tree_path.moveTo(tx, ty - tree_h);
  276. tree_path.lineTo(tx - tree_w, ty);
  277. tree_path.lineTo(tx + tree_w, ty);
  278. tree_path.closeSubpath();
  279. painter.drawPath(tree_path);
  280. }
  281. }
  282. }
  283. void MinimapGenerator::render_rivers(QImage &image,
  284. const MapDefinition &map_def) {
  285. if (map_def.rivers.empty()) {
  286. return;
  287. }
  288. QPainter painter(&image);
  289. painter.setRenderHint(QPainter::Antialiasing, true);
  290. for (const auto &river : map_def.rivers) {
  291. const auto [x1, y1] =
  292. world_to_pixel(river.start.x(), river.start.z(), map_def.grid);
  293. const auto [x2, y2] =
  294. world_to_pixel(river.end.x(), river.end.z(), map_def.grid);
  295. float pixel_width = world_to_pixel_size(river.width, map_def.grid);
  296. pixel_width = std::max(pixel_width, 1.5F);
  297. draw_river_segment(painter, x1, y1, x2, y2, pixel_width);
  298. }
  299. }
  300. void MinimapGenerator::draw_river_segment(QPainter &painter, float x1, float y1,
  301. float x2, float y2, float width) {
  302. QPainterPath river_path;
  303. river_path.moveTo(x1, y1);
  304. const float dx = x2 - x1;
  305. const float dy = y2 - y1;
  306. const float length = std::sqrt(dx * dx + dy * dy);
  307. if (length > 10.0F) {
  308. const float mid_x = (x1 + x2) * 0.5F;
  309. const float mid_y = (y1 + y2) * 0.5F;
  310. const float perp_x = -dy / length;
  311. const float perp_y = dx / length;
  312. const float wave_amount =
  313. hash_coords(static_cast<int>(x1), static_cast<int>(y1)) * width * 0.5F;
  314. river_path.quadTo(mid_x + perp_x * wave_amount,
  315. mid_y + perp_y * wave_amount, x2, y2);
  316. } else {
  317. river_path.lineTo(x2, y2);
  318. }
  319. painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
  320. QPen outline_pen(Palette::WATER_DARK);
  321. outline_pen.setWidthF(width * 1.4F);
  322. outline_pen.setCapStyle(Qt::RoundCap);
  323. outline_pen.setJoinStyle(Qt::RoundJoin);
  324. painter.setPen(outline_pen);
  325. painter.setBrush(Qt::NoBrush);
  326. painter.drawPath(river_path);
  327. QPen main_pen(Palette::WATER_MAIN);
  328. main_pen.setWidthF(width);
  329. main_pen.setCapStyle(Qt::RoundCap);
  330. main_pen.setJoinStyle(Qt::RoundJoin);
  331. painter.setPen(main_pen);
  332. painter.drawPath(river_path);
  333. if (width > 2.0F) {
  334. QPen highlight_pen(Palette::WATER_LIGHT);
  335. highlight_pen.setWidthF(width * 0.4F);
  336. highlight_pen.setCapStyle(Qt::RoundCap);
  337. highlight_pen.setJoinStyle(Qt::RoundJoin);
  338. painter.setPen(highlight_pen);
  339. painter.drawPath(river_path);
  340. }
  341. }
  342. void MinimapGenerator::render_roads(QImage &image,
  343. const MapDefinition &map_def) {
  344. if (map_def.roads.empty()) {
  345. return;
  346. }
  347. QPainter painter(&image);
  348. painter.setRenderHint(QPainter::Antialiasing, true);
  349. for (const auto &road : map_def.roads) {
  350. const auto [x1, y1] =
  351. world_to_pixel(road.start.x(), road.start.z(), map_def.grid);
  352. const auto [x2, y2] =
  353. world_to_pixel(road.end.x(), road.end.z(), map_def.grid);
  354. float pixel_width = world_to_pixel_size(road.width, map_def.grid);
  355. pixel_width = std::max(pixel_width, 1.5F);
  356. draw_road_segment(painter, x1, y1, x2, y2, pixel_width);
  357. }
  358. }
  359. void MinimapGenerator::draw_road_segment(QPainter &painter, float x1, float y1,
  360. float x2, float y2, float width) {
  361. QPen road_pen(Palette::ROAD_MAIN);
  362. road_pen.setWidthF(width);
  363. road_pen.setCapStyle(Qt::RoundCap);
  364. QVector<qreal> dash_pattern;
  365. dash_pattern << 3.0 << 2.0;
  366. road_pen.setDashPattern(dash_pattern);
  367. painter.setPen(road_pen);
  368. painter.drawLine(QPointF(x1, y1), QPointF(x2, y2));
  369. const float dx = x2 - x1;
  370. const float dy = y2 - y1;
  371. const float length = std::sqrt(dx * dx + dy * dy);
  372. if (length > 8.0F) {
  373. painter.setPen(Qt::NoPen);
  374. painter.setBrush(Palette::ROAD_HIGHLIGHT);
  375. const int num_dots = static_cast<int>(length / 6.0F);
  376. for (int i = 1; i < num_dots; ++i) {
  377. const float t = static_cast<float>(i) / static_cast<float>(num_dots);
  378. const float dot_x = x1 + dx * t;
  379. const float dot_y = y1 + dy * t;
  380. painter.drawEllipse(QPointF(dot_x, dot_y), width * 0.25F, width * 0.25F);
  381. }
  382. }
  383. }
  384. void MinimapGenerator::render_bridges(QImage &image,
  385. const MapDefinition &map_def) {
  386. if (map_def.bridges.empty()) {
  387. return;
  388. }
  389. QPainter painter(&image);
  390. painter.setRenderHint(QPainter::Antialiasing, true);
  391. for (const auto &bridge : map_def.bridges) {
  392. const auto [x1, y1] =
  393. world_to_pixel(bridge.start.x(), bridge.start.z(), map_def.grid);
  394. const auto [x2, y2] =
  395. world_to_pixel(bridge.end.x(), bridge.end.z(), map_def.grid);
  396. float pixel_width = world_to_pixel_size(bridge.width, map_def.grid);
  397. pixel_width = std::max(pixel_width, 2.0F);
  398. painter.setPen(QPen(Palette::INK_DARK, 1.0));
  399. painter.setBrush(Palette::STRUCTURE_STONE);
  400. const float dx = x2 - x1;
  401. const float dy = y2 - y1;
  402. const float length = std::sqrt(dx * dx + dy * dy);
  403. if (length > 0.01F) {
  404. const float perp_x = -dy / length * pixel_width * 0.5F;
  405. const float perp_y = dx / length * pixel_width * 0.5F;
  406. QPolygonF bridge_poly;
  407. bridge_poly << QPointF(x1 - perp_x, y1 - perp_y)
  408. << QPointF(x1 + perp_x, y1 + perp_y)
  409. << QPointF(x2 + perp_x, y2 + perp_y)
  410. << QPointF(x2 - perp_x, y2 - perp_y);
  411. painter.drawPolygon(bridge_poly);
  412. painter.setPen(QPen(Palette::INK_LIGHT, 0.5));
  413. const int num_planks = static_cast<int>(length / 3.0F);
  414. for (int i = 1; i < num_planks; ++i) {
  415. const float t = static_cast<float>(i) / static_cast<float>(num_planks);
  416. const float plank_x = x1 + dx * t;
  417. const float plank_y = y1 + dy * t;
  418. painter.drawLine(QPointF(plank_x - perp_x, plank_y - perp_y),
  419. QPointF(plank_x + perp_x, plank_y + perp_y));
  420. }
  421. }
  422. }
  423. }
  424. void MinimapGenerator::render_structures(QImage &image,
  425. const MapDefinition &map_def) {
  426. if (map_def.spawns.empty()) {
  427. return;
  428. }
  429. QPainter painter(&image);
  430. painter.setRenderHint(QPainter::Antialiasing, true);
  431. for (const auto &spawn : map_def.spawns) {
  432. if (!Game::Units::is_building_spawn(spawn.type)) {
  433. continue;
  434. }
  435. const auto [world_x, world_z] =
  436. grid_to_world_coords(spawn.x, spawn.z, map_def);
  437. const auto [px, py] = world_to_pixel(world_x, world_z, map_def.grid);
  438. QColor fill_color = Palette::STRUCTURE_STONE;
  439. QColor border_color = Palette::STRUCTURE_SHADOW;
  440. if (spawn.player_id == 1) {
  441. fill_color = Palette::TEAM_BLUE;
  442. border_color = Palette::TEAM_BLUE_DARK;
  443. } else if (spawn.player_id == 2) {
  444. fill_color = Palette::TEAM_RED;
  445. border_color = Palette::TEAM_RED_DARK;
  446. } else if (spawn.player_id > 0) {
  447. const int hue = (spawn.player_id * 47 + 30) % 360;
  448. fill_color.setHsv(hue, 140, 180);
  449. border_color.setHsv(hue, 180, 100);
  450. }
  451. draw_fortress_icon(painter, px, py, fill_color, border_color);
  452. }
  453. }
  454. void MinimapGenerator::draw_fortress_icon(QPainter &painter, float cx, float cy,
  455. const QColor &fill,
  456. const QColor &border) {
  457. constexpr float SIZE = 10.0F;
  458. constexpr float HALF = SIZE * 0.5F;
  459. painter.setBrush(fill);
  460. painter.setPen(QPen(border, 1.5));
  461. painter.drawRect(
  462. QRectF(cx - HALF * 0.7F, cy - HALF * 0.7F, SIZE * 0.7F, SIZE * 0.7F));
  463. constexpr float TOWER_SIZE = SIZE * 0.35F;
  464. constexpr float TOWER_OFFSET = HALF * 0.85F;
  465. painter.setBrush(fill);
  466. painter.setPen(QPen(border, 1.0));
  467. for (int i = 0; i < 4; ++i) {
  468. const float tx = cx + ((i & 1) != 0 ? TOWER_OFFSET : -TOWER_OFFSET);
  469. const float ty = cy + ((i & 2) != 0 ? TOWER_OFFSET : -TOWER_OFFSET);
  470. painter.drawRect(QRectF(tx - TOWER_SIZE * 0.5F, ty - TOWER_SIZE * 0.5F,
  471. TOWER_SIZE, TOWER_SIZE));
  472. }
  473. painter.setBrush(border);
  474. painter.setPen(Qt::NoPen);
  475. painter.drawRect(
  476. QRectF(cx - SIZE * 0.12F, cy + SIZE * 0.15F, SIZE * 0.24F, SIZE * 0.25F));
  477. constexpr float MERLON_W = SIZE * 0.15F;
  478. constexpr float MERLON_H = SIZE * 0.12F;
  479. painter.setBrush(fill);
  480. painter.setPen(QPen(border, 0.8));
  481. for (int i = 0; i < 3; ++i) {
  482. const float mx = cx - SIZE * 0.25F + static_cast<float>(i) * SIZE * 0.25F;
  483. const float my = cy - HALF * 0.7F - MERLON_H;
  484. painter.drawRect(QRectF(mx, my, MERLON_W, MERLON_H));
  485. }
  486. }
  487. void MinimapGenerator::apply_historical_styling(QImage &image) {
  488. QPainter painter(&image);
  489. painter.setRenderHint(QPainter::Antialiasing, true);
  490. draw_map_border(painter, image.width(), image.height());
  491. apply_vignette(painter, image.width(), image.height());
  492. draw_compass_rose(painter, image.width(), image.height());
  493. }
  494. void MinimapGenerator::draw_map_border(QPainter &painter, int width,
  495. int height) {
  496. constexpr float OUTER_MARGIN = 2.0F;
  497. constexpr float INNER_MARGIN = 5.0F;
  498. painter.setPen(QPen(Palette::INK_MEDIUM, 1.5));
  499. painter.setBrush(Qt::NoBrush);
  500. painter.drawRect(QRectF(OUTER_MARGIN, OUTER_MARGIN,
  501. static_cast<float>(width) - OUTER_MARGIN * 2,
  502. static_cast<float>(height) - OUTER_MARGIN * 2));
  503. painter.setPen(QPen(Palette::INK_LIGHT, 0.8));
  504. painter.drawRect(QRectF(INNER_MARGIN, INNER_MARGIN,
  505. static_cast<float>(width) - INNER_MARGIN * 2,
  506. static_cast<float>(height) - INNER_MARGIN * 2));
  507. }
  508. void MinimapGenerator::apply_vignette(QPainter &painter, int width,
  509. int height) {
  510. const float radius = static_cast<float>(std::max(width, height)) * 0.75F;
  511. QRadialGradient vignette(static_cast<float>(width) * 0.5F,
  512. static_cast<float>(height) * 0.5F, radius);
  513. vignette.setColorAt(0.0, Qt::transparent);
  514. vignette.setColorAt(0.7, Qt::transparent);
  515. vignette.setColorAt(1.0, QColor(60, 45, 30, 35));
  516. painter.setCompositionMode(QPainter::CompositionMode_Multiply);
  517. painter.fillRect(0, 0, width, height, vignette);
  518. painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
  519. }
  520. void MinimapGenerator::draw_compass_rose(QPainter &painter, int width,
  521. int height) {
  522. const float min_dim = static_cast<float>(std::min(width, height));
  523. const float margin = std::clamp(min_dim * 0.06F, 12.0F, 32.0F);
  524. const float SIZE = std::clamp(min_dim * 0.08F, 14.0F, 42.0F);
  525. const float cx = static_cast<float>(width) - margin;
  526. const float cy = static_cast<float>(height) - margin;
  527. const float stroke = std::max(1.2F, SIZE * 0.08F);
  528. painter.setPen(QPen(Palette::INK_MEDIUM, stroke));
  529. painter.setBrush(Qt::NoBrush);
  530. QPainterPath north_arrow;
  531. north_arrow.moveTo(cx, cy - SIZE);
  532. north_arrow.lineTo(cx - SIZE * 0.3F, cy);
  533. north_arrow.lineTo(cx + SIZE * 0.3F, cy);
  534. north_arrow.closeSubpath();
  535. painter.setBrush(Palette::INK_DARK);
  536. painter.drawPath(north_arrow);
  537. QPainterPath south_arrow;
  538. south_arrow.moveTo(cx, cy + SIZE);
  539. south_arrow.lineTo(cx - SIZE * 0.3F, cy);
  540. south_arrow.lineTo(cx + SIZE * 0.3F, cy);
  541. south_arrow.closeSubpath();
  542. painter.setBrush(Palette::PARCHMENT_LIGHT);
  543. painter.drawPath(south_arrow);
  544. painter.drawLine(QPointF(cx - SIZE * 0.7F, cy),
  545. QPointF(cx + SIZE * 0.7F, cy));
  546. painter.setBrush(Palette::INK_MEDIUM);
  547. const float dot_radius = std::max(2.0F, SIZE * 0.2F);
  548. painter.drawEllipse(QPointF(cx, cy), dot_radius, dot_radius);
  549. painter.setPen(QPen(Palette::INK_DARK, stroke));
  550. const float n_half_width = SIZE * 0.35F;
  551. const float n_left = cx - n_half_width;
  552. const float n_right = cx + n_half_width;
  553. const float n_top = cy - SIZE - SIZE * 0.7F;
  554. const float n_bottom = cy - SIZE - SIZE * 0.15F;
  555. QPainterPath n_path;
  556. n_path.moveTo(n_left, n_bottom);
  557. n_path.lineTo(n_left, n_top);
  558. n_path.lineTo(n_right, n_bottom);
  559. n_path.lineTo(n_right, n_top);
  560. painter.drawPath(n_path);
  561. }
  562. auto MinimapGenerator::biome_to_base_color(const BiomeSettings &biome)
  563. -> QColor {
  564. const auto &grass = biome.grass_primary;
  565. QColor base = QColor::fromRgbF(static_cast<double>(grass.x()),
  566. static_cast<double>(grass.y()),
  567. static_cast<double>(grass.z()));
  568. int h, s, v;
  569. base.getHsv(&h, &s, &v);
  570. base.setHsv(h, static_cast<int>(s * 0.4), static_cast<int>(v * 0.85));
  571. return base;
  572. }
  573. auto MinimapGenerator::terrain_feature_color(TerrainType type) -> QColor {
  574. switch (type) {
  575. case TerrainType::Mountain:
  576. return Palette::MOUNTAIN_SHADOW;
  577. case TerrainType::Hill:
  578. return Palette::HILL_BASE;
  579. case TerrainType::River:
  580. return Palette::WATER_MAIN;
  581. case TerrainType::Forest:
  582. return Palette::FOREST_BASE;
  583. case TerrainType::Flat:
  584. default:
  585. return Palette::PARCHMENT_DARK;
  586. }
  587. }
  588. } // namespace Game::Map::Minimap