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

JackAshwell11 / Hades / 17012457186

16 Aug 2025 08:13PM UTC coverage: 93.521% (-0.9%) from 94.372%
17012457186

push

github

JackAshwell11
Fixed a bug where the player sprite would not appear, but the user would still be able to control the game. This occurred because the game engine was initialised (which created the player game object) before the game scene was able to be initialised, causing the `on_game_object_creation()` callback to not be called. This was fixed by lazily loading the game engine in the model so it would be called during access in `HadesWindow.setup()`.

This also allowed other recurring problems to be fixed, such as relying on a `PythonSprite` for mapping game object IDs to sprite objects, which has now been fixed by using a `sprites` dict in `HadesModel` removing the Python dependency in the C++ library as well.

31 of 37 new or added lines in 8 files covered. (83.78%)

49 existing lines in 13 files now uncovered.

2165 of 2315 relevant lines covered (93.52%)

20669.7 hits per line

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

98.08
/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 "ecs/systems/physics.hpp"
15
#include "events.hpp"
16

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

228
void DamageSystem::deal_damage(const GameObjectID game_object_id, const double damage) const {
21✔
229
  // Damage the armour and carry over the extra damage to the health
230
  const auto health{get_registry()->get_component<Health>(game_object_id)};
21✔
231
  const auto armour{get_registry()->get_component<Armour>(game_object_id)};
19✔
232
  health->set_value(health->get_value() - std::max(damage - armour->get_value(), 0.0));
19✔
233
  armour->set_value(armour->get_value() - damage);
19✔
234

235
  // If the health is now 0, delete the game object
236
  if (health->get_value() <= 0) {
19✔
237
    get_registry()->mark_for_deletion(game_object_id);
2✔
238
  }
239
}
38✔
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