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

JackAshwell11 / Hades / 16128995446

07 Jul 2025 10:12PM UTC coverage: 93.907% (-0.05%) from 93.958%
16128995446

push

github

JackAshwell11
[pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/charliermarsh/ruff-pre-commit: v0.12.1 → v0.12.2](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.12.1...v0.12.2)

1865 of 1986 relevant lines covered (93.91%)

30213.62 hits per line

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

99.1
/src/hades_extensions/src/game_engine.cpp
1
// Related header
2
#include "game_engine.hpp"
3

4
// External headers
5
#include <nlohmann/json.hpp>
6

7
// Local headers
8
#include "ecs/systems/armour_regen.hpp"
9
#include "ecs/systems/attacks.hpp"
10
#include "ecs/systems/effects.hpp"
11
#include "ecs/systems/inventory.hpp"
12
#include "ecs/systems/level.hpp"
13
#include "ecs/systems/movements.hpp"
14
#include "ecs/systems/physics.hpp"
15
#include "ecs/systems/shop.hpp"
16
#include "factories.hpp"
17

18
namespace {
19
/// The deviation of the level distribution.
20
constexpr int LEVEL_DISTRIBUTION_DEVIATION{2};
21

22
/// The number of cellular automata runs to perform.
23
constexpr int CELLULAR_AUTOMATA_SIMULATIONS{3};
24

25
/// The distance from the player to generate an enemy.
26
constexpr double ENEMY_GENERATION_DISTANCE{5 * SPRITE_SIZE};
27

28
/// The interval in seconds between enemy generations.
29
constexpr double ENEMY_GENERATION_INTERVAL{1.0};
30

31
/// The number of non-boss levels in the game.
32
constexpr int LEVEL_COUNT{2};
33

34
/// Get a component type from a string.
35
///
36
/// @param type - The string representation of the component type.
37
/// @throws std::runtime_error if the type is not recognised.
38
/// @return The type index of the component type.
39
auto get_component_type_from_string(const std::string &type) -> std::type_index {
3✔
40
  static const std::unordered_map<std::string, std::type_index> type_map{{"Health", typeid(Health)},
41
                                                                         {"Armour", typeid(Armour)}};
8✔
42
  if (type_map.contains(type)) {
3✔
43
    return type_map.at(type);
2✔
44
  }
45
  throw std::runtime_error("Unknown component type: " + type);
1✔
46
}
1✔
47
}  // namespace
48

49
GameEngine::GameEngine() {
58✔
50
  registry_.add_system<ArmourRegenSystem>();
58✔
51
  registry_.add_system<AttackSystem>();
58✔
52
  registry_.add_system<DamageSystem>();
58✔
53
  registry_.add_system<EffectSystem>();
58✔
54
  registry_.add_system<FootprintSystem>();
58✔
55
  registry_.add_system<InventorySystem>();
58✔
56
  registry_.add_system<KeyboardMovementSystem>();
58✔
57
  registry_.add_system<PhysicsSystem>();
58✔
58
  registry_.add_system<ShopSystem>();
58✔
59
  registry_.add_system<SteeringMovementSystem>();
58✔
60
}
58✔
61

62
auto GameEngine::is_player_touching_type(const GameObjectType game_object_type) const -> bool {
37✔
63
  return get_nearest_item() != -1 && registry_.get_game_object_type(get_nearest_item()) == game_object_type;
37✔
64
}
65

66
void GameEngine::set_seed(const std::string &seed) {
56✔
67
  constexpr std::hash<std::string> hasher;
56✔
68
  game_state_.dungeon_run.random_generator.seed(static_cast<unsigned int>(hasher(seed)));
56✔
69
}
56✔
70

71
void GameEngine::reset_level(const LevelType level_type) {
130✔
72
  // Always preserve the player for the current dungeon run
73
  if (get_player_id() != -1) {
130✔
74
    std::unordered_set<GameObjectID> preserved_ids;
73✔
75
    if (level_type != LevelType::Lobby) {
73✔
76
      preserved_ids.insert(get_player_id());
63✔
77
      const auto inventory{registry_.get_component<Inventory>(get_player_id())};
63✔
78
      preserved_ids.insert(inventory->items.begin(), inventory->items.end());
63✔
79
    }
63✔
80
    registry_.clear_game_objects(preserved_ids);
73✔
81
  }
73✔
82

83
  // Set up the player and game state
84
  auto &dungeon_run{game_state_.dungeon_run};
130✔
85
  game_state_.current_level = {.floor_positions = {},
130✔
86
                               .is_lobby = (level_type == LevelType::Lobby),
130✔
87
                               .is_boss_level = (level_type == LevelType::Boss)};
260✔
88
  if (level_type == LevelType::Lobby) {
130✔
89
    create_player();
66✔
90
    dungeon_run = {.player_id = get_player_id(),
198✔
91
                   .game_level = registry_.get_component<PlayerLevel>(get_player_id())->level,
132✔
92
                   .level_distribution = {}};
132✔
93
    dungeon_run.level_distribution = std::normal_distribution<>(get_game_level(), LEVEL_DISTRIBUTION_DEVIATION);
66✔
94
  }
95
  dungeon_run.dungeon_level++;
130✔
96

97
  // Create the game objects for the level
98
  Grid grid(0, 0);
130✔
99
  if (level_type == LevelType::Lobby) {
130✔
100
    grid = MapGenerator{}.place_lobby().get_grid();
66✔
101
  } else {
102
    grid = MapGenerator{get_game_level(), dungeon_run.random_generator}
128✔
103
               .generate_rooms()
128✔
104
               .place_obstacles()
64✔
105
               .create_connections()
64✔
106
               .generate_hallways()
64✔
107
               .cellular_automata(CELLULAR_AUTOMATA_SIMULATIONS)
64✔
108
               .generate_walls()
64✔
109
               .place_player()
64✔
110
               .place_items()
64✔
111
               .place_goal()
64✔
112
               .get_grid();
64✔
113
  }
114
  create_game_objects(grid, level_type != LevelType::Lobby);
130✔
115

116
  // Notify the registry of the level reset
117
  if (level_type == LevelType::Lobby) {
129✔
118
    registry_.notify<EventType::InventoryUpdate>(std::vector<GameObjectID>{});
66✔
119
    registry_.notify<EventType::RangedAttackSwitch>(0);
66✔
120
    registry_.notify<EventType::AttackCooldownUpdate>(get_player_id(), 0.0, 0.0, 0.0);
66✔
121
    registry_.notify<EventType::StatusEffectUpdate>(std::unordered_map<StatusEffectType, double>{});
66✔
122
  }
123
}
259✔
124

125
void GameEngine::setup_shop(std::istream &stream) const {
8✔
126
  const auto shop_system{registry_.get_system<ShopSystem>()};
8✔
127
  nlohmann::json offerings;
8✔
128
  stream >> offerings;
8✔
129
  for (int i{0}; std::cmp_less(i, offerings.size()); i++) {
12✔
130
    const auto &offering{offerings[i]};
8✔
131
    const auto type{offering["type"].get<std::string>()};
8✔
132
    const auto name{offering["name"].get<std::string>()};
8✔
133
    const auto description{offering["description"].get<std::string>()};
8✔
134
    const auto icon_type{offering["icon_type"].get<std::string>()};
8✔
135
    const auto base_cost{offering["base_cost"].get<double>()};
8✔
136
    const auto cost_multiplier{offering["cost_multiplier"].get<double>()};
8✔
137
    if (type == "stat") {
8✔
138
      shop_system->add_stat_upgrade(name, description, get_component_type_from_string(offering["stat_type"]), base_cost,
10✔
139
                                    cost_multiplier, offering["base_value"], offering["value_multiplier"]);
6✔
140
    } else if (type == "component") {
5✔
141
      shop_system->add_component_unlock(name, description, base_cost, cost_multiplier);
2✔
142
    } else if (type == "item") {
3✔
143
      shop_system->add_item(name, description, base_cost, cost_multiplier);
2✔
144
    } else {
145
      throw std::runtime_error("Unknown offering type: " + type);
1✔
146
    }
147
    registry_.notify<EventType::ShopItemLoaded>(i, std::make_tuple(name, description, icon_type),
12✔
148
                                                shop_system->get_offering_cost(i, get_player_id()));
12✔
149
  }
14✔
150
}
16✔
151

152
void GameEngine::on_update(const double delta_time) {
31✔
153
  auto &current_level{game_state_.current_level};
31✔
154
  current_level.nearest_item = registry_.get_system<PhysicsSystem>()->get_nearest_item(get_player_id());
33✔
155
  if (is_player_touching_type(GameObjectType::Goal) && !current_level.is_lobby) {
29✔
156
    if (get_dungeon_level() > LEVEL_COUNT) {
3✔
157
      // Player has completed the game, return to the lobby
158
      reset_level(LevelType::Lobby);
1✔
159
    } else if (get_dungeon_level() == LEVEL_COUNT) {
2✔
160
      // Player has completed the last level, create a boss level
161
      reset_level(LevelType::Boss);
1✔
162
    } else {
163
      // Player has completed a level, reset the level
164
      reset_level(LevelType::Normal);
1✔
165
    }
166
    return;
3✔
167
  }
168
  current_level.enemy_generation_timer += delta_time;
26✔
169
  if (current_level.enemy_generation_timer >= ENEMY_GENERATION_INTERVAL &&
84✔
170
      std::cmp_less(registry_.get_game_object_ids(GameObjectType::Enemy).size(),
58✔
171
                    MapGenerator::get_enemy_limit(get_game_level()))) {
16✔
172
    generate_enemy();
11✔
173
    current_level.enemy_generation_timer = 0.0;
11✔
174
  }
175
}
176

177
void GameEngine::on_fixed_update(const double delta_time) { registry_.update(delta_time); }
1✔
178

179
void GameEngine::on_key_press(const int symbol, const int /*modifiers*/) const {
9✔
180
  const auto player_movement{registry_.get_component<KeyboardMovement>(get_player_id())};
9✔
181
  switch (symbol) {
9✔
182
    case KEY_W:
2✔
183
      player_movement->moving_north = true;
2✔
184
      break;
2✔
185
    case KEY_A:
2✔
186
      player_movement->moving_west = true;
2✔
187
      break;
2✔
188
    case KEY_S:
2✔
189
      player_movement->moving_south = true;
2✔
190
      break;
2✔
191
    case KEY_D:
2✔
192
      player_movement->moving_east = true;
2✔
193
      break;
2✔
194
    default:
1✔
195
      break;
1✔
196
  }
197
}
18✔
198

199
void GameEngine::on_key_release(const int symbol, const int /*modifiers*/) {
18✔
200
  const auto player_movement{registry_.get_component<KeyboardMovement>(get_player_id())};
18✔
201
  switch (symbol) {
18✔
202
    case KEY_W:
1✔
203
      player_movement->moving_north = false;
1✔
204
      break;
1✔
205
    case KEY_A:
1✔
206
      player_movement->moving_west = false;
1✔
207
      break;
1✔
208
    case KEY_S:
1✔
209
      player_movement->moving_south = false;
1✔
210
      break;
1✔
211
    case KEY_D:
1✔
212
      player_movement->moving_east = false;
1✔
213
      break;
1✔
214
    case KEY_C:
1✔
215
      registry_.get_system<InventorySystem>()->add_item_to_inventory(get_player_id(), get_nearest_item());
1✔
216
      break;
1✔
217
    case KEY_E:
5✔
218
      if (game_state_.current_level.is_lobby) {
5✔
219
        if (is_player_touching_type(GameObjectType::Goal)) {
3✔
220
          reset_level(LevelType::Normal);
1✔
221
        } else if (is_player_touching_type(GameObjectType::Shop)) {
2✔
222
          registry_.notify<EventType::ShopOpen>();
1✔
223
        }
224
      } else {
225
        use_item(get_player_id(), get_nearest_item());
2✔
226
      }
227
      break;
5✔
228
    case KEY_Q:
2✔
229
      if (game_state_.current_level.is_lobby) {
2✔
230
        registry_.notify<EventType::GameOptionsOpen>();
1✔
231
      }
232
      break;
2✔
233
    case KEY_Z:
1✔
234
      registry_.get_system<AttackSystem>()->previous_ranged_attack(get_player_id());
1✔
235
      break;
1✔
236
    case KEY_X:
2✔
237
      registry_.get_system<AttackSystem>()->next_ranged_attack(get_player_id());
2✔
238
      break;
2✔
239
    case KEY_I:
2✔
240
      if (!game_state_.current_level.is_lobby) {
2✔
241
        registry_.notify<EventType::InventoryOpen>();
1✔
242
      }
243
    default:
244
      break;
3✔
245
  }
246
}
36✔
247

248
auto GameEngine::on_mouse_press(const double /*x*/, const double /*y*/, const int button, const int /*modifiers*/) const
2✔
249
    -> bool {
250
  if (button == MOUSE_BUTTON_LEFT) {
2✔
251
    return registry_.get_system<AttackSystem>()->do_attack(get_player_id(), AttackType::Ranged);
1✔
252
  }
253
  return false;
1✔
254
}
255

256
void GameEngine::use_item(const GameObjectID target_id, const GameObjectID item_id) {
7✔
257
  // Check if the item is a valid game object or not
258
  if (!registry_.has_game_object(item_id)) {
7✔
259
    return;
2✔
260
  }
261

262
  // Use the item if it can be used
263
  bool used{false};
5✔
264
  if (registry_.has_component(item_id, typeid(EffectApplier))) {
5✔
265
    used = registry_.get_system<EffectSystem>()->apply_effects(item_id, target_id);
5✔
266
  }
267

268
  // If the item has been used, remove it from the inventory or the dungeon
269
  if (used) {
4✔
270
    if (const auto inventory_system{registry_.get_system<InventorySystem>()};
3✔
271
        inventory_system->has_item_in_inventory(target_id, item_id)) {
3✔
272
      inventory_system->remove_item_from_inventory(target_id, item_id);
1✔
273
    } else {
274
      registry_.delete_game_object(item_id);
2✔
275
    }
3✔
276
  }
277
}
278

279
void GameEngine::create_player() {
66✔
280
  game_state_.dungeon_run.player_id =
66✔
281
      registry_.create_game_object(GameObjectType::Player, cpvzero, get_game_object_components(GameObjectType::Player));
132✔
282
}
66✔
283

284
void GameEngine::create_game_objects(const Grid &grid, const bool store_floor_positions) {
130✔
285
  for (auto i{0}; std::cmp_less(i, grid.grid.size()); i++) {
65,642✔
286
    const auto game_object_type{grid.grid[i]};
65,513✔
287
    if (game_object_type == GameObjectType::Empty || game_object_type == GameObjectType::Obstacle) {
65,513✔
288
      continue;
7,608✔
289
    }
290

291
    // Get the game object's position
292
    const auto [x, y]{grid.convert_position(i)};
57,905✔
293
    const auto position{cpv(x, y)};
57,905✔
294

295
    // Store the floor position for enemy generation
296
    if (game_object_type != GameObjectType::Wall) {
57,905✔
297
      if (store_floor_positions) {
42,294✔
298
        game_state_.current_level.floor_positions.emplace_back(position);
34,572✔
299
      }
300
      if (game_object_type != GameObjectType::Floor) {
42,294✔
301
        registry_.create_game_object(GameObjectType::Floor, position,
1,406✔
302
                                     get_game_object_components(GameObjectType::Floor));
1,406✔
303
      }
304
    }
305

306
    // Handle game object creation
307
    if (game_object_type == GameObjectType::Player) {
57,905✔
308
      cpBodySetPosition(*registry_.get_component<KinematicComponent>(get_player_id())->body,
130✔
309
                        grid_pos_to_pixel(position));
310
    } else {
311
      registry_.create_game_object(game_object_type, position, get_game_object_components(game_object_type));
57,775✔
312
    }
313
  }
314
}
129✔
315

316
void GameEngine::generate_enemy() {
11✔
317
  // Get a random floor position and check if it is valid for enemy generation
318
  const auto &current_level{game_state_.current_level};
11✔
319
  if (current_level.floor_positions.empty()) {
11✔
320
    return;
×
321
  }
322
  auto dist{std::uniform_int_distribution<size_t>(0, current_level.floor_positions.size() - 1)};
11✔
323
  const auto position{current_level.floor_positions.at(dist(game_state_.dungeon_run.random_generator))};
11✔
324
  const bool intersecting_enemies{
325
      cpSpacePointQueryNearest(registry_.get_space(), position, 0.0,
11✔
326
                               {CP_NO_GROUP, CP_ALL_CATEGORIES, static_cast<cpBitmask>(GameObjectType::Enemy)},
327
                               nullptr) != nullptr};
11✔
328
  if (const auto player_position{
11✔
329
          cpBodyGetPosition(*registry_.get_component<KinematicComponent>(get_player_id())->body)};
11✔
330
      intersecting_enemies || cpvdist(position, player_position) < ENEMY_GENERATION_DISTANCE) {
11✔
331
    return;
×
332
  }
333

334
  // Generate the enemy at the position
335
  const auto enemy_id{
336
      registry_.create_game_object(GameObjectType::Enemy, position, get_game_object_components(GameObjectType::Enemy))};
11✔
337
  registry_.get_component<SteeringMovement>(enemy_id)->target_id = get_player_id();
11✔
338
}
339

340
auto GameEngine::get_game_object_components(const GameObjectType game_object_type)
58,555✔
341
    -> std::vector<std::shared_ptr<ComponentBase>> {
342
  auto &dungeon_run{game_state_.dungeon_run};
58,555✔
343
  return get_factories().at(game_object_type)(
58,555✔
344
      std::max(0, static_cast<int>(dungeon_run.level_distribution(dungeon_run.random_generator))));
58,555✔
345
}
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