formation_system.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. #include "formation_system.h"
  2. #include "../units/troop_config.h"
  3. #include <QDebug>
  4. #include <algorithm>
  5. #include <cmath>
  6. #include <memory>
  7. #include <qglobal.h>
  8. #include <qvectornd.h>
  9. #include <utility>
  10. #include <vector>
  11. namespace Game::Systems {
  12. namespace {
  13. constexpr float ROMAN_LINE_SPACING = 3.5F;
  14. constexpr float ROMAN_UNIT_SPACING = 2.5F;
  15. constexpr float CARTHAGE_LINE_SPACING = 3.0F;
  16. constexpr float CARTHAGE_UNIT_SPACING = 2.8F;
  17. auto get_unit_spacing(Game::Units::TroopType type,
  18. float base_spacing) -> float {
  19. float const selection_size =
  20. Game::Units::TroopConfig::instance().getSelectionRingSize(type);
  21. return (selection_size * 2.0F + 0.5F) * base_spacing;
  22. }
  23. auto is_infantry(Game::Units::TroopType type) -> bool {
  24. return type == Game::Units::TroopType::Swordsman ||
  25. type == Game::Units::TroopType::Spearman;
  26. }
  27. auto is_ranged(Game::Units::TroopType type) -> bool {
  28. return type == Game::Units::TroopType::Archer;
  29. }
  30. auto is_cavalry(Game::Units::TroopType type) -> bool {
  31. return type == Game::Units::TroopType::MountedKnight ||
  32. type == Game::Units::TroopType::HorseArcher ||
  33. type == Game::Units::TroopType::HorseSpearman;
  34. }
  35. auto is_siege(Game::Units::TroopType type) -> bool {
  36. return type == Game::Units::TroopType::Catapult ||
  37. type == Game::Units::TroopType::Ballista;
  38. }
  39. auto is_support(Game::Units::TroopType type) -> bool {
  40. return type == Game::Units::TroopType::Healer ||
  41. type == Game::Units::TroopType::Builder;
  42. }
  43. auto calculate_balanced_rows(int total_units, int max_per_row,
  44. int min_per_row) -> std::vector<int> {
  45. std::vector<int> row_sizes;
  46. if (total_units <= 0) {
  47. return row_sizes;
  48. }
  49. if (total_units <= max_per_row) {
  50. row_sizes.push_back(total_units);
  51. return row_sizes;
  52. }
  53. int num_rows = (total_units + max_per_row - 1) / max_per_row;
  54. int base_per_row = total_units / num_rows;
  55. int extra_units = total_units % num_rows;
  56. for (int r = 0; r < num_rows; ++r) {
  57. int units_in_row = base_per_row + (r < extra_units ? 1 : 0);
  58. units_in_row = std::max(min_per_row, std::min(max_per_row, units_in_row));
  59. row_sizes.push_back(units_in_row);
  60. }
  61. return row_sizes;
  62. }
  63. } // namespace
  64. auto RomanFormation::calculate_positions(
  65. int unit_count, const QVector3D &center,
  66. float base_spacing) const -> std::vector<QVector3D> {
  67. std::vector<QVector3D> positions;
  68. positions.reserve(unit_count);
  69. if (unit_count <= 0) {
  70. return positions;
  71. }
  72. float spacing = base_spacing * 1.2F;
  73. if (unit_count > 100) {
  74. spacing *= 2.0F;
  75. } else if (unit_count > 50) {
  76. spacing *= 1.5F;
  77. }
  78. int const rows = std::max(1, static_cast<int>(std::sqrt(unit_count * 0.7F)));
  79. int const cols = (unit_count + rows - 1) / rows;
  80. for (int i = 0; i < unit_count; ++i) {
  81. int const row = i / cols;
  82. int const col = i % cols;
  83. float const offset_x = (col - (cols - 1) * 0.5F) * spacing;
  84. float const offset_z = (row - (rows - 1) * 0.5F) * spacing * 0.9F;
  85. positions.emplace_back(center.x() + offset_x, center.y(),
  86. center.z() + offset_z);
  87. }
  88. return positions;
  89. }
  90. auto RomanFormation::calculate_formation_positions(
  91. const std::vector<UnitFormationInfo> &units, const QVector3D &center,
  92. float base_spacing) const -> std::vector<FormationPosition> {
  93. std::vector<FormationPosition> positions;
  94. positions.reserve(units.size());
  95. if (units.empty()) {
  96. return positions;
  97. }
  98. std::vector<UnitFormationInfo> infantry, archers, cavalry, siege, support;
  99. for (const auto &unit : units) {
  100. if (is_infantry(unit.troop_type)) {
  101. infantry.push_back(unit);
  102. } else if (is_ranged(unit.troop_type)) {
  103. archers.push_back(unit);
  104. } else if (is_cavalry(unit.troop_type)) {
  105. cavalry.push_back(unit);
  106. } else if (is_siege(unit.troop_type)) {
  107. siege.push_back(unit);
  108. } else if (is_support(unit.troop_type)) {
  109. support.push_back(unit);
  110. }
  111. }
  112. float const forward_facing = 0.0F;
  113. float row_offset = 0.0F;
  114. float max_row_width = 0.0F;
  115. if (!infantry.empty()) {
  116. int const max_per_row = std::min(8, static_cast<int>(infantry.size()));
  117. int const min_per_row = std::max(3, max_per_row / 2);
  118. auto row_sizes = calculate_balanced_rows(static_cast<int>(infantry.size()),
  119. max_per_row, min_per_row);
  120. float const unit_spacing =
  121. get_unit_spacing(infantry[0].troop_type, base_spacing);
  122. float const row_spacing = unit_spacing;
  123. int max_units_in_row = 0;
  124. for (int size : row_sizes) {
  125. max_units_in_row = std::max(max_units_in_row, size);
  126. }
  127. float const type_max_width = (max_units_in_row - 1) * unit_spacing;
  128. max_row_width = std::max(max_row_width, type_max_width);
  129. size_t unit_idx = 0;
  130. for (size_t row = 0; row < row_sizes.size() && unit_idx < infantry.size();
  131. ++row) {
  132. int const units_in_row = row_sizes[row];
  133. for (int col = 0; col < units_in_row && unit_idx < infantry.size();
  134. ++col) {
  135. float const x_offset = (col - (units_in_row - 1) * 0.5F) * unit_spacing;
  136. float const z_offset =
  137. row_offset - static_cast<float>(row) * row_spacing;
  138. FormationPosition pos;
  139. pos.position =
  140. QVector3D(center.x() + x_offset, center.y(), center.z() + z_offset);
  141. pos.facing_angle = forward_facing;
  142. pos.entity_id = infantry[unit_idx].entity_id;
  143. positions.push_back(pos);
  144. ++unit_idx;
  145. }
  146. }
  147. row_offset -= static_cast<float>(row_sizes.size()) * row_spacing;
  148. }
  149. if (!archers.empty()) {
  150. int const max_per_row = std::min(10, static_cast<int>(archers.size()));
  151. int const min_per_row = std::max(4, max_per_row / 2);
  152. auto row_sizes = calculate_balanced_rows(static_cast<int>(archers.size()),
  153. max_per_row, min_per_row);
  154. float const unit_spacing =
  155. get_unit_spacing(archers[0].troop_type, base_spacing);
  156. float const row_spacing = unit_spacing;
  157. int max_units_in_row = 0;
  158. for (int size : row_sizes) {
  159. max_units_in_row = std::max(max_units_in_row, size);
  160. }
  161. float const type_max_width = (max_units_in_row - 1) * unit_spacing;
  162. max_row_width = std::max(max_row_width, type_max_width);
  163. size_t unit_idx = 0;
  164. for (size_t row = 0; row < row_sizes.size() && unit_idx < archers.size();
  165. ++row) {
  166. int const units_in_row = row_sizes[row];
  167. for (int col = 0; col < units_in_row && unit_idx < archers.size();
  168. ++col) {
  169. float const x_offset = (col - (units_in_row - 1) * 0.5F) * unit_spacing;
  170. float const z_offset =
  171. row_offset - static_cast<float>(row) * row_spacing;
  172. FormationPosition pos;
  173. pos.position =
  174. QVector3D(center.x() + x_offset, center.y(), center.z() + z_offset);
  175. pos.facing_angle = forward_facing;
  176. pos.entity_id = archers[unit_idx].entity_id;
  177. positions.push_back(pos);
  178. ++unit_idx;
  179. }
  180. }
  181. row_offset -= static_cast<float>(row_sizes.size()) * row_spacing;
  182. }
  183. if (!cavalry.empty()) {
  184. float const cavalry_z_offset = center.z();
  185. for (size_t i = 0; i < cavalry.size(); ++i) {
  186. float const spacing =
  187. get_unit_spacing(cavalry[i].troop_type, base_spacing) * 1.2F;
  188. float x_offset;
  189. if (i % 2 == 0) {
  190. x_offset = (i / 2 + 1) * spacing + 5.0F * base_spacing;
  191. } else {
  192. x_offset = -((i / 2 + 1) * spacing + 5.0F * base_spacing);
  193. }
  194. FormationPosition pos;
  195. pos.position =
  196. QVector3D(center.x() + x_offset, center.y(), cavalry_z_offset);
  197. pos.facing_angle = forward_facing;
  198. pos.entity_id = cavalry[i].entity_id;
  199. positions.push_back(pos);
  200. }
  201. }
  202. if (!siege.empty()) {
  203. float const spacing =
  204. get_unit_spacing(siege[0].troop_type, base_spacing) * 1.5F;
  205. for (size_t i = 0; i < siege.size(); ++i) {
  206. float const x_offset =
  207. (static_cast<int>(i) - (static_cast<int>(siege.size()) - 1) * 0.5F) *
  208. spacing;
  209. float const z_offset = row_offset - ROMAN_LINE_SPACING * base_spacing;
  210. FormationPosition pos;
  211. pos.position =
  212. QVector3D(center.x() + x_offset, center.y(), center.z() + z_offset);
  213. pos.facing_angle = forward_facing;
  214. pos.entity_id = siege[i].entity_id;
  215. positions.push_back(pos);
  216. }
  217. row_offset -= ROMAN_LINE_SPACING * base_spacing * 1.5F;
  218. }
  219. if (!support.empty()) {
  220. float const spacing = get_unit_spacing(support[0].troop_type, base_spacing);
  221. for (size_t i = 0; i < support.size(); ++i) {
  222. float const x_offset = (static_cast<int>(i) -
  223. (static_cast<int>(support.size()) - 1) * 0.5F) *
  224. spacing;
  225. float const z_offset = row_offset - ROMAN_LINE_SPACING * base_spacing;
  226. FormationPosition pos;
  227. pos.position =
  228. QVector3D(center.x() + x_offset, center.y(), center.z() + z_offset);
  229. pos.facing_angle = forward_facing;
  230. pos.entity_id = support[i].entity_id;
  231. positions.push_back(pos);
  232. }
  233. }
  234. return positions;
  235. }
  236. auto BarbarianFormation::calculate_formation_positions(
  237. const std::vector<UnitFormationInfo> &units, const QVector3D &center,
  238. float base_spacing) const -> std::vector<FormationPosition> {
  239. std::vector<FormationPosition> positions;
  240. auto simple_pos =
  241. calculate_positions(static_cast<int>(units.size()), center, base_spacing);
  242. for (size_t i = 0; i < simple_pos.size() && i < units.size(); ++i) {
  243. FormationPosition fpos;
  244. fpos.position = simple_pos[i];
  245. fpos.facing_angle = 0.0F;
  246. fpos.entity_id = units[i].entity_id;
  247. positions.push_back(fpos);
  248. }
  249. return positions;
  250. }
  251. auto BarbarianFormation::calculate_positions(
  252. int unit_count, const QVector3D &center,
  253. float base_spacing) const -> std::vector<QVector3D> {
  254. std::vector<QVector3D> positions;
  255. positions.reserve(unit_count);
  256. if (unit_count <= 0) {
  257. return positions;
  258. }
  259. float spacing = base_spacing * 1.8F;
  260. if (unit_count > 100) {
  261. spacing *= 2.0F;
  262. } else if (unit_count > 50) {
  263. spacing *= 1.5F;
  264. }
  265. int const side = std::ceil(std::sqrt(static_cast<float>(unit_count)));
  266. for (int i = 0; i < unit_count; ++i) {
  267. int const gx = i % side;
  268. int const gy = i / side;
  269. float const base_x = (gx - (side - 1) * 0.5F) * spacing;
  270. float const base_z = (gy - (side - 1) * 0.5F) * spacing;
  271. positions.emplace_back(center.x() + base_x, center.y(),
  272. center.z() + base_z);
  273. }
  274. return positions;
  275. }
  276. auto CarthageFormation::calculate_formation_positions(
  277. const std::vector<UnitFormationInfo> &units, const QVector3D &center,
  278. float base_spacing) const -> std::vector<FormationPosition> {
  279. std::vector<FormationPosition> positions;
  280. positions.reserve(units.size());
  281. if (units.empty()) {
  282. return positions;
  283. }
  284. std::vector<UnitFormationInfo> infantry, archers, cavalry, siege, support;
  285. for (const auto &unit : units) {
  286. if (is_infantry(unit.troop_type)) {
  287. infantry.push_back(unit);
  288. } else if (is_ranged(unit.troop_type)) {
  289. archers.push_back(unit);
  290. } else if (is_cavalry(unit.troop_type)) {
  291. cavalry.push_back(unit);
  292. } else if (is_siege(unit.troop_type)) {
  293. siege.push_back(unit);
  294. } else if (is_support(unit.troop_type)) {
  295. support.push_back(unit);
  296. }
  297. }
  298. float const forward_facing = 0.0F;
  299. float row_offset = 0.0F;
  300. if (!infantry.empty()) {
  301. int const max_per_row = std::min(7, static_cast<int>(infantry.size()));
  302. int const min_per_row = std::max(4, max_per_row / 2);
  303. auto row_sizes = calculate_balanced_rows(static_cast<int>(infantry.size()),
  304. max_per_row, min_per_row);
  305. float const spacing =
  306. get_unit_spacing(infantry[0].troop_type, base_spacing);
  307. size_t unit_idx = 0;
  308. for (size_t row = 0; row < row_sizes.size() && unit_idx < infantry.size();
  309. ++row) {
  310. int const units_in_row = row_sizes[row];
  311. float const row_echelon = static_cast<float>(row) * 0.8F * spacing;
  312. for (int col = 0; col < units_in_row && unit_idx < infantry.size();
  313. ++col) {
  314. float const x_offset =
  315. (col - (units_in_row - 1) * 0.5F) * spacing + row_echelon;
  316. float const z_offset = row_offset - static_cast<float>(row) *
  317. CARTHAGE_LINE_SPACING *
  318. base_spacing;
  319. FormationPosition pos;
  320. pos.position =
  321. QVector3D(center.x() + x_offset, center.y(), center.z() + z_offset);
  322. pos.facing_angle = forward_facing;
  323. pos.entity_id = infantry[unit_idx].entity_id;
  324. positions.push_back(pos);
  325. ++unit_idx;
  326. }
  327. }
  328. row_offset -= static_cast<float>(row_sizes.size()) * CARTHAGE_LINE_SPACING *
  329. base_spacing;
  330. }
  331. if (!archers.empty()) {
  332. int const max_per_row = std::min(9, static_cast<int>(archers.size()));
  333. int const min_per_row = std::max(5, max_per_row / 2);
  334. auto row_sizes = calculate_balanced_rows(static_cast<int>(archers.size()),
  335. max_per_row, min_per_row);
  336. float const spacing = get_unit_spacing(archers[0].troop_type, base_spacing);
  337. size_t unit_idx = 0;
  338. for (size_t row = 0; row < row_sizes.size() && unit_idx < archers.size();
  339. ++row) {
  340. int const units_in_row = row_sizes[row];
  341. float const row_echelon = -static_cast<float>(row) * 0.8F * spacing;
  342. for (int col = 0; col < units_in_row && unit_idx < archers.size();
  343. ++col) {
  344. float const x_offset =
  345. (col - (units_in_row - 1) * 0.5F) * spacing + row_echelon;
  346. float const z_offset = row_offset - static_cast<float>(row) *
  347. CARTHAGE_LINE_SPACING *
  348. base_spacing;
  349. FormationPosition pos;
  350. pos.position =
  351. QVector3D(center.x() + x_offset, center.y(), center.z() + z_offset);
  352. pos.facing_angle = forward_facing;
  353. pos.entity_id = archers[unit_idx].entity_id;
  354. positions.push_back(pos);
  355. ++unit_idx;
  356. }
  357. }
  358. row_offset -= static_cast<float>(row_sizes.size()) * CARTHAGE_LINE_SPACING *
  359. base_spacing;
  360. }
  361. if (!siege.empty()) {
  362. for (size_t i = 0; i < siege.size(); ++i) {
  363. float const spacing =
  364. get_unit_spacing(siege[i].troop_type, base_spacing) * 2.0F;
  365. float const x_offset =
  366. (static_cast<int>(i) - (static_cast<int>(siege.size()) - 1) * 0.5F) *
  367. spacing;
  368. FormationPosition pos;
  369. pos.position = QVector3D(center.x() + x_offset, center.y(),
  370. center.z() + row_offset -
  371. CARTHAGE_LINE_SPACING * base_spacing);
  372. pos.facing_angle = forward_facing;
  373. pos.entity_id = siege[i].entity_id;
  374. positions.push_back(pos);
  375. }
  376. row_offset -= CARTHAGE_LINE_SPACING * base_spacing * 1.5F;
  377. }
  378. if (!cavalry.empty()) {
  379. float const cavalry_z_offset = center.z();
  380. int const right_flank_count = (static_cast<int>(cavalry.size()) + 1) / 2;
  381. int const left_flank_count =
  382. static_cast<int>(cavalry.size()) - right_flank_count;
  383. for (int i = 0; i < right_flank_count; ++i) {
  384. float const spacing =
  385. get_unit_spacing(cavalry[static_cast<size_t>(i)].troop_type,
  386. base_spacing) *
  387. 1.3F;
  388. float const x_offset = (i + 1) * spacing + 6.0F * base_spacing;
  389. float const z_forward = i * 0.7F * base_spacing;
  390. FormationPosition pos;
  391. pos.position = QVector3D(center.x() + x_offset, center.y(),
  392. cavalry_z_offset + z_forward);
  393. pos.facing_angle = forward_facing;
  394. pos.entity_id = cavalry[static_cast<size_t>(i)].entity_id;
  395. positions.push_back(pos);
  396. }
  397. for (int i = 0; i < left_flank_count; ++i) {
  398. float const spacing =
  399. get_unit_spacing(
  400. cavalry[static_cast<size_t>(right_flank_count + i)].troop_type,
  401. base_spacing) *
  402. 1.3F;
  403. float const x_offset = -((i + 1) * spacing + 6.0F * base_spacing);
  404. float const z_forward = i * 0.7F * base_spacing;
  405. FormationPosition pos;
  406. pos.position = QVector3D(center.x() + x_offset, center.y(),
  407. cavalry_z_offset + z_forward);
  408. pos.facing_angle = forward_facing;
  409. pos.entity_id =
  410. cavalry[static_cast<size_t>(right_flank_count + i)].entity_id;
  411. positions.push_back(pos);
  412. }
  413. }
  414. if (!support.empty()) {
  415. for (size_t i = 0; i < support.size(); ++i) {
  416. float const spacing =
  417. get_unit_spacing(support[i].troop_type, base_spacing);
  418. float const x_offset = (static_cast<int>(i) -
  419. (static_cast<int>(support.size()) - 1) * 0.5F) *
  420. spacing;
  421. float const z_offset = row_offset - CARTHAGE_LINE_SPACING * base_spacing;
  422. FormationPosition pos;
  423. pos.position =
  424. QVector3D(center.x() + x_offset, center.y(), center.z() + z_offset);
  425. pos.facing_angle = forward_facing;
  426. pos.entity_id = support[i].entity_id;
  427. positions.push_back(pos);
  428. }
  429. }
  430. return positions;
  431. }
  432. auto CarthageFormation::calculate_positions(
  433. int unit_count, const QVector3D &center,
  434. float base_spacing) const -> std::vector<QVector3D> {
  435. std::vector<QVector3D> positions;
  436. positions.reserve(unit_count);
  437. if (unit_count <= 0) {
  438. return positions;
  439. }
  440. float spacing = base_spacing * 1.5F;
  441. if (unit_count > 100) {
  442. spacing *= 2.0F;
  443. } else if (unit_count > 50) {
  444. spacing *= 1.5F;
  445. }
  446. int const rows = std::max(1, static_cast<int>(std::sqrt(unit_count * 0.8F)));
  447. int const cols = (unit_count + rows - 1) / rows;
  448. for (int i = 0; i < unit_count; ++i) {
  449. int const row = i / cols;
  450. int const col = i % cols;
  451. float const base_x = (col - (cols - 1) * 0.5F) * spacing;
  452. float const base_z = (row - (rows - 1) * 0.5F) * spacing * 0.85F;
  453. positions.emplace_back(center.x() + base_x, center.y(),
  454. center.z() + base_z);
  455. }
  456. return positions;
  457. }
  458. auto FormationSystem::instance() -> FormationSystem & {
  459. static FormationSystem inst;
  460. return inst;
  461. }
  462. FormationSystem::FormationSystem() { initialize_defaults(); }
  463. void FormationSystem::initialize_defaults() {
  464. register_formation(FormationType::Roman, std::make_unique<RomanFormation>());
  465. register_formation(FormationType::Barbarian,
  466. std::make_unique<BarbarianFormation>());
  467. register_formation(FormationType::Carthage,
  468. std::make_unique<CarthageFormation>());
  469. }
  470. auto FormationSystem::get_formation_positions(
  471. FormationType type, int unit_count, const QVector3D &center,
  472. float base_spacing) -> std::vector<QVector3D> {
  473. auto it = m_formations.find(type);
  474. if (it == m_formations.end()) {
  475. qWarning() << "Formation type not found, using default spread";
  476. return RomanFormation().calculate_positions(unit_count, center,
  477. base_spacing);
  478. }
  479. return it->second->calculate_positions(unit_count, center, base_spacing);
  480. }
  481. auto FormationSystem::get_formation_positions_with_facing(
  482. FormationType type, const std::vector<UnitFormationInfo> &units,
  483. const QVector3D &center,
  484. float base_spacing) -> std::vector<FormationPosition> {
  485. auto it = m_formations.find(type);
  486. if (it == m_formations.end()) {
  487. qWarning() << "Formation type not found, using default";
  488. return RomanFormation().calculate_formation_positions(units, center,
  489. base_spacing);
  490. }
  491. return it->second->calculate_formation_positions(units, center, base_spacing);
  492. }
  493. void FormationSystem::register_formation(
  494. FormationType type, std::unique_ptr<IFormation> formation) {
  495. m_formations[type] = std::move(formation);
  496. }
  497. auto FormationSystem::get_formation(FormationType type) const
  498. -> const IFormation * {
  499. auto it = m_formations.find(type);
  500. if (it == m_formations.end()) {
  501. return nullptr;
  502. }
  503. return it->second.get();
  504. }
  505. } // namespace Game::Systems