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

JackAshwell11 / Hades / 17672255871

12 Sep 2025 10:47AM UTC coverage: 95.21% (+0.2%) from 94.994%
17672255871

push

github

JackAshwell11
Moved `GameScene.on_mouse_motion()` to `InputHandler::on_mouse_motion()` allowing the Python module to be reduced even more. This did require adding `GameState::set_window_size()` allowing the C++ module to know what the size of the window currently is.

Removed `DynamicSprite` and moved sprite position updating to `PhysicsSystem::update()` which uses the new `PositionChanged` event to notify the Python module of position updates. This also allowed `GameScene.on_update()` to directly use the player sprite position instead of going through the `KinematicComponent`.

Removed the `components` extension module along with many other bindings as they were no longer used.

26 of 27 new or added lines in 6 files covered. (96.3%)

19 existing lines in 6 files now uncovered.

2246 of 2359 relevant lines covered (95.21%)

22253.7 hits per line

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

98.15
/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
#include <utility>
7

8
// External headers
9
#include <nlohmann/json.hpp>
10

11
// Local headers
12
#include "ecs/registry.hpp"
13
#include "ecs/systems/movements.hpp"
14
#include "events.hpp"
15

16
namespace {
17
/// The size of the cone angle for the multi-bullet attack (45 degrees).
18
constexpr auto MULTI_BULLET_CONE_ANGLE{std::numbers::pi / 4};
19

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

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

56
void AttackStat::to_file(nlohmann::json& json) const { to_file_base(json); }
946✔
57

58
void AttackStat::from_file(const nlohmann::json& json) { from_file_base(json); }
31✔
59

60
void SingleBulletAttack::perform_attack(const Registry* registry, const GameObjectID game_object_id) const {
3✔
61
  create_bullet_cone(registry, game_object_id, 1, velocity.get_value(), damage.get_value());
3✔
62
}
3✔
63

64
void MultiBulletAttack::perform_attack(const Registry* registry, const GameObjectID game_object_id) const {
1✔
65
  create_bullet_cone(registry, game_object_id, static_cast<int>(bullet_count.get_value()), velocity.get_value(),
1✔
66
                     damage.get_value());
1✔
67
}
1✔
68

69
void MeleeAttack::perform_attack(const Registry* registry, const GameObjectID game_object_id) const {
1✔
70
  const auto kinematic_component{registry->get_component<KinematicComponent>(game_object_id)};
1✔
71
  const auto current_position{cpBodyGetPosition(*kinematic_component->body)};
1✔
72
  const auto direction{cpvforangle(kinematic_component->rotation)};
1✔
73
  for (const auto target : get_target_ids(registry, game_object_id)) {
9✔
74
    const auto target_position{cpBodyGetPosition(*registry->get_component<KinematicComponent>(target)->body)};
8✔
75
    const auto target_direction{cpvsub(target_position, current_position)};
8✔
76

77
    // Check if the target is within the attack range and circle sector
78
    if (const auto distance{cpvdist(current_position, target_position)}; distance <= range.get_value()) {
8✔
79
      if (const auto theta{std::atan2(cpvcross(direction, target_direction), cpvdot(direction, target_direction))};
6✔
80
          theta >= -size.get_value() && theta <= size.get_value()) {
6✔
81
        registry->get_system<DamageSystem>()->deal_damage(target, damage.get_value());
5✔
82
      }
83
    }
84
  }
1✔
85
}
2✔
86

87
void AreaOfEffectAttack::perform_attack(const Registry* registry, const GameObjectID game_object_id) const {
1✔
88
  const auto kinematic_component{registry->get_component<KinematicComponent>(game_object_id)};
1✔
89
  for (const auto target : get_target_ids(registry, game_object_id)) {
9✔
90
    if (cpvdist(cpBodyGetPosition(*kinematic_component->body),
16✔
91
                cpBodyGetPosition(*registry->get_component<KinematicComponent>(target)->body)) <= range.get_value()) {
16✔
92
      registry->get_system<DamageSystem>()->deal_damage(target, damage.get_value());
6✔
93
    }
94
  }
1✔
95
}
2✔
96

97
void Attack::reset() { selected_ranged_attack = 0; }
22✔
98

99
void Attack::to_file(nlohmann::json& json) const {
62✔
100
  json["selected_ranged_attack"] = selected_ranged_attack;
62✔
101
  json["ranged_attack"] = nlohmann::json::array();
62✔
102
  for (const auto& ranged_attack : ranged_attacks) {
186✔
103
    nlohmann::json ranged_json;
124✔
104
    ranged_attack->cooldown.to_file(ranged_json["cooldown"]);
124✔
105
    ranged_attack->damage.to_file(ranged_json["damage"]);
124✔
106
    ranged_attack->range.to_file(ranged_json["range"]);
124✔
107
    ranged_attack->velocity.to_file(ranged_json["velocity"]);
124✔
108
    json["ranged_attack"].push_back(ranged_json);
124✔
109
  }
124✔
110
  if (melee_attack) {
62✔
111
    nlohmann::json melee_json;
62✔
112
    melee_attack->cooldown.to_file(melee_json["cooldown"]);
62✔
113
    melee_attack->damage.to_file(melee_json["damage"]);
62✔
114
    melee_attack->range.to_file(melee_json["range"]);
62✔
115
    melee_attack->size.to_file(melee_json["size"]);
62✔
116
    json["melee_attack"] = melee_json;
62✔
117
  }
62✔
118
  if (special_attack) {
62✔
119
    nlohmann::json special_json;
62✔
120
    special_attack->cooldown.to_file(special_json["cooldown"]);
62✔
121
    special_attack->damage.to_file(special_json["damage"]);
62✔
122
    special_attack->range.to_file(special_json["range"]);
62✔
123
    json["special_attack"] = special_json;
62✔
124
  }
62✔
125
}
62✔
126

127
void Attack::from_file(const nlohmann::json& json) {
2✔
128
  selected_ranged_attack = json.at("selected_ranged_attack").get<int>();
2✔
129
  for (auto i{0}; std::cmp_less(i, json.at("ranged_attack").size()); i++) {
6✔
130
    const nlohmann::json& ranged_json{json.at("ranged_attack")[i]};
4✔
131
    ranged_attacks.at(i)->cooldown.from_file(ranged_json.at("cooldown"));
4✔
132
    ranged_attacks.at(i)->damage.from_file(ranged_json.at("damage"));
4✔
133
    ranged_attacks.at(i)->range.from_file(ranged_json.at("range"));
4✔
134
    ranged_attacks.at(i)->velocity.from_file(ranged_json.at("velocity"));
4✔
135
  }
136
  if (json.contains("melee_attack")) {
2✔
137
    const nlohmann::json& json_melee{json.at("melee_attack")};
2✔
138
    melee_attack->cooldown.from_file(json_melee.at("cooldown"));
2✔
139
    melee_attack->damage.from_file(json_melee.at("damage"));
2✔
140
    melee_attack->range.from_file(json_melee.at("range"));
2✔
141
    melee_attack->size.from_file(json_melee.at("size"));
2✔
142
  }
143
  if (json.contains("special_attack")) {
2✔
144
    const nlohmann::json& json_special{json.at("special_attack")};
2✔
145
    special_attack->cooldown.from_file(json_special.at("cooldown"));
2✔
146
    special_attack->damage.from_file(json_special.at("damage"));
2✔
147
    special_attack->range.from_file(json_special.at("range"));
2✔
148
  }
149
}
2✔
150

151
void AttackSystem::update(const double delta_time) const {
18✔
152
  for (const auto& [game_object_id, component_tuple] : get_registry()->get_game_object_components<Attack>()) {
54✔
153
    const auto [attack]{component_tuple};
18✔
154
    for (const auto& ranged_attack : attack->ranged_attacks) {
44✔
155
      ranged_attack->update(delta_time);
26✔
156
    }
157
    if (attack->melee_attack) {
18✔
158
      attack->melee_attack->update(delta_time);
6✔
159
    }
160
    if (attack->special_attack) {
18✔
161
      attack->special_attack->update(delta_time);
6✔
162
    }
163
    notify<EventType::AttackCooldownUpdate>(
18✔
164
        game_object_id,
165
        std::cmp_less(attack->selected_ranged_attack, attack->ranged_attacks.size())
18✔
166
            ? attack->ranged_attacks[attack->selected_ranged_attack]->get_time_until_attack()
54✔
167
            : 0.0,
168
        attack->melee_attack ? attack->melee_attack->get_time_until_attack() : 0.0,
36✔
169
        attack->special_attack ? attack->special_attack->get_time_until_attack() : 0.0);
36✔
170

171
    // If the game object has a steering movement component, they are in the target state, and their cooldown is up,
172
    // then attack
173
    if (get_registry()->has_component<SteeringMovement>(game_object_id) && !attack->ranged_attacks.empty()) {
18✔
174
      if (const auto steering_movement{get_registry()->get_component<SteeringMovement>(game_object_id)};
3✔
175
          steering_movement->movement_state == SteeringMovementState::Target) {
3✔
176
        (void)do_attack(game_object_id, AttackType::Ranged);
1✔
177
      }
3✔
178
    }
179
  }
18✔
180
}
18✔
181

182
void AttackSystem::previous_ranged_attack(const GameObjectID game_object_id) const {
7✔
183
  if (const auto attack{get_registry()->get_component<Attack>(game_object_id)}; attack->selected_ranged_attack > 0) {
7✔
184
    attack->selected_ranged_attack--;
4✔
185
    notify<EventType::RangedAttackSwitch>(attack->selected_ranged_attack);
4✔
186
  }
7✔
187
}
7✔
188

189
void AttackSystem::next_ranged_attack(const GameObjectID game_object_id) const {
10✔
190
  if (const auto attack{get_registry()->get_component<Attack>(game_object_id)};
10✔
191
      !attack->ranged_attacks.empty() &&
19✔
192
      std::cmp_less(attack->selected_ranged_attack, attack->ranged_attacks.size() - 1)) {
9✔
193
    attack->selected_ranged_attack++;
7✔
194
    notify<EventType::RangedAttackSwitch>(attack->selected_ranged_attack);
7✔
195
  }
10✔
196
}
10✔
197

198
auto AttackSystem::do_attack(const GameObjectID game_object_id, const AttackType attack_type) const -> bool {
13✔
199
  // Get the attack object based on the attack type
200
  const auto attack{get_registry()->get_component<Attack>(game_object_id)};
13✔
UNCOV
201
  BaseAttack* attack_obj{[&]() -> BaseAttack* {
×
202
    switch (attack_type) {
12✔
203
      case AttackType::Ranged:
7✔
204
        return std::cmp_greater_equal(attack->selected_ranged_attack, attack->ranged_attacks.size())
7✔
205
                   ? nullptr
7✔
206
                   : attack->get_selected_ranged_attack();
7✔
207
      case AttackType::Melee:
3✔
208
        return attack->melee_attack ? &attack->melee_attack.value() : nullptr;
3✔
209
      case AttackType::Special:
2✔
210
        return attack->special_attack ? &attack->special_attack.value() : nullptr;
2✔
UNCOV
211
      default:
×
212
        return nullptr;
×
213
    }
214
  }()};
12✔
215

216
  // Check if the game object can attack or not
217
  if (attack_obj == nullptr || !attack_obj->is_ready()) {
12✔
218
    return false;
6✔
219
  }
220

221
  // Perform the selected attack on the targets
222
  attack_obj->time_since_last_use = 0;
6✔
223
  attack_obj->perform_attack(get_registry(), game_object_id);
6✔
224
  return true;
6✔
225
}
12✔
226

227
void DamageSystem::deal_damage(const GameObjectID game_object_id, const double damage) const {
23✔
228
  // Damage the armour and carry over the extra damage to the health
229
  const auto health{get_registry()->get_component<Health>(game_object_id)};
23✔
230
  const auto armour{get_registry()->get_component<Armour>(game_object_id)};
21✔
231
  const auto old_health{health->get_value()};
21✔
232
  const auto old_armour{armour->get_value()};
21✔
233
  health->set_value(health->get_value() - std::max(damage - armour->get_value(), 0.0));
21✔
234
  armour->set_value(armour->get_value() - damage);
21✔
235
  if (health->get_value() != old_health) {
21✔
236
    notify<EventType::HealthChanged>(game_object_id, health->get_value() / health->get_max_value());
17✔
237
  }
238
  if (armour->get_value() != old_armour) {
21✔
239
    notify<EventType::ArmourChanged>(game_object_id, armour->get_value() / armour->get_max_value());
5✔
240
  }
241

242
  // If the health is now 0, delete the game object
243
  if (health->get_value() <= 0) {
21✔
244
    get_registry()->mark_for_deletion(game_object_id);
2✔
245
  }
246
}
42✔
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