production_system.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. #include "production_system.h"
  2. #include "../core/component.h"
  3. #include "../core/ownership_constants.h"
  4. #include "../core/world.h"
  5. #include "../game_config.h"
  6. #include "../map/map_transformer.h"
  7. #include "../map/terrain_service.h"
  8. #include "../units/factory.h"
  9. #include "../units/troop_config.h"
  10. #include "building_collision_registry.h"
  11. #include "command_service.h"
  12. #include "nation_registry.h"
  13. #include "pathfinding.h"
  14. #include "troop_profile_service.h"
  15. #include "units/spawn_type.h"
  16. #include "units/unit.h"
  17. #include <cmath>
  18. #include <limits>
  19. #include <qvectornd.h>
  20. namespace Game::Systems {
  21. namespace {
  22. void apply_production_profile(Engine::Core::ProductionComponent *prod,
  23. Game::Systems::NationID nation_id,
  24. Game::Units::TroopType troop_type) {
  25. if (prod == nullptr) {
  26. return;
  27. }
  28. const auto profile =
  29. TroopProfileService::instance().get_profile(nation_id, troop_type);
  30. prod->build_time = profile.production.build_time;
  31. prod->villager_cost = profile.production.cost;
  32. }
  33. auto resolve_nation_id(const Engine::Core::UnitComponent *unit,
  34. int owner_id) -> Game::Systems::NationID {
  35. auto &registry = NationRegistry::instance();
  36. if (const auto *nation = registry.get_nation_for_player(owner_id)) {
  37. return nation->id;
  38. }
  39. return registry.default_nation_id();
  40. }
  41. auto compute_builder_exit_position(
  42. float center_x, float center_z, const QVector3D &builder_pos,
  43. float unit_radius, const std::string &building_type) -> QVector3D {
  44. auto const size = BuildingCollisionRegistry::get_building_size(building_type);
  45. float const half_width = size.width * 0.5F;
  46. float const half_depth = size.depth * 0.5F;
  47. float const clearance = unit_radius + 0.25F;
  48. float dir_x = builder_pos.x() - center_x;
  49. float dir_z = builder_pos.z() - center_z;
  50. float const len_sq = dir_x * dir_x + dir_z * dir_z;
  51. if (len_sq < 0.0001F) {
  52. dir_x = 1.0F;
  53. dir_z = 0.0F;
  54. } else {
  55. float const len = std::sqrt(len_sq);
  56. dir_x /= len;
  57. dir_z /= len;
  58. }
  59. float const abs_x = std::fabs(dir_x);
  60. float const abs_z = std::fabs(dir_z);
  61. float const sx = (abs_x > 0.0001F) ? (half_width + clearance) / abs_x
  62. : std::numeric_limits<float>::infinity();
  63. float const sz = (abs_z > 0.0001F) ? (half_depth + clearance) / abs_z
  64. : std::numeric_limits<float>::infinity();
  65. float const scale = std::min(sx, sz);
  66. float const fallback_scale = std::max(half_width, half_depth) + clearance;
  67. float const final_scale =
  68. std::isfinite(scale) && scale > 0.0F ? scale : fallback_scale;
  69. return {center_x + dir_x * final_scale, builder_pos.y(),
  70. center_z + dir_z * final_scale};
  71. }
  72. auto find_guaranteed_valid_exit(float exit_x, float exit_z,
  73. float unit_radius) -> QVector3D {
  74. Pathfinding *pathfinder = CommandService::get_pathfinder();
  75. if (pathfinder == nullptr) {
  76. return {exit_x, 0.0F, exit_z};
  77. }
  78. auto &terrain_service = Game::Map::TerrainService::instance();
  79. Point const exit_grid = CommandService::world_to_grid(exit_x, exit_z);
  80. bool is_valid = pathfinder->is_walkable_with_radius(exit_grid.x, exit_grid.y,
  81. unit_radius);
  82. if (is_valid && terrain_service.is_initialized()) {
  83. is_valid = terrain_service.is_walkable(exit_grid.x, exit_grid.y);
  84. }
  85. if (is_valid) {
  86. return {exit_x, 0.0F, exit_z};
  87. }
  88. constexpr int kMaxSearchRadius = 50;
  89. Point safe_grid = exit_grid;
  90. for (int radius = 1; radius <= kMaxSearchRadius; ++radius) {
  91. for (int dy = -radius; dy <= radius; ++dy) {
  92. for (int dx = -radius; dx <= radius; ++dx) {
  93. if (std::abs(dx) != radius && std::abs(dy) != radius) {
  94. continue;
  95. }
  96. int const check_x = exit_grid.x + dx;
  97. int const check_y = exit_grid.y + dy;
  98. bool valid =
  99. pathfinder->is_walkable_with_radius(check_x, check_y, unit_radius);
  100. if (valid && terrain_service.is_initialized()) {
  101. valid = terrain_service.is_walkable(check_x, check_y);
  102. }
  103. if (valid) {
  104. safe_grid = {check_x, check_y};
  105. return CommandService::grid_to_world(safe_grid);
  106. }
  107. }
  108. }
  109. }
  110. return CommandService::grid_to_world(safe_grid);
  111. }
  112. void activate_bypass_movement(Engine::Core::BuilderProductionComponent *builder,
  113. float target_x, float target_z) {
  114. if (builder == nullptr) {
  115. return;
  116. }
  117. builder->bypass_movement_active = true;
  118. builder->bypass_target_x = target_x;
  119. builder->bypass_target_z = target_z;
  120. }
  121. } // namespace
  122. void ProductionSystem::update(Engine::Core::World *world, float delta_time) {
  123. if (world == nullptr) {
  124. return;
  125. }
  126. auto entities = world->get_entities_with<Engine::Core::ProductionComponent>();
  127. for (auto *e : entities) {
  128. auto *prod = e->get_component<Engine::Core::ProductionComponent>();
  129. if (prod == nullptr) {
  130. continue;
  131. }
  132. auto *unit_comp = e->get_component<Engine::Core::UnitComponent>();
  133. if ((unit_comp != nullptr) &&
  134. Game::Core::isNeutralOwner(unit_comp->owner_id)) {
  135. continue;
  136. }
  137. if (!prod->in_progress) {
  138. continue;
  139. }
  140. const int owner_id = (unit_comp != nullptr) ? unit_comp->owner_id : -1;
  141. const auto nation_id = resolve_nation_id(unit_comp, owner_id);
  142. const auto current_profile = TroopProfileService::instance().get_profile(
  143. nation_id, prod->product_type);
  144. int const individuals_per_unit = current_profile.individuals_per_unit;
  145. int const production_cost = current_profile.production.cost;
  146. if (prod->produced_count + production_cost > prod->max_units) {
  147. prod->in_progress = false;
  148. continue;
  149. }
  150. prod->time_remaining -= delta_time;
  151. if (prod->time_remaining <= 0.0F) {
  152. auto *t = e->get_component<Engine::Core::TransformComponent>();
  153. auto *u = e->get_component<Engine::Core::UnitComponent>();
  154. if ((t != nullptr) && (u != nullptr)) {
  155. int const current_troops =
  156. Engine::Core::World::count_troops_for_player(u->owner_id);
  157. int const max_troops =
  158. Game::GameConfig::instance().get_max_troops_per_player();
  159. if (current_troops + production_cost > max_troops) {
  160. prod->in_progress = false;
  161. prod->time_remaining = 0.0F;
  162. continue;
  163. }
  164. float const exit_offset = 2.5F + 0.2F * float(prod->produced_count % 5);
  165. float const exit_angle = 0.5F * float(prod->produced_count % 8);
  166. QVector3D const exit_pos =
  167. QVector3D(t->position.x + exit_offset * std::cos(exit_angle), 0.0F,
  168. t->position.z + exit_offset * std::sin(exit_angle));
  169. auto reg = Game::Map::MapTransformer::getFactoryRegistry();
  170. if (reg) {
  171. Game::Units::SpawnParams sp;
  172. sp.position = exit_pos;
  173. sp.player_id = u->owner_id;
  174. sp.spawn_type =
  175. Game::Units::spawn_typeFromTroopType(prod->product_type);
  176. sp.ai_controlled =
  177. e->has_component<Engine::Core::AIControlledComponent>();
  178. sp.nation_id = nation_id;
  179. sp.is_initial_spawn = false;
  180. auto unit = reg->create(sp.spawn_type, *world, sp);
  181. if (unit && prod->rally_set) {
  182. unit->move_to(prod->rally_x, prod->rally_z);
  183. }
  184. }
  185. prod->produced_count += production_cost;
  186. }
  187. prod->in_progress = false;
  188. prod->time_remaining = 0.0F;
  189. if (!prod->production_queue.empty()) {
  190. prod->product_type = prod->production_queue.front();
  191. prod->production_queue.erase(prod->production_queue.begin());
  192. apply_production_profile(prod, nation_id, prod->product_type);
  193. prod->time_remaining = prod->build_time;
  194. prod->in_progress = true;
  195. }
  196. }
  197. }
  198. constexpr float CONSTRUCTION_ARRIVAL_DISTANCE_SQ = 4.0F;
  199. constexpr float MAX_CONSTRUCTION_DISTANCE_SQ = 9.0F;
  200. auto builder_entities =
  201. world->get_entities_with<Engine::Core::BuilderProductionComponent>();
  202. for (auto *e : builder_entities) {
  203. auto *builder_prod =
  204. e->get_component<Engine::Core::BuilderProductionComponent>();
  205. if (builder_prod == nullptr) {
  206. continue;
  207. }
  208. if (builder_prod->is_placement_preview) {
  209. continue;
  210. }
  211. auto *transform = e->get_component<Engine::Core::TransformComponent>();
  212. auto *movement = e->get_component<Engine::Core::MovementComponent>();
  213. if (builder_prod->has_construction_site &&
  214. !builder_prod->at_construction_site) {
  215. if (transform != nullptr) {
  216. float dx = builder_prod->construction_site_x - transform->position.x;
  217. float dz = builder_prod->construction_site_z - transform->position.z;
  218. float dist_sq = dx * dx + dz * dz;
  219. if (dist_sq < CONSTRUCTION_ARRIVAL_DISTANCE_SQ) {
  220. builder_prod->at_construction_site = true;
  221. builder_prod->in_progress = true;
  222. builder_prod->bypass_movement_active = false;
  223. transform->position.x = builder_prod->construction_site_x;
  224. transform->position.z = builder_prod->construction_site_z;
  225. if (movement != nullptr) {
  226. movement->goal_x = builder_prod->construction_site_x;
  227. movement->goal_y = builder_prod->construction_site_z;
  228. movement->target_x = builder_prod->construction_site_x;
  229. movement->target_y = builder_prod->construction_site_z;
  230. movement->has_target = false;
  231. movement->clear_path();
  232. movement->vx = 0.0F;
  233. movement->vz = 0.0F;
  234. }
  235. } else {
  236. if (!builder_prod->bypass_movement_active) {
  237. activate_bypass_movement(builder_prod,
  238. builder_prod->construction_site_x,
  239. builder_prod->construction_site_z);
  240. }
  241. }
  242. }
  243. continue;
  244. }
  245. if (!builder_prod->in_progress) {
  246. continue;
  247. }
  248. if (builder_prod->at_construction_site && transform != nullptr) {
  249. float dx = builder_prod->construction_site_x - transform->position.x;
  250. float dz = builder_prod->construction_site_z - transform->position.z;
  251. float dist_sq = dx * dx + dz * dz;
  252. if (dist_sq > MAX_CONSTRUCTION_DISTANCE_SQ) {
  253. builder_prod->has_construction_site = false;
  254. builder_prod->at_construction_site = false;
  255. builder_prod->in_progress = false;
  256. builder_prod->construction_complete = false;
  257. builder_prod->time_remaining = 0.0F;
  258. continue;
  259. }
  260. }
  261. builder_prod->time_remaining -= delta_time;
  262. if (builder_prod->time_remaining <= 0.0F) {
  263. auto *t = e->get_component<Engine::Core::TransformComponent>();
  264. auto *u = e->get_component<Engine::Core::UnitComponent>();
  265. if ((t != nullptr) && (u != nullptr)) {
  266. auto reg = Game::Map::MapTransformer::getFactoryRegistry();
  267. if (reg) {
  268. Game::Units::SpawnParams sp;
  269. if (builder_prod->has_construction_site) {
  270. sp.position =
  271. QVector3D(builder_prod->construction_site_x, t->position.y,
  272. builder_prod->construction_site_z);
  273. } else {
  274. sp.position =
  275. QVector3D(t->position.x, t->position.y, t->position.z);
  276. }
  277. sp.player_id = u->owner_id;
  278. sp.ai_controlled =
  279. e->has_component<Engine::Core::AIControlledComponent>();
  280. sp.nation_id = u->nation_id;
  281. sp.is_initial_spawn = false;
  282. if (builder_prod->product_type == "catapult") {
  283. sp.spawn_type = Game::Units::SpawnType::Catapult;
  284. } else if (builder_prod->product_type == "ballista") {
  285. sp.spawn_type = Game::Units::SpawnType::Ballista;
  286. } else if (builder_prod->product_type == "defense_tower") {
  287. sp.spawn_type = Game::Units::SpawnType::DefenseTower;
  288. } else if (builder_prod->product_type == "home") {
  289. sp.spawn_type = Game::Units::SpawnType::Home;
  290. } else {
  291. builder_prod->in_progress = false;
  292. builder_prod->time_remaining = 0.0F;
  293. builder_prod->has_construction_site = false;
  294. builder_prod->at_construction_site = false;
  295. continue;
  296. }
  297. reg->create(sp.spawn_type, *world, sp);
  298. if (builder_prod->has_construction_site && movement != nullptr &&
  299. t != nullptr) {
  300. float const unit_radius =
  301. CommandService::get_unit_radius(*world, e->get_id());
  302. QVector3D const preferred_exit = compute_builder_exit_position(
  303. builder_prod->construction_site_x,
  304. builder_prod->construction_site_z,
  305. QVector3D(t->position.x, t->position.y, t->position.z),
  306. unit_radius, builder_prod->product_type);
  307. QVector3D const safe_exit = find_guaranteed_valid_exit(
  308. preferred_exit.x(), preferred_exit.z(), unit_radius);
  309. activate_bypass_movement(builder_prod, safe_exit.x(),
  310. safe_exit.z());
  311. movement->goal_x = safe_exit.x();
  312. movement->goal_y = safe_exit.z();
  313. movement->target_x = safe_exit.x();
  314. movement->target_y = safe_exit.z();
  315. }
  316. }
  317. }
  318. builder_prod->in_progress = false;
  319. builder_prod->time_remaining = 0.0F;
  320. builder_prod->construction_complete = true;
  321. builder_prod->has_construction_site = false;
  322. builder_prod->at_construction_site = false;
  323. }
  324. }
  325. }
  326. } // namespace Game::Systems