combat_system.cpp 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957
  1. #include "combat_system.h"
  2. #include "../core/component.h"
  3. #include "../core/event_manager.h"
  4. #include "../core/world.h"
  5. #include "../units/troop_config.h"
  6. #include "../visuals/team_colors.h"
  7. #include "arrow_system.h"
  8. #include "building_collision_registry.h"
  9. #include "command_service.h"
  10. #include "owner_registry.h"
  11. #include "units/spawn_type.h"
  12. #include <algorithm>
  13. #include <cmath>
  14. #include <limits>
  15. #include <numbers>
  16. #include <qvectornd.h>
  17. #include <random>
  18. #include <vector>
  19. namespace Game::Systems {
  20. void CombatSystem::update(Engine::Core::World *world, float deltaTime) {
  21. processAttacks(world, deltaTime);
  22. processAutoEngagement(world, deltaTime);
  23. }
  24. void CombatSystem::processAttacks(Engine::Core::World *world, float deltaTime) {
  25. auto units = world->getEntitiesWith<Engine::Core::UnitComponent>();
  26. auto *arrow_sys = world->getSystem<ArrowSystem>();
  27. for (auto *attacker : units) {
  28. if (attacker->hasComponent<Engine::Core::PendingRemovalComponent>()) {
  29. continue;
  30. }
  31. auto *attacker_unit = attacker->getComponent<Engine::Core::UnitComponent>();
  32. auto *attacker_transform =
  33. attacker->getComponent<Engine::Core::TransformComponent>();
  34. auto *attacker_atk =
  35. attacker->getComponent<Engine::Core::AttackComponent>();
  36. if ((attacker_unit == nullptr) || (attacker_transform == nullptr)) {
  37. continue;
  38. }
  39. if (attacker_unit->health <= 0) {
  40. continue;
  41. }
  42. if ((attacker_atk != nullptr) && attacker_atk->inMeleeLock) {
  43. auto *lock_target = world->getEntity(attacker_atk->meleeLockTargetId);
  44. if ((lock_target == nullptr) ||
  45. lock_target->hasComponent<Engine::Core::PendingRemovalComponent>()) {
  46. attacker_atk->inMeleeLock = false;
  47. attacker_atk->meleeLockTargetId = 0;
  48. } else {
  49. auto *lock_target_unit =
  50. lock_target->getComponent<Engine::Core::UnitComponent>();
  51. if ((lock_target_unit == nullptr) || lock_target_unit->health <= 0) {
  52. attacker_atk->inMeleeLock = false;
  53. attacker_atk->meleeLockTargetId = 0;
  54. } else {
  55. auto *att_t = attacker_transform;
  56. auto *tgt_t =
  57. lock_target->getComponent<Engine::Core::TransformComponent>();
  58. if ((att_t != nullptr) && (tgt_t != nullptr)) {
  59. float const dx = tgt_t->position.x - att_t->position.x;
  60. float const dz = tgt_t->position.z - att_t->position.z;
  61. float const dist = std::sqrt(dx * dx + dz * dz);
  62. const float ideal_melee_distance = 0.6F;
  63. const float max_melee_separation = 0.9F;
  64. if (dist > max_melee_separation) {
  65. float const pull_amount =
  66. (dist - ideal_melee_distance) * 0.3F * deltaTime * 5.0F;
  67. if (dist > 0.001F) {
  68. QVector3D const direction(dx / dist, 0.0F, dz / dist);
  69. att_t->position.x += direction.x() * pull_amount;
  70. att_t->position.z += direction.z() * pull_amount;
  71. }
  72. }
  73. }
  74. }
  75. }
  76. }
  77. if ((attacker_atk != nullptr) && attacker_atk->inMeleeLock &&
  78. attacker_atk->meleeLockTargetId != 0) {
  79. auto *lock_target = world->getEntity(attacker_atk->meleeLockTargetId);
  80. if ((lock_target != nullptr) &&
  81. !lock_target->hasComponent<Engine::Core::PendingRemovalComponent>()) {
  82. auto *attack_target =
  83. attacker->getComponent<Engine::Core::AttackTargetComponent>();
  84. if (attack_target == nullptr) {
  85. attack_target =
  86. attacker->addComponent<Engine::Core::AttackTargetComponent>();
  87. }
  88. if (attack_target != nullptr) {
  89. attack_target->target_id = attacker_atk->meleeLockTargetId;
  90. attack_target->shouldChase = false;
  91. }
  92. }
  93. }
  94. float range = 2.0F;
  95. int damage = 10;
  96. float cooldown = 1.0F;
  97. float *t_accum = nullptr;
  98. float tmp_accum = 0.0F;
  99. if (attacker_atk != nullptr) {
  100. updateCombatMode(attacker, world, attacker_atk);
  101. range = attacker_atk->getCurrentRange();
  102. damage = attacker_atk->getCurrentDamage();
  103. cooldown = attacker_atk->getCurrentCooldown();
  104. auto *hold_mode =
  105. attacker->getComponent<Engine::Core::HoldModeComponent>();
  106. if ((hold_mode != nullptr) && hold_mode->active) {
  107. if (attacker_unit->spawn_type == Game::Units::SpawnType::Archer) {
  108. range *= 1.5F;
  109. damage = static_cast<int>(damage * 1.3F);
  110. } else if (attacker_unit->spawn_type ==
  111. Game::Units::SpawnType::Spearman) {
  112. damage = static_cast<int>(damage * 1.4F);
  113. }
  114. }
  115. attacker_atk->timeSinceLast += deltaTime;
  116. t_accum = &attacker_atk->timeSinceLast;
  117. } else {
  118. tmp_accum += deltaTime;
  119. t_accum = &tmp_accum;
  120. }
  121. if (*t_accum < cooldown) {
  122. continue;
  123. }
  124. auto *attack_target =
  125. attacker->getComponent<Engine::Core::AttackTargetComponent>();
  126. Engine::Core::Entity *best_target = nullptr;
  127. if ((attack_target != nullptr) && attack_target->target_id != 0) {
  128. auto *target = world->getEntity(attack_target->target_id);
  129. if ((target != nullptr) &&
  130. !target->hasComponent<Engine::Core::PendingRemovalComponent>()) {
  131. auto *target_unit = target->getComponent<Engine::Core::UnitComponent>();
  132. auto &owner_registry = Game::Systems::OwnerRegistry::instance();
  133. bool const is_ally = owner_registry.areAllies(attacker_unit->owner_id,
  134. target_unit->owner_id);
  135. if ((target_unit != nullptr) && target_unit->health > 0 &&
  136. target_unit->owner_id != attacker_unit->owner_id && !is_ally) {
  137. if (isInRange(attacker, target, range)) {
  138. best_target = target;
  139. bool is_ranged_unit = false;
  140. if ((attacker_atk != nullptr) && attacker_atk->canRanged &&
  141. attacker_atk->currentMode ==
  142. Engine::Core::AttackComponent::CombatMode::Ranged) {
  143. is_ranged_unit = true;
  144. }
  145. if (is_ranged_unit) {
  146. auto *movement =
  147. attacker->getComponent<Engine::Core::MovementComponent>();
  148. if ((movement != nullptr) && movement->hasTarget) {
  149. movement->hasTarget = false;
  150. movement->vx = 0.0F;
  151. movement->vz = 0.0F;
  152. movement->path.clear();
  153. if (attacker_transform != nullptr) {
  154. movement->target_x = attacker_transform->position.x;
  155. movement->target_y = attacker_transform->position.z;
  156. movement->goalX = attacker_transform->position.x;
  157. movement->goalY = attacker_transform->position.z;
  158. }
  159. }
  160. }
  161. if (auto *att_t =
  162. attacker
  163. ->getComponent<Engine::Core::TransformComponent>()) {
  164. if (auto *tgt_t =
  165. target
  166. ->getComponent<Engine::Core::TransformComponent>()) {
  167. float const dx = tgt_t->position.x - att_t->position.x;
  168. float const dz = tgt_t->position.z - att_t->position.z;
  169. float const yaw =
  170. std::atan2(dx, dz) * 180.0F / std::numbers::pi_v<float>;
  171. att_t->desiredYaw = yaw;
  172. att_t->hasDesiredYaw = true;
  173. }
  174. }
  175. } else if (attack_target->shouldChase) {
  176. auto *hold_mode =
  177. attacker->getComponent<Engine::Core::HoldModeComponent>();
  178. if ((hold_mode != nullptr) && hold_mode->active) {
  179. if (!isInRange(attacker, target, range)) {
  180. attacker
  181. ->removeComponent<Engine::Core::AttackTargetComponent>();
  182. }
  183. continue;
  184. }
  185. bool is_ranged_unit = false;
  186. if ((attacker_atk != nullptr) && attacker_atk->canRanged &&
  187. attacker_atk->currentMode ==
  188. Engine::Core::AttackComponent::CombatMode::Ranged) {
  189. is_ranged_unit = true;
  190. }
  191. bool const currently_in_range = isInRange(attacker, target, range);
  192. if (is_ranged_unit && currently_in_range) {
  193. auto *movement =
  194. attacker->getComponent<Engine::Core::MovementComponent>();
  195. if (movement != nullptr) {
  196. movement->hasTarget = false;
  197. movement->vx = 0.0F;
  198. movement->vz = 0.0F;
  199. movement->path.clear();
  200. auto *attacker_transform_component =
  201. attacker->getComponent<Engine::Core::TransformComponent>();
  202. if (attacker_transform_component != nullptr) {
  203. movement->target_x = attacker_transform_component->position.x;
  204. movement->target_y = attacker_transform_component->position.z;
  205. movement->goalX = attacker_transform_component->position.x;
  206. movement->goalY = attacker_transform_component->position.z;
  207. }
  208. }
  209. best_target = target;
  210. } else {
  211. auto *target_transform =
  212. target->getComponent<Engine::Core::TransformComponent>();
  213. auto *attacker_transform_component =
  214. attacker->getComponent<Engine::Core::TransformComponent>();
  215. if ((target_transform != nullptr) &&
  216. (attacker_transform_component != nullptr)) {
  217. QVector3D const attacker_pos(
  218. attacker_transform_component->position.x, 0.0F,
  219. attacker_transform_component->position.z);
  220. QVector3D const target_pos(target_transform->position.x, 0.0F,
  221. target_transform->position.z);
  222. QVector3D desired_pos = target_pos;
  223. bool hold_position = false;
  224. bool const target_is_building =
  225. target->hasComponent<Engine::Core::BuildingComponent>();
  226. if (target_is_building) {
  227. float const scale_x = target_transform->scale.x;
  228. float const scale_z = target_transform->scale.z;
  229. float const target_radius = std::max(scale_x, scale_z) * 0.5F;
  230. QVector3D direction = target_pos - attacker_pos;
  231. float const distance_sq = direction.lengthSquared();
  232. if (distance_sq > 0.000001F) {
  233. float const distance = std::sqrt(distance_sq);
  234. direction /= distance;
  235. float const desired_distance =
  236. target_radius + std::max(range - 0.2F, 0.2F);
  237. if (distance > desired_distance + 0.15F) {
  238. desired_pos = target_pos - direction * desired_distance;
  239. } else {
  240. hold_position = true;
  241. }
  242. }
  243. } else if (is_ranged_unit) {
  244. QVector3D direction = target_pos - attacker_pos;
  245. float const distance_sq = direction.lengthSquared();
  246. if (distance_sq > 0.000001F) {
  247. float const distance = std::sqrt(distance_sq);
  248. direction /= distance;
  249. float const optimal_range = range * 0.85F;
  250. if (distance > optimal_range + 0.5F) {
  251. desired_pos = target_pos - direction * optimal_range;
  252. } else {
  253. hold_position = true;
  254. }
  255. }
  256. }
  257. auto *movement =
  258. attacker->getComponent<Engine::Core::MovementComponent>();
  259. if (movement == nullptr) {
  260. movement =
  261. attacker->addComponent<Engine::Core::MovementComponent>();
  262. }
  263. if (movement != nullptr) {
  264. if (hold_position) {
  265. movement->hasTarget = false;
  266. movement->vx = 0.0F;
  267. movement->vz = 0.0F;
  268. movement->path.clear();
  269. if (attacker_transform_component != nullptr) {
  270. movement->target_x =
  271. attacker_transform_component->position.x;
  272. movement->target_y =
  273. attacker_transform_component->position.z;
  274. movement->goalX =
  275. attacker_transform_component->position.x;
  276. movement->goalY =
  277. attacker_transform_component->position.z;
  278. }
  279. } else {
  280. QVector3D planned_target(movement->target_x, 0.0F,
  281. movement->target_y);
  282. if (!movement->path.empty()) {
  283. const auto &final_node = movement->path.back();
  284. planned_target =
  285. QVector3D(final_node.first, 0.0F, final_node.second);
  286. }
  287. float const diff_sq =
  288. (planned_target - desired_pos).lengthSquared();
  289. bool need_new_command = !movement->pathPending;
  290. if (movement->hasTarget && diff_sq <= 0.25F * 0.25F) {
  291. need_new_command = false;
  292. }
  293. if (need_new_command) {
  294. CommandService::MoveOptions options;
  295. options.clearAttackIntent = false;
  296. options.allowDirectFallback = true;
  297. std::vector<Engine::Core::EntityID> const unit_ids = {
  298. attacker->getId()};
  299. std::vector<QVector3D> const move_targets = {desired_pos};
  300. CommandService::moveUnits(*world, unit_ids, move_targets,
  301. options);
  302. }
  303. }
  304. }
  305. }
  306. if (isInRange(attacker, target, range)) {
  307. best_target = target;
  308. }
  309. }
  310. } else {
  311. attacker->removeComponent<Engine::Core::AttackTargetComponent>();
  312. }
  313. } else {
  314. attacker->removeComponent<Engine::Core::AttackTargetComponent>();
  315. }
  316. } else {
  317. attacker->removeComponent<Engine::Core::AttackTargetComponent>();
  318. }
  319. }
  320. if ((best_target == nullptr) && (attack_target == nullptr)) {
  321. auto &owner_registry = Game::Systems::OwnerRegistry::instance();
  322. for (auto *target : units) {
  323. if (target == attacker) {
  324. continue;
  325. }
  326. auto *target_unit = target->getComponent<Engine::Core::UnitComponent>();
  327. if ((target_unit == nullptr) || target_unit->health <= 0) {
  328. continue;
  329. }
  330. if (target_unit->owner_id == attacker_unit->owner_id) {
  331. continue;
  332. }
  333. if (owner_registry.areAllies(attacker_unit->owner_id,
  334. target_unit->owner_id)) {
  335. continue;
  336. }
  337. if (target->hasComponent<Engine::Core::BuildingComponent>()) {
  338. continue;
  339. }
  340. if (isInRange(attacker, target, range)) {
  341. best_target = target;
  342. break;
  343. }
  344. }
  345. }
  346. if (best_target != nullptr) {
  347. auto *best_target_unit =
  348. best_target->getComponent<Engine::Core::UnitComponent>();
  349. if (!attacker->hasComponent<Engine::Core::AttackTargetComponent>()) {
  350. auto *new_target =
  351. attacker->addComponent<Engine::Core::AttackTargetComponent>();
  352. new_target->target_id = best_target->getId();
  353. new_target->shouldChase = false;
  354. } else {
  355. auto *existing_target =
  356. attacker->getComponent<Engine::Core::AttackTargetComponent>();
  357. if (existing_target->target_id != best_target->getId()) {
  358. existing_target->target_id = best_target->getId();
  359. existing_target->shouldChase = false;
  360. }
  361. }
  362. bool is_ranged_unit = false;
  363. if ((attacker_atk != nullptr) && attacker_atk->canRanged &&
  364. attacker_atk->currentMode ==
  365. Engine::Core::AttackComponent::CombatMode::Ranged) {
  366. is_ranged_unit = true;
  367. }
  368. if (is_ranged_unit) {
  369. auto *movement =
  370. attacker->getComponent<Engine::Core::MovementComponent>();
  371. if ((movement != nullptr) && movement->hasTarget) {
  372. movement->hasTarget = false;
  373. movement->vx = 0.0F;
  374. movement->vz = 0.0F;
  375. movement->path.clear();
  376. if (attacker_transform != nullptr) {
  377. movement->target_x = attacker_transform->position.x;
  378. movement->target_y = attacker_transform->position.z;
  379. movement->goalX = attacker_transform->position.x;
  380. movement->goalY = attacker_transform->position.z;
  381. }
  382. }
  383. }
  384. if (auto *att_t =
  385. attacker->getComponent<Engine::Core::TransformComponent>()) {
  386. if (auto *tgt_t =
  387. best_target->getComponent<Engine::Core::TransformComponent>()) {
  388. float const dx = tgt_t->position.x - att_t->position.x;
  389. float const dz = tgt_t->position.z - att_t->position.z;
  390. float const yaw =
  391. std::atan2(dx, dz) * 180.0F / std::numbers::pi_v<float>;
  392. att_t->desiredYaw = yaw;
  393. att_t->hasDesiredYaw = true;
  394. }
  395. }
  396. if (arrow_sys != nullptr) {
  397. auto *att_t =
  398. attacker->getComponent<Engine::Core::TransformComponent>();
  399. auto *tgt_t =
  400. best_target->getComponent<Engine::Core::TransformComponent>();
  401. auto *att_u = attacker->getComponent<Engine::Core::UnitComponent>();
  402. if ((attacker_atk == nullptr) ||
  403. attacker_atk->currentMode !=
  404. Engine::Core::AttackComponent::CombatMode::Melee) {
  405. QVector3D const a_pos(att_t->position.x, att_t->position.y,
  406. att_t->position.z);
  407. QVector3D const t_pos(tgt_t->position.x, tgt_t->position.y,
  408. tgt_t->position.z);
  409. QVector3D const dir = (t_pos - a_pos).normalized();
  410. QVector3D const color =
  411. (att_u != nullptr)
  412. ? Game::Visuals::team_colorForOwner(att_u->owner_id)
  413. : QVector3D(0.8F, 0.9F, 1.0F);
  414. int arrow_count = 1;
  415. if (att_u != nullptr) {
  416. int const troop_size =
  417. Game::Units::TroopConfig::instance().getIndividualsPerUnit(
  418. att_u->spawn_type);
  419. int const max_arrows = std::max(1, troop_size / 3);
  420. static thread_local std::mt19937 gen(std::random_device{}());
  421. std::uniform_int_distribution<> dist(1, max_arrows);
  422. arrow_count = dist(gen);
  423. }
  424. for (int i = 0; i < arrow_count; ++i) {
  425. static thread_local std::mt19937 spread_gen(std::random_device{}());
  426. std::uniform_real_distribution<float> spread_dist(-0.15F, 0.15F);
  427. QVector3D const perpendicular(-dir.z(), 0.0F, dir.x());
  428. QVector3D const up_vector(0.0F, 1.0F, 0.0F);
  429. float const lateral_offset = spread_dist(spread_gen);
  430. float const vertical_offset = spread_dist(spread_gen) * 1.5F;
  431. float const depth_offset = spread_dist(spread_gen) * 1.3F;
  432. QVector3D const start_offset =
  433. perpendicular * lateral_offset + up_vector * vertical_offset;
  434. QVector3D const end_offset = perpendicular * lateral_offset +
  435. up_vector * vertical_offset +
  436. dir * depth_offset;
  437. QVector3D const start = a_pos + QVector3D(0.0F, 0.6F, 0.0F) +
  438. dir * 0.35F + start_offset;
  439. QVector3D const end =
  440. t_pos + QVector3D(0.5F, 0.5F, 0.0F) + end_offset;
  441. arrow_sys->spawnArrow(start, end, color, 14.0F);
  442. }
  443. }
  444. }
  445. if ((attacker_atk != nullptr) &&
  446. attacker_atk->currentMode ==
  447. Engine::Core::AttackComponent::CombatMode::Melee) {
  448. attacker_atk->inMeleeLock = true;
  449. attacker_atk->meleeLockTargetId = best_target->getId();
  450. auto *target_atk =
  451. best_target->getComponent<Engine::Core::AttackComponent>();
  452. if (target_atk != nullptr) {
  453. target_atk->inMeleeLock = true;
  454. target_atk->meleeLockTargetId = attacker->getId();
  455. }
  456. auto *att_t =
  457. attacker->getComponent<Engine::Core::TransformComponent>();
  458. auto *tgt_t =
  459. best_target->getComponent<Engine::Core::TransformComponent>();
  460. if ((att_t != nullptr) && (tgt_t != nullptr)) {
  461. float const dx = tgt_t->position.x - att_t->position.x;
  462. float const dz = tgt_t->position.z - att_t->position.z;
  463. float const dist = std::sqrt(dx * dx + dz * dz);
  464. const float ideal_melee_distance = 0.6F;
  465. if (dist > ideal_melee_distance + 0.1F) {
  466. float const move_amount = (dist - ideal_melee_distance) * 0.5F;
  467. if (dist > 0.001F) {
  468. QVector3D const direction(dx / dist, 0.0F, dz / dist);
  469. att_t->position.x += direction.x() * move_amount;
  470. att_t->position.z += direction.z() * move_amount;
  471. tgt_t->position.x -= direction.x() * move_amount;
  472. tgt_t->position.z -= direction.z() * move_amount;
  473. }
  474. }
  475. }
  476. }
  477. dealDamage(world, best_target, damage, attacker->getId());
  478. *t_accum = 0.0F;
  479. } else {
  480. if ((attack_target == nullptr) &&
  481. attacker->hasComponent<Engine::Core::AttackTargetComponent>()) {
  482. attacker->removeComponent<Engine::Core::AttackTargetComponent>();
  483. }
  484. }
  485. }
  486. }
  487. auto CombatSystem::isInRange(Engine::Core::Entity *attacker,
  488. Engine::Core::Entity *target,
  489. float range) -> bool {
  490. auto *attacker_transform =
  491. attacker->getComponent<Engine::Core::TransformComponent>();
  492. auto *target_transform =
  493. target->getComponent<Engine::Core::TransformComponent>();
  494. if ((attacker_transform == nullptr) || (target_transform == nullptr)) {
  495. return false;
  496. }
  497. float const dx =
  498. target_transform->position.x - attacker_transform->position.x;
  499. float const dz =
  500. target_transform->position.z - attacker_transform->position.z;
  501. float const dy =
  502. target_transform->position.y - attacker_transform->position.y;
  503. float const distance_squared = dx * dx + dz * dz;
  504. float target_radius = 0.0F;
  505. if (target->hasComponent<Engine::Core::BuildingComponent>()) {
  506. float const scale_x = target_transform->scale.x;
  507. float const scale_z = target_transform->scale.z;
  508. target_radius = std::max(scale_x, scale_z) * 0.5F;
  509. } else {
  510. float const scale_x = target_transform->scale.x;
  511. float const scale_z = target_transform->scale.z;
  512. target_radius = std::max(scale_x, scale_z) * 0.5F;
  513. }
  514. float const effective_range = range + target_radius;
  515. if (distance_squared > effective_range * effective_range) {
  516. return false;
  517. }
  518. auto *attacker_atk = attacker->getComponent<Engine::Core::AttackComponent>();
  519. if ((attacker_atk != nullptr) &&
  520. attacker_atk->currentMode ==
  521. Engine::Core::AttackComponent::CombatMode::Melee) {
  522. float const height_diff = std::abs(dy);
  523. if (height_diff > attacker_atk->max_heightDifference) {
  524. return false;
  525. }
  526. }
  527. return true;
  528. }
  529. void CombatSystem::dealDamage(Engine::Core::World *world,
  530. Engine::Core::Entity *target, int damage,
  531. Engine::Core::EntityID attackerId) {
  532. auto *unit = target->getComponent<Engine::Core::UnitComponent>();
  533. if (unit != nullptr) {
  534. unit->health = std::max(0, unit->health - damage);
  535. int attacker_owner_id = 0;
  536. if (attackerId != 0 && (world != nullptr)) {
  537. auto *attacker = world->getEntity(attackerId);
  538. if (attacker != nullptr) {
  539. auto *attacker_unit =
  540. attacker->getComponent<Engine::Core::UnitComponent>();
  541. if (attacker_unit != nullptr) {
  542. attacker_owner_id = attacker_unit->owner_id;
  543. }
  544. }
  545. }
  546. if (target->hasComponent<Engine::Core::BuildingComponent>() &&
  547. unit->health > 0) {
  548. Engine::Core::EventManager::instance().publish(
  549. Engine::Core::BuildingAttackedEvent(target->getId(), unit->owner_id,
  550. unit->spawn_type, attackerId,
  551. attacker_owner_id, damage));
  552. }
  553. if (unit->health <= 0) {
  554. int const killer_owner_id = attacker_owner_id;
  555. Engine::Core::EventManager::instance().publish(
  556. Engine::Core::UnitDiedEvent(target->getId(), unit->owner_id,
  557. unit->spawn_type, attackerId,
  558. killer_owner_id));
  559. auto *target_atk = target->getComponent<Engine::Core::AttackComponent>();
  560. if ((target_atk != nullptr) && target_atk->inMeleeLock &&
  561. target_atk->meleeLockTargetId != 0) {
  562. if (world != nullptr) {
  563. auto *lock_partner = world->getEntity(target_atk->meleeLockTargetId);
  564. if ((lock_partner != nullptr) &&
  565. !lock_partner
  566. ->hasComponent<Engine::Core::PendingRemovalComponent>()) {
  567. auto *partner_atk =
  568. lock_partner->getComponent<Engine::Core::AttackComponent>();
  569. if ((partner_atk != nullptr) &&
  570. partner_atk->meleeLockTargetId == target->getId()) {
  571. partner_atk->inMeleeLock = false;
  572. partner_atk->meleeLockTargetId = 0;
  573. }
  574. }
  575. }
  576. }
  577. if (target->hasComponent<Engine::Core::BuildingComponent>()) {
  578. BuildingCollisionRegistry::instance().unregisterBuilding(
  579. target->getId());
  580. }
  581. if (auto *r = target->getComponent<Engine::Core::RenderableComponent>()) {
  582. r->visible = false;
  583. }
  584. if (auto *movement =
  585. target->getComponent<Engine::Core::MovementComponent>()) {
  586. movement->hasTarget = false;
  587. movement->vx = 0.0F;
  588. movement->vz = 0.0F;
  589. movement->path.clear();
  590. movement->pathPending = false;
  591. }
  592. target->addComponent<Engine::Core::PendingRemovalComponent>();
  593. }
  594. }
  595. }
  596. void CombatSystem::updateCombatMode(
  597. Engine::Core::Entity *attacker, Engine::Core::World *world,
  598. Engine::Core::AttackComponent *attack_comp) {
  599. if (attack_comp == nullptr) {
  600. return;
  601. }
  602. if (attack_comp->preferredMode !=
  603. Engine::Core::AttackComponent::CombatMode::Auto) {
  604. attack_comp->currentMode = attack_comp->preferredMode;
  605. return;
  606. }
  607. auto *attacker_transform =
  608. attacker->getComponent<Engine::Core::TransformComponent>();
  609. if (attacker_transform == nullptr) {
  610. return;
  611. }
  612. auto *attacker_unit = attacker->getComponent<Engine::Core::UnitComponent>();
  613. if (attacker_unit == nullptr) {
  614. return;
  615. }
  616. auto &owner_registry = Game::Systems::OwnerRegistry::instance();
  617. auto units = world->getEntitiesWith<Engine::Core::UnitComponent>();
  618. float closest_enemy_dist_sq = std::numeric_limits<float>::max();
  619. float closest_height_diff = 0.0F;
  620. for (auto *target : units) {
  621. if (target == attacker) {
  622. continue;
  623. }
  624. auto *target_unit = target->getComponent<Engine::Core::UnitComponent>();
  625. if ((target_unit == nullptr) || target_unit->health <= 0) {
  626. continue;
  627. }
  628. if (owner_registry.areAllies(attacker_unit->owner_id,
  629. target_unit->owner_id)) {
  630. continue;
  631. }
  632. auto *target_transform =
  633. target->getComponent<Engine::Core::TransformComponent>();
  634. if (target_transform == nullptr) {
  635. continue;
  636. }
  637. float const dx =
  638. target_transform->position.x - attacker_transform->position.x;
  639. float const dz =
  640. target_transform->position.z - attacker_transform->position.z;
  641. float const dy =
  642. target_transform->position.y - attacker_transform->position.y;
  643. float const dist_sq = dx * dx + dz * dz;
  644. if (dist_sq < closest_enemy_dist_sq) {
  645. closest_enemy_dist_sq = dist_sq;
  646. closest_height_diff = std::abs(dy);
  647. }
  648. }
  649. if (closest_enemy_dist_sq == std::numeric_limits<float>::max()) {
  650. if (attack_comp->canRanged) {
  651. attack_comp->currentMode =
  652. Engine::Core::AttackComponent::CombatMode::Ranged;
  653. } else {
  654. attack_comp->currentMode =
  655. Engine::Core::AttackComponent::CombatMode::Melee;
  656. }
  657. return;
  658. }
  659. float const closest_dist = std::sqrt(closest_enemy_dist_sq);
  660. bool const in_melee_range =
  661. attack_comp->isInMeleeRange(closest_dist, closest_height_diff);
  662. bool const in_ranged_range = attack_comp->isInRangedRange(closest_dist);
  663. if (in_melee_range && attack_comp->canMelee) {
  664. attack_comp->currentMode = Engine::Core::AttackComponent::CombatMode::Melee;
  665. } else if (in_ranged_range && attack_comp->canRanged) {
  666. attack_comp->currentMode =
  667. Engine::Core::AttackComponent::CombatMode::Ranged;
  668. } else if (attack_comp->canRanged) {
  669. attack_comp->currentMode =
  670. Engine::Core::AttackComponent::CombatMode::Ranged;
  671. } else {
  672. attack_comp->currentMode = Engine::Core::AttackComponent::CombatMode::Melee;
  673. }
  674. }
  675. void CombatSystem::processAutoEngagement(Engine::Core::World *world,
  676. float deltaTime) {
  677. auto units = world->getEntitiesWith<Engine::Core::UnitComponent>();
  678. for (auto it = m_engagementCooldowns.begin();
  679. it != m_engagementCooldowns.end();) {
  680. it->second -= deltaTime;
  681. if (it->second <= 0.0F) {
  682. it = m_engagementCooldowns.erase(it);
  683. } else {
  684. ++it;
  685. }
  686. }
  687. for (auto *unit : units) {
  688. if (unit->hasComponent<Engine::Core::PendingRemovalComponent>()) {
  689. continue;
  690. }
  691. auto *unit_comp = unit->getComponent<Engine::Core::UnitComponent>();
  692. if ((unit_comp == nullptr) || unit_comp->health <= 0) {
  693. continue;
  694. }
  695. if (unit->hasComponent<Engine::Core::BuildingComponent>()) {
  696. continue;
  697. }
  698. auto *attack_comp = unit->getComponent<Engine::Core::AttackComponent>();
  699. if ((attack_comp == nullptr) || !attack_comp->canMelee) {
  700. continue;
  701. }
  702. if (attack_comp->canRanged &&
  703. attack_comp->preferredMode !=
  704. Engine::Core::AttackComponent::CombatMode::Melee) {
  705. continue;
  706. }
  707. if (m_engagementCooldowns.find(unit->getId()) !=
  708. m_engagementCooldowns.end()) {
  709. continue;
  710. }
  711. if (!isUnitIdle(unit)) {
  712. continue;
  713. }
  714. float const vision_range = unit_comp->vision_range;
  715. auto *nearest_enemy = findNearestEnemy(unit, world, vision_range);
  716. if (nearest_enemy != nullptr) {
  717. auto *attack_target =
  718. unit->getComponent<Engine::Core::AttackTargetComponent>();
  719. if (attack_target == nullptr) {
  720. attack_target =
  721. unit->addComponent<Engine::Core::AttackTargetComponent>();
  722. }
  723. if (attack_target != nullptr) {
  724. attack_target->target_id = nearest_enemy->getId();
  725. attack_target->shouldChase = true;
  726. m_engagementCooldowns[unit->getId()] = ENGAGEMENT_COOLDOWN;
  727. }
  728. }
  729. }
  730. }
  731. auto CombatSystem::isUnitIdle(Engine::Core::Entity *unit) -> bool {
  732. auto *hold_mode = unit->getComponent<Engine::Core::HoldModeComponent>();
  733. if ((hold_mode != nullptr) && hold_mode->active) {
  734. return false;
  735. }
  736. auto *attack_target =
  737. unit->getComponent<Engine::Core::AttackTargetComponent>();
  738. if ((attack_target != nullptr) && attack_target->target_id != 0) {
  739. return false;
  740. }
  741. auto *movement = unit->getComponent<Engine::Core::MovementComponent>();
  742. if ((movement != nullptr) && movement->hasTarget) {
  743. return false;
  744. }
  745. auto *attack_comp = unit->getComponent<Engine::Core::AttackComponent>();
  746. if ((attack_comp != nullptr) && attack_comp->inMeleeLock) {
  747. return false;
  748. }
  749. auto *patrol = unit->getComponent<Engine::Core::PatrolComponent>();
  750. return (patrol == nullptr) || !patrol->patrolling;
  751. }
  752. auto CombatSystem::findNearestEnemy(Engine::Core::Entity *unit,
  753. Engine::Core::World *world,
  754. float maxRange) -> Engine::Core::Entity * {
  755. auto *unit_comp = unit->getComponent<Engine::Core::UnitComponent>();
  756. auto *unit_transform = unit->getComponent<Engine::Core::TransformComponent>();
  757. if ((unit_comp == nullptr) || (unit_transform == nullptr)) {
  758. return nullptr;
  759. }
  760. auto &owner_registry = Game::Systems::OwnerRegistry::instance();
  761. auto units = world->getEntitiesWith<Engine::Core::UnitComponent>();
  762. Engine::Core::Entity *nearest_enemy = nullptr;
  763. float nearest_dist_sq = maxRange * maxRange;
  764. for (auto *target : units) {
  765. if (target == unit) {
  766. continue;
  767. }
  768. if (target->hasComponent<Engine::Core::PendingRemovalComponent>()) {
  769. continue;
  770. }
  771. auto *target_unit = target->getComponent<Engine::Core::UnitComponent>();
  772. if ((target_unit == nullptr) || target_unit->health <= 0) {
  773. continue;
  774. }
  775. if (target_unit->owner_id == unit_comp->owner_id) {
  776. continue;
  777. }
  778. if (owner_registry.areAllies(unit_comp->owner_id, target_unit->owner_id)) {
  779. continue;
  780. }
  781. if (target->hasComponent<Engine::Core::BuildingComponent>()) {
  782. continue;
  783. }
  784. auto *target_transform =
  785. target->getComponent<Engine::Core::TransformComponent>();
  786. if (target_transform == nullptr) {
  787. continue;
  788. }
  789. float const dx = target_transform->position.x - unit_transform->position.x;
  790. float const dz = target_transform->position.z - unit_transform->position.z;
  791. float const dist_sq = dx * dx + dz * dz;
  792. if (dist_sq < nearest_dist_sq) {
  793. nearest_dist_sq = dist_sq;
  794. nearest_enemy = target;
  795. }
  796. }
  797. return nearest_enemy;
  798. }
  799. } // namespace Game::Systems