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

JackAshwell11 / Hades / 15798972019

21 Jun 2025 07:32PM UTC coverage: 70.171% (-0.1%) from 70.274%
15798972019

push

github

JackAshwell11
Introduced a new `Shop` tile type in the lobby allowing the player to open the shop view when pressing the `E` key. This required separating the inventory and shop layouts in the `Player` module into separate `Player` and `Shop` modules, each dealing with their respective functionality.

Introduced a new `ShopOpen` event allowing Python to show the `Shop` view upon being notified. This should allow further refactors and moving more Python code to the C++ module.

Improved the UI design of `Player` and `Shop` by integrating the `StatsLayout` into the `PaginatedGridLayout` making for a more cohesive and understandable UI.

Renamed `GameEngine::is_player_touching_goal()` to `GameEngine::is_player_touching_type()` allowing it to be used for different `GameObjectType`s.

24 of 244 new or added lines in 12 files covered. (9.84%)

80 existing lines in 6 files now uncovered.

1435 of 2045 relevant lines covered (70.17%)

34025.69 hits per line

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

98.61
/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 {
2✔
40
  static const std::unordered_map<std::string, std::type_index> type_map{{"Health", typeid(Health)},
41
                                                                         {"Armour", typeid(Armour)}};
7✔
42
  if (type_map.contains(type)) {
2✔
43
    return type_map.at(type);
2✔
44
  }
UNCOV
45
  throw std::runtime_error("Unknown component type: " + type);
×
46
}
1✔
47

48
/// Get a mapping from tile types to game object types.
49
///
50
/// @return A constant reference to the mapping.
51
auto get_tile_to_game_object_type() -> const std::unordered_map<TileType, GameObjectType> & {
51,828✔
52
  static const std::unordered_map<TileType, GameObjectType> mapping{
53
      {TileType::Floor, GameObjectType::Floor},
54
      {TileType::Wall, GameObjectType::Wall},
55
      {TileType::Goal, GameObjectType::Goal},
56
      {TileType::Player, GameObjectType::Player},
57
      {TileType::HealthPotion, GameObjectType::HealthPotion},
58
      {TileType::Chest, GameObjectType::Chest},
59
      {TileType::Shop, GameObjectType::Shop}};
51,830✔
60
  return mapping;
51,828✔
61
}
62
}  // namespace
63

64
GameEngine::GameEngine() {
52✔
65
  registry_.add_system<ArmourRegenSystem>();
52✔
66
  registry_.add_system<AttackSystem>();
52✔
67
  registry_.add_system<DamageSystem>();
52✔
68
  registry_.add_system<EffectSystem>();
52✔
69
  registry_.add_system<FootprintSystem>();
52✔
70
  registry_.add_system<InventorySystem>();
52✔
71
  registry_.add_system<KeyboardMovementSystem>();
52✔
72
  registry_.add_system<PhysicsSystem>();
52✔
73
  registry_.add_system<ShopSystem>();
52✔
74
  registry_.add_system<SteeringMovementSystem>();
52✔
75
}
52✔
76

77
auto GameEngine::is_player_touching_type(const GameObjectType game_object_type) const -> bool {
37✔
78
  return get_nearest_item() != -1 && registry_.get_game_object_type(get_nearest_item()) == game_object_type;
37✔
79
}
80

81
void GameEngine::set_seed(const unsigned int seed) { game_state_.dungeon_run.random_generator.seed(seed); }
50✔
82

83
void GameEngine::reset_level(const LevelType level_type) {
116✔
84
  // Always preserve the player for the current dungeon run
85
  if (get_player_id() != -1) {
116✔
86
    std::unordered_set<GameObjectID> preserved_ids;
65✔
87
    if (level_type != LevelType::Lobby) {
65✔
88
      preserved_ids.insert(get_player_id());
57✔
89
      const auto inventory{registry_.get_component<Inventory>(get_player_id())};
57✔
90
      preserved_ids.insert(inventory->items.begin(), inventory->items.end());
57✔
91
    }
57✔
92
    registry_.clear_game_objects(preserved_ids);
65✔
93
  }
65✔
94

95
  // Set up the player and game state
96
  auto &dungeon_run{game_state_.dungeon_run};
116✔
97
  game_state_.current_level = {.floor_positions = {},
116✔
98
                               .is_lobby = (level_type == LevelType::Lobby),
116✔
99
                               .is_boss_level = (level_type == LevelType::Boss)};
232✔
100
  if (level_type == LevelType::Lobby) {
116✔
101
    create_player();
58✔
102
    dungeon_run = {.player_id = get_player_id(),
174✔
103
                   .game_level = registry_.get_component<PlayerLevel>(get_player_id())->level,
116✔
104
                   .level_distribution = {}};
116✔
105
    dungeon_run.level_distribution = std::normal_distribution<>(get_game_level(), LEVEL_DISTRIBUTION_DEVIATION);
58✔
106
  }
107
  dungeon_run.dungeon_level++;
116✔
108

109
  // Create the game objects for the level
110
  Grid grid(0, 0);
116✔
111
  if (level_type == LevelType::Lobby) {
116✔
112
    grid = MapGenerator{}.place_lobby().get_grid();
58✔
113
  } else {
114
    grid = MapGenerator{get_game_level(), dungeon_run.random_generator}
116✔
115
               .generate_rooms()
116✔
116
               .place_obstacles()
58✔
117
               .create_connections()
58✔
118
               .generate_hallways()
58✔
119
               .cellular_automata(CELLULAR_AUTOMATA_SIMULATIONS)
58✔
120
               .generate_walls()
58✔
121
               .place_player()
58✔
122
               .place_items()
58✔
123
               .place_goal()
58✔
124
               .get_grid();
58✔
125
  }
126
  create_game_objects(grid, level_type != LevelType::Lobby);
116✔
127

128
  // Notify the registry of the level reset
129
  if (level_type == LevelType::Lobby) {
115✔
130
    registry_.notify<EventType::InventoryUpdate>(std::vector<GameObjectID>{});
58✔
131
    registry_.notify<EventType::RangedAttackSwitch>(0);
58✔
132
    registry_.notify<EventType::AttackCooldownUpdate>(get_player_id(), 0.0, 0.0, 0.0);
58✔
133
    registry_.notify<EventType::StatusEffectUpdate>(std::unordered_map<StatusEffectType, double>{});
58✔
134
  }
135
}
231✔
136

137
void GameEngine::setup_shop(std::istream &stream) const {
6✔
138
  const auto shop_system{registry_.get_system<ShopSystem>()};
6✔
139
  nlohmann::json offerings;
6✔
140
  stream >> offerings;
6✔
141
  for (int i{0}; std::cmp_less(i, offerings.size()); i++) {
11✔
142
    const auto &offering{offerings[i]};
7✔
143
    const auto type{offering["type"].get<std::string>()};
7✔
144
    const auto name{offering["name"].get<std::string>()};
7✔
145
    const auto description{offering["description"].get<std::string>()};
7✔
146
    const auto icon_type{offering["icon_type"].get<std::string>()};
7✔
147
    const auto base_cost{offering["base_cost"].get<double>()};
7✔
148
    const auto cost_multiplier{offering["cost_multiplier"].get<double>()};
7✔
149
    if (type == "stat") {
7✔
150
      shop_system->add_stat_upgrade(name, description, get_component_type_from_string(offering["stat_type"]), base_cost,
6✔
151
                                    cost_multiplier, offering["base_value"], offering["value_multiplier"]);
4✔
152
    } else if (type == "component") {
5✔
153
      shop_system->add_component_unlock(name, description, base_cost, cost_multiplier);
2✔
154
    } else if (type == "item") {
3✔
155
      shop_system->add_item(name, description, base_cost, cost_multiplier);
2✔
156
    } else {
157
      throw std::runtime_error("Unknown offering type: " + type);
1✔
158
    }
159
    registry_.notify<EventType::ShopItemLoaded>(i, std::make_tuple(name, description, icon_type),
12✔
160
                                                shop_system->get_offering_cost(i, get_player_id()));
12✔
161
  }
10✔
162
}
12✔
163

164
void GameEngine::on_update(const double delta_time) {
31✔
165
  auto &current_level{game_state_.current_level};
31✔
166
  current_level.nearest_item = registry_.get_system<PhysicsSystem>()->get_nearest_item(get_player_id());
33✔
167
  if (is_player_touching_type(GameObjectType::Goal) && !current_level.is_lobby) {
29✔
168
    if (get_dungeon_level() > LEVEL_COUNT) {
3✔
169
      // Player has completed the game, return to the lobby
170
      reset_level(LevelType::Lobby);
1✔
171
    } else if (get_dungeon_level() == LEVEL_COUNT) {
2✔
172
      // Player has completed the last level, create a boss level
173
      reset_level(LevelType::Boss);
1✔
174
    } else {
175
      // Player has completed a level, reset the level
176
      reset_level(LevelType::Normal);
1✔
177
    }
178
    return;
3✔
179
  }
180
  current_level.enemy_generation_timer += delta_time;
26✔
181
  if (current_level.enemy_generation_timer >= ENEMY_GENERATION_INTERVAL &&
84✔
182
      std::cmp_less(registry_.get_game_object_ids(GameObjectType::Enemy).size(),
58✔
183
                    MapGenerator::get_enemy_limit(get_game_level()))) {
16✔
184
    generate_enemy();
11✔
185
    current_level.enemy_generation_timer = 0.0;
11✔
186
  }
187
}
188

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

191
void GameEngine::on_key_press(const int symbol, const int /*modifiers*/) const {
9✔
192
  const auto player_movement{registry_.get_component<KeyboardMovement>(get_player_id())};
9✔
193
  switch (symbol) {
9✔
194
    case KEY_W:
2✔
195
      player_movement->moving_north = true;
2✔
196
      break;
2✔
197
    case KEY_A:
2✔
198
      player_movement->moving_west = true;
2✔
199
      break;
2✔
200
    case KEY_S:
2✔
201
      player_movement->moving_south = true;
2✔
202
      break;
2✔
203
    case KEY_D:
2✔
204
      player_movement->moving_east = true;
2✔
205
      break;
2✔
206
    default:
1✔
207
      break;
1✔
208
  }
209
}
18✔
210

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

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

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

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

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

282
void GameEngine::create_player() {
58✔
283
  game_state_.dungeon_run.player_id =
58✔
284
      registry_.create_game_object(GameObjectType::Player, cpvzero, get_game_object_components(GameObjectType::Player));
116✔
285
}
58✔
286

287
void GameEngine::create_game_objects(const Grid &grid, const bool store_floor_positions) {
116✔
288
  for (auto i{0}; std::cmp_less(i, grid.grid.size()); i++) {
58,997✔
289
    const auto tile_type{grid.grid[i]};
58,882✔
290
    if (tile_type == TileType::Empty || tile_type == TileType::Obstacle) {
58,882✔
291
      continue;
7,054✔
292
    }
293

294
    // Get the game object's type and position
295
    const auto game_object_type{get_tile_to_game_object_type().at(tile_type)};
51,828✔
296
    const auto [x, y]{grid.convert_position(i)};
51,828✔
297
    const auto position{cpv(x, y)};
51,828✔
298

299
    // Store the floor position for enemy generation
300
    if (tile_type != TileType::Wall) {
51,828✔
301
      if (store_floor_positions) {
37,831✔
302
        game_state_.current_level.floor_positions.emplace_back(position);
31,045✔
303
      }
304
      if (tile_type != TileType::Floor) {
37,831✔
305
        registry_.create_game_object(GameObjectType::Floor, position,
1,254✔
306
                                     get_game_object_components(GameObjectType::Floor));
1,254✔
307
      }
308
    }
309

310
    // Handle game object creation
311
    if (tile_type == TileType::Player) {
51,828✔
312
      cpBodySetPosition(*registry_.get_component<KinematicComponent>(get_player_id())->body,
116✔
313
                        grid_pos_to_pixel(position));
314
    } else {
315
      registry_.create_game_object(game_object_type, position, get_game_object_components(game_object_type));
51,712✔
316
    }
317
  }
318
}
115✔
319

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

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

344
auto GameEngine::get_game_object_components(const GameObjectType game_object_type)
52,408✔
345
    -> std::vector<std::shared_ptr<ComponentBase>> {
346
  auto &dungeon_run{game_state_.dungeon_run};
52,408✔
347
  return get_factories().at(game_object_type)(
52,408✔
348
      std::max(0, static_cast<int>(dungeon_run.level_distribution(dungeon_run.random_generator))));
52,408✔
349
}
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