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

JackAshwell11 / Hades / 15519789778

30 May 2025 02:18PM UTC coverage: 71.527% (+0.4%) from 71.083%
15519789778

push

github

JackAshwell11
Renamed `GameModel` to `HadesModel` and moved its ownership from `Game` to `HadesWindow` meaning it can be used across all views. Additionally, `registry` and `player_id` are now `cached_property`s which should improve performance.

Removed `GameEngine::generate_enemy()` from the Python bindings as its clock functionality has been moved to `GameEngine::on_update()`.

Refactored `HadesEnvironment` to no longer require all enemies to be present during the first step.

26 of 62 new or added lines in 8 files covered. (41.94%)

6 existing lines in 4 files now uncovered.

1354 of 1893 relevant lines covered (71.53%)

17231.75 hits per line

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

98.85
/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/movements.hpp"
13
#include "ecs/systems/physics.hpp"
14
#include "ecs/systems/shop.hpp"
15
#include "factories.hpp"
16

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

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

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

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

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

45
GameEngine::GameEngine(const int level, const std::optional<unsigned int> seed)
42✔
46
    : level_(level),
42✔
47
      random_generator_(seed.has_value() ? seed.value() : std::random_device{}()),
42✔
48
      level_distribution_(level, LEVEL_DISTRIBUTION_DEVIATION),
42✔
49
      registry_(std::make_shared<Registry>(random_generator_)),
43✔
50
      player_id_(-1) {
43✔
51
  if (level < 0) {
42✔
52
    throw std::length_error("Level must be bigger than or equal to 0.");
1✔
53
  }
54
  generator_ = MapGenerator{level, random_generator_};
41✔
55
  generator_.generate_rooms()
41✔
56
      .place_obstacles()
41✔
57
      .create_connections()
41✔
58
      .generate_hallways()
41✔
59
      .cellular_automata(CELLULAR_AUTOMATA_SIMULATIONS)
41✔
60
      .generate_walls()
41✔
61
      .place_player()
41✔
62
      .place_items()
41✔
63
      .place_goal();
41✔
64

65
  // Add the systems to the registry
66
  registry_->add_system<ArmourRegenSystem>();
41✔
67
  registry_->add_system<AttackSystem>();
41✔
68
  registry_->add_system<DamageSystem>();
41✔
69
  registry_->add_system<EffectSystem>();
41✔
70
  registry_->add_system<FootprintSystem>();
41✔
71
  registry_->add_system<InventorySystem>();
41✔
72
  registry_->add_system<KeyboardMovementSystem>();
41✔
73
  registry_->add_system<PhysicsSystem>();
41✔
74
  registry_->add_system<ShopSystem>();
41✔
75
  registry_->add_system<SteeringMovementSystem>();
41✔
76
}
43✔
77

78
auto GameEngine::get_level_constants() -> std::tuple<int, int, int> {
4✔
79
  return {generator_.get_grid().width, generator_.get_grid().height, generator_.get_enemy_limit()};
4✔
80
}
81

82
void GameEngine::create_game_objects() {
37✔
83
  // Create the game objects ignoring empty and obstacle tiles
84
  const auto &grid{*generator_.get_grid().grid};
37✔
85
  for (auto i{0}; std::cmp_less(i, grid.size()); i++) {
22,237✔
86
    const auto tile_type{grid[i]};
22,200✔
87
    if (tile_type == TileType::Empty || tile_type == TileType::Obstacle) {
22,200✔
88
      continue;
2,636✔
89
    }
90

91
    // If the tile is not a wall tile, we want an extra floor tile placed at the same position
92
    static const std::unordered_map<TileType, GameObjectType> tile_to_game_object_type{
93
        {TileType::Floor, GameObjectType::Floor},
94
        {TileType::Wall, GameObjectType::Wall},
95
        {TileType::Goal, GameObjectType::Goal},
96
        {TileType::Player, GameObjectType::Player},
97
        {TileType::HealthPotion, GameObjectType::HealthPotion},
98
        {TileType::Chest, GameObjectType::Chest},
99
    };
19,566✔
100
    const auto game_object_type{tile_to_game_object_type.at(tile_type)};
19,564✔
101
    const auto [x, y]{generator_.get_grid().convert_position(i)};
19,564✔
102
    if (tile_type != TileType::Wall && tile_type != TileType::Floor) {
19,564✔
103
      registry_->create_game_object(GameObjectType::Floor, cpv(x, y),
444✔
104
                                    get_game_object_components(GameObjectType::Floor));
444✔
105
    }
106
    const auto game_object_id{
107
        registry_->create_game_object(game_object_type, cpv(x, y), get_game_object_components(game_object_type))};
19,564✔
108
    if (tile_type == TileType::Player) {
19,564✔
109
      player_id_ = game_object_id;
37✔
110
    }
111
  }
112
}
37✔
113

114
void GameEngine::setup_shop(std::istream &stream) const {
6✔
115
  const auto shop_system{registry_->get_system<ShopSystem>()};
6✔
116
  nlohmann::json offerings;
6✔
117
  stream >> offerings;
6✔
118
  for (int i{0}; std::cmp_less(i, offerings.size()); i++) {
11✔
119
    const auto &offering{offerings[i]};
7✔
120
    const auto type{offering["type"].get<std::string>()};
7✔
121
    const auto name{offering["name"].get<std::string>()};
7✔
122
    const auto description{offering["description"].get<std::string>()};
7✔
123
    const auto icon_type{offering["icon_type"].get<std::string>()};
7✔
124
    const auto base_cost{offering["base_cost"].get<double>()};
7✔
125
    const auto cost_multiplier{offering["cost_multiplier"].get<double>()};
7✔
126
    if (type == "stat") {
7✔
127
      shop_system->add_stat_upgrade(name, description, get_component_type_from_string(offering["stat_type"]), base_cost,
6✔
128
                                    cost_multiplier, offering["base_value"], offering["value_multiplier"]);
4✔
129
    } else if (type == "component") {
5✔
130
      shop_system->add_component_unlock(name, description, base_cost, cost_multiplier);
2✔
131
    } else if (type == "item") {
3✔
132
      shop_system->add_item(name, description, base_cost, cost_multiplier);
2✔
133
    } else {
134
      throw std::runtime_error("Unknown offering type: " + type);
1✔
135
    }
136
    registry_->notify<EventType::ShopItemLoaded>(i, std::make_tuple(name, description, icon_type),
12✔
137
                                                 shop_system->get_offering_cost(i, player_id_));
12✔
138
  }
10✔
139
}
12✔
140

141
void GameEngine::on_update(const double delta_time) {
23✔
142
  nearest_item_ = registry_->get_system<PhysicsSystem>()->get_nearest_item(player_id_);
25✔
143
  if (nearest_item_ != -1 && registry_->get_game_object_type(nearest_item_) == GameObjectType::Goal) {
21✔
144
    registry_->delete_game_object(player_id_);
1✔
145
  }
146
  enemy_generation_timer_ += delta_time;
21✔
147
  if (enemy_generation_timer_ >= ENEMY_GENERATION_INTERVAL) {
21✔
148
    generate_enemy();
16✔
149
    enemy_generation_timer_ = 0.0;
16✔
150
  }
151
}
21✔
152

153
void GameEngine::on_fixed_update(const double delta_time) const { registry_->update(delta_time); }
1✔
154

155
void GameEngine::on_key_press(const int symbol, const int /*modifiers*/) const {
9✔
156
  const auto player_movement{registry_->get_component<KeyboardMovement>(player_id_)};
9✔
157
  switch (symbol) {
9✔
158
    case KEY_W:
2✔
159
      player_movement->moving_north = true;
2✔
160
      break;
2✔
161
    case KEY_A:
2✔
162
      player_movement->moving_west = true;
2✔
163
      break;
2✔
164
    case KEY_S:
2✔
165
      player_movement->moving_south = true;
2✔
166
      break;
2✔
167
    case KEY_D:
2✔
168
      player_movement->moving_east = true;
2✔
169
      break;
2✔
170
    default:
1✔
171
      break;
1✔
172
  }
173
}
18✔
174

175
void GameEngine::on_key_release(const int symbol, const int /*modifiers*/) const {
10✔
176
  const auto player_movement{registry_->get_component<KeyboardMovement>(player_id_)};
10✔
177
  switch (symbol) {
10✔
178
    case KEY_W:
1✔
179
      player_movement->moving_north = false;
1✔
180
      break;
1✔
181
    case KEY_A:
1✔
182
      player_movement->moving_west = false;
1✔
183
      break;
1✔
184
    case KEY_S:
1✔
185
      player_movement->moving_south = false;
1✔
186
      break;
1✔
187
    case KEY_D:
1✔
188
      player_movement->moving_east = false;
1✔
189
      break;
1✔
190
    case KEY_C:
1✔
191
      registry_->get_system<InventorySystem>()->add_item_to_inventory(player_id_, nearest_item_);
1✔
192
      break;
1✔
193
    case KEY_E:
1✔
194
      use_item(player_id_, nearest_item_);
1✔
195
      break;
1✔
196
    case KEY_Z:
1✔
197
      registry_->get_system<AttackSystem>()->previous_ranged_attack(player_id_);
1✔
198
      break;
1✔
199
    case KEY_X:
2✔
200
      registry_->get_system<AttackSystem>()->next_ranged_attack(player_id_);
2✔
201
      break;
2✔
202
    default:
1✔
203
      break;
1✔
204
  }
205
}
20✔
206

207
auto GameEngine::on_mouse_press(const double /*x*/, const double /*y*/, const int button, const int /*modifiers*/) const
2✔
208
    -> bool {
209
  if (button == MOUSE_BUTTON_LEFT) {
2✔
210
    return registry_->get_system<AttackSystem>()->do_attack(player_id_, AttackType::Ranged);
1✔
211
  }
212
  return false;
1✔
213
}
214

215
void GameEngine::use_item(const GameObjectID target_id, const GameObjectID item_id) const {
6✔
216
  // Check if the item is a valid game object or not
217
  if (!registry_->has_game_object(item_id)) {
6✔
218
    return;
1✔
219
  }
220

221
  // Use the item if it can be used
222
  bool used{false};
5✔
223
  if (registry_->has_component(item_id, typeid(EffectApplier))) {
5✔
224
    used = registry_->get_system<EffectSystem>()->apply_effects(item_id, target_id);
5✔
225
  }
226

227
  // If the item has been used, remove it from the inventory or the dungeon
228
  if (used) {
4✔
229
    if (const auto inventory_system{registry_->get_system<InventorySystem>()};
3✔
230
        inventory_system->has_item_in_inventory(target_id, item_id)) {
3✔
231
      inventory_system->remove_item_from_inventory(target_id, item_id);
1✔
232
    } else {
233
      registry_->delete_game_object(item_id);
2✔
234
    }
3✔
235
  }
236
}
237

238
void GameEngine::generate_enemy() {
16✔
239
  if (std::cmp_greater_equal(registry_->get_game_object_ids(GameObjectType::Enemy).size(),
16✔
240
                             generator_.get_enemy_limit())) {
16✔
241
    return;
6✔
242
  }
243

244
  // Collect all floor positions and shuffle them
245
  const auto &grid{*generator_.get_grid().grid};
10✔
246
  std::vector<cpVect> floor_positions;
10✔
247
  for (auto i{0}; std::cmp_less(i, grid.size()); i++) {
6,010✔
248
    if (grid[i] == TileType::Floor) {
6,000✔
249
      const auto [x, y]{generator_.get_grid().convert_position(i)};
3,820✔
250
      floor_positions.push_back(cpv(x, y));
3,820✔
251
    }
252
  }
253

254
  // Get a random floor position and check if it is valid for enemy generation
255
  auto dist{std::uniform_int_distribution<size_t>(0, floor_positions.size() - 1)};
10✔
256
  const auto position{floor_positions[dist(random_generator_)]};
10✔
257
  const bool intersecting_enemies{
258
      cpSpacePointQueryNearest(registry_->get_space(), position, 0.0,
10✔
259
                               {CP_NO_GROUP, CP_ALL_CATEGORIES, static_cast<cpBitmask>(GameObjectType::Enemy)},
260
                               nullptr) != nullptr};
10✔
261
  if (const auto player_position{cpBodyGetPosition(*registry_->get_component<KinematicComponent>(player_id_)->body)};
10✔
262
      intersecting_enemies || cpvdist(position, player_position) < ENEMY_GENERATION_DISTANCE) {
10✔
NEW
263
    return;
×
264
  }
265

266
  // Generate the enemy at the position
267
  const auto enemy_id{registry_->create_game_object(GameObjectType::Enemy, position,
20✔
268
                                                    get_game_object_components(GameObjectType::Enemy))};
20✔
269
  registry_->get_component<SteeringMovement>(enemy_id)->target_id = player_id_;
10✔
270
}
10✔
271

272
auto GameEngine::get_game_object_components(const GameObjectType game_object_type)
19,796✔
273
    -> std::vector<std::shared_ptr<ComponentBase>> {
274
  return get_factories().at(game_object_type)(std::max(0, static_cast<int>(level_distribution_(random_generator_))));
19,796✔
275
}
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