• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JackAshwell11 / Hades / 14824897810

04 May 2025 08:25PM UTC coverage: 83.234% (+0.4%) from 82.809%
14824897810

push

github

JackAshwell11
Rewrote the attacks system to be more flexible and easier to use whilst maintaining the existing functionality. This new system also implements the fundamentals for having ranged attacks be the default with melee and special attacks being complementary.

All attack algorithms are now separate and independently control their own cooldown, damage, and range (as well as any additional stats they might require), allowing each attack to be customised and upgraded individually.

`Registry::get_game_object_ids` is now const as it did not modify `Registry`'s state. `cpDataPointerToGameObjectID()` has also been changed to `cpShapeToGameObjectID()` to reduce duplication of getting a game object ID from a Chipmunk2D shape.

112 of 121 new or added lines in 8 files covered. (92.56%)

1 existing line in 1 file now uncovered.

1400 of 1682 relevant lines covered (83.23%)

9478.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.47
/src/hades_extensions/src/ecs/systems/attacks.cpp
1
// Related header
2
#include "ecs/systems/attacks.hpp"
3

4
// Std headers
5
#include <numbers>
6

7
// Local headers
8
#include "ecs/systems/movements.hpp"
9
#include "ecs/systems/physics.hpp"
10

11
namespace {
12
/// The size of the cone angle for the multi-bullet attack (45 degrees).
13
constexpr auto MULTI_BULLET_CONE_ANGLE{std::numbers::pi / 4};
14

15
/// Get the target IDs for an attack based on the game object type.
16
///
17
/// @param registry - The registry that manages the game objects, components, and systems.
18
/// @param game_object_id - The game object ID of the attacking game object.
19
/// @throws RegistryError - If the game object ID is not registered with the registry.
20
/// @return The target IDs for the attack.
21
auto get_target_ids(const Registry *registry, const GameObjectID game_object_id) -> std::vector<GameObjectID> {
2✔
22
  return registry->get_game_object_ids(registry->get_game_object_type(game_object_id) == GameObjectType::Player
4✔
23
                                           ? GameObjectType::Enemy
24
                                           : GameObjectType::Player);
2✔
25
}
26

27
/// Create multiple bullets spread out in a cone shape around the direction of the attack.
28
///
29
/// @param registry - The registry that manages the game objects, components, and systems.
30
/// @param game_object_id - The game object ID of the attacking game object.
31
/// @param bullet_count - The number of bullets to create.
32
/// @param velocity - The velocity of the bullets.
33
/// @param damage - The damage of the bullets.
34
/// @throws RegistryError - If the game object ID is not registered with the registry or does not have a kinematic
35
/// component.
36
void create_bullet_cone(const Registry *registry, const GameObjectID game_object_id, const int bullet_count,
4✔
37
                        const double velocity, const double damage) {
38
  const auto kinematic_component{registry->get_component<KinematicComponent>(game_object_id)};
4✔
39
  const auto direction{cpvforangle(kinematic_component->rotation)};
4✔
40
  const auto angle_step{bullet_count > 1 ? 2 * MULTI_BULLET_CONE_ANGLE / (bullet_count - 1) : 0.0};
4✔
41
  for (int i{0}; i < bullet_count; i++) {
12✔
42
    const auto bullet_angle{bullet_count > 1 ? -MULTI_BULLET_CONE_ANGLE + (i * angle_step) : 0.0};
8✔
43
    const auto bullet_position{cpBodyGetPosition(*kinematic_component->body) + direction * SPRITE_SIZE};
8✔
44
    const auto bullet_velocity{cpvrotate(direction, cpvforangle(bullet_angle)) * velocity};
8✔
45
    registry->get_system<PhysicsSystem>()->add_bullet({bullet_position, bullet_velocity}, damage,
8✔
46
                                                      registry->get_game_object_type(game_object_id));
47
  }
48
}
4✔
49
}  // namespace
50

51
void SingleBulletAttack::perform_attack(const Registry *registry, const GameObjectID game_object_id) const {
3✔
52
  create_bullet_cone(registry, game_object_id, 1, velocity.get_value(), damage.get_value());
3✔
53
}
3✔
54

55
void MultiBulletAttack::perform_attack(const Registry *registry, const GameObjectID game_object_id) const {
1✔
56
  create_bullet_cone(registry, game_object_id, static_cast<int>(bullet_count.get_value()), velocity.get_value(),
1✔
57
                     damage.get_value());
58
}
1✔
59

60
void MeleeAttack::perform_attack(const Registry *registry, const GameObjectID game_object_id) const {
1✔
61
  const auto kinematic_component{registry->get_component<KinematicComponent>(game_object_id)};
1✔
62
  const auto current_position{cpBodyGetPosition(*kinematic_component->body)};
1✔
63
  const auto direction{cpvforangle(kinematic_component->rotation)};
1✔
64
  for (const auto target : get_target_ids(registry, game_object_id)) {
9✔
65
    const auto target_position{cpBodyGetPosition(*registry->get_component<KinematicComponent>(target)->body)};
8✔
66
    const auto target_direction{cpvsub(target_position, current_position)};
8✔
67

68
    // Check if the target is within the attack range and circle sector
69
    if (const auto distance{cpvdist(current_position, target_position)}; distance <= range.get_value()) {
8✔
70
      if (const auto theta{std::atan2(cpvcross(direction, target_direction), cpvdot(direction, target_direction))};
6✔
71
          theta >= -size.get_value() && theta <= size.get_value()) {
6✔
72
        registry->get_system<DamageSystem>()->deal_damage(target, damage.get_value());
5✔
73
      }
74
    }
75
  }
1✔
76
}
1✔
77

78
void AreaOfEffectAttack::perform_attack(const Registry *registry, const GameObjectID game_object_id) const {
1✔
79
  const auto kinematic_component{registry->get_component<KinematicComponent>(game_object_id)};
1✔
80
  for (const auto target : get_target_ids(registry, game_object_id)) {
9✔
81
    if (cpvdist(cpBodyGetPosition(*kinematic_component->body),
8✔
82
                cpBodyGetPosition(*registry->get_component<KinematicComponent>(target)->body)) <= range.get_value()) {
16✔
83
      registry->get_system<DamageSystem>()->deal_damage(target, damage.get_value());
6✔
84
    }
85
  }
1✔
86
}
1✔
87

88
void AttackSystem::update(const double delta_time) const {
15✔
89
  for (const auto &[game_object_id, component_tuple] : get_registry()->find_components<Attack>()) {
47✔
90
    const auto [attack]{component_tuple};
16✔
91
    for (const auto &ranged_attack : attack->ranged_attacks) {
37✔
92
      ranged_attack->update(delta_time);
21✔
93
    }
94
    if (attack->melee_attack) {
16✔
95
      attack->melee_attack->update(delta_time);
3✔
96
    }
97
    if (attack->special_attack) {
16✔
98
      attack->special_attack->update(delta_time);
3✔
99
    }
100

101
    // If the game object has a steering movement component, they are in the target state, and their cooldown is up,
102
    // then attack
103
    if (get_registry()->has_component(game_object_id, typeid(SteeringMovement)) && !attack->ranged_attacks.empty()) {
16✔
104
      if (const auto steering_movement{get_registry()->get_component<SteeringMovement>(game_object_id)};
4✔
105
          steering_movement->movement_state == SteeringMovementState::Target) {
4✔
106
        (void)do_attack(game_object_id, AttackType::Ranged);
1✔
107
      }
4✔
108
    }
109
  }
16✔
110
}
15✔
111

112
auto AttackSystem::do_attack(const GameObjectID game_object_id, const AttackType attack_type) const -> bool {
13✔
113
  // Get the attack object based on the attack type
114
  const auto attack{get_registry()->get_component<Attack>(game_object_id)};
13✔
NEW
115
  BaseAttack *attack_obj{[&]() -> BaseAttack * {
×
116
    switch (attack_type) {
12✔
117
      case AttackType::Ranged:
7✔
118
        return std::cmp_greater_equal(attack->selected_ranged_attack, attack->ranged_attacks.size())
7✔
119
                   ? nullptr
7✔
120
                   : attack->get_selected_ranged_attack();
7✔
121
      case AttackType::Melee:
3✔
122
        return attack->melee_attack ? &attack->melee_attack.value() : nullptr;
3✔
123
      case AttackType::Special:
2✔
124
        return attack->special_attack ? &attack->special_attack.value() : nullptr;
2✔
NEW
125
      default:
×
NEW
126
        return nullptr;
×
127
    }
128
  }()};
12✔
129

130
  // Check if the game object can attack or not
131
  if (attack_obj == nullptr || !attack_obj->is_ready()) {
12✔
132
    return false;
6✔
133
  }
134

135
  // Perform the selected attack on the targets
136
  attack_obj->time_since_last_use = 0;
6✔
137
  attack_obj->perform_attack(get_registry(), game_object_id);
6✔
138
  return true;
6✔
139
}
12✔
140

141
void DamageSystem::deal_damage(const GameObjectID game_object_id, const double damage) const {
21✔
142
  // Damage the armour and carry over the extra damage to the health
143
  const auto health{get_registry()->get_component<Health>(game_object_id)};
21✔
144
  const auto armour{get_registry()->get_component<Armour>(game_object_id)};
19✔
145
  health->set_value(health->get_value() - std::max(damage - armour->get_value(), 0.0));
19✔
146
  armour->set_value(armour->get_value() - damage);
19✔
147

148
  // If the health is now 0, delete the game object
149
  if (health->get_value() <= 0) {
19✔
150
    get_registry()->mark_for_deletion(game_object_id);
2✔
151
  }
152
}
19✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc