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

JackAshwell11 / Hades / 15258938923

26 May 2025 05:02PM UTC coverage: 71.083% (+1.1%) from 69.989%
15258938923

push

github

JackAshwell11
Replaced `UpgradeSystem` with `ShopSystem`, which supports stat upgrades (identical to the previous `UpgradeSystem`), component unlocks, and item purchases. This new system also implements the `ShopItemLoaded` and `ShopItemPurchased` events allowing the `Player` Python module to react to state changes.

Added `GameEngine::setup_shop()` which uses a JSON-based configuration file to load the offerings for the `ShopSystem`.

Removed unused Python bindings from the `Stat` class including `add_to_max_value()`, `get_current_level()`, `increment_current_level()`, and `get_max_level()`.

Resolved test naming conflict by renaming `TestStat` to `TestEffectsStat` and `TestShopStat`.

98 of 135 new or added lines in 5 files covered. (72.59%)

2 existing lines in 2 files now uncovered.

1352 of 1902 relevant lines covered (71.08%)

16755.34 hits per line

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

98.84
/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 int ENEMY_GENERATION_DISTANCE{5};
26

27
// How many times an enemy should be attempted to be generated.
28
constexpr int ENEMY_RETRY_ATTEMPTS{3};
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
  }
NEW
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)
41✔
46
    : level_(level),
41✔
47
      random_generator_(seed.has_value() ? seed.value() : std::random_device{}()),
41✔
48
      level_distribution_(level, LEVEL_DISTRIBUTION_DEVIATION),
41✔
49
      registry_(std::make_shared<Registry>(random_generator_)),
42✔
50
      player_id_(-1) {
42✔
51
  if (level < 0) {
41✔
52
    throw std::length_error("Level must be bigger than or equal to 0.");
1✔
53
  }
54
  generator_ = MapGenerator{level, random_generator_};
40✔
55
  generator_.generate_rooms()
40✔
56
      .place_obstacles()
40✔
57
      .create_connections()
40✔
58
      .generate_hallways()
40✔
59
      .cellular_automata(CELLULAR_AUTOMATA_SIMULATIONS)
40✔
60
      .generate_walls()
40✔
61
      .place_player()
40✔
62
      .place_items()
40✔
63
      .place_goal();
40✔
64

65
  // Add the systems to the registry
66
  registry_->add_system<ArmourRegenSystem>();
40✔
67
  registry_->add_system<AttackSystem>();
40✔
68
  registry_->add_system<DamageSystem>();
40✔
69
  registry_->add_system<EffectSystem>();
40✔
70
  registry_->add_system<FootprintSystem>();
40✔
71
  registry_->add_system<InventorySystem>();
40✔
72
  registry_->add_system<KeyboardMovementSystem>();
40✔
73
  registry_->add_system<PhysicsSystem>();
40✔
74
  registry_->add_system<ShopSystem>();
40✔
75
  registry_->add_system<SteeringMovementSystem>();
40✔
76
}
42✔
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,577✔
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,625✔
100
    const auto game_object_type{tile_to_game_object_type.at(tile_type)};
19,623✔
101
    const auto [x, y]{generator_.get_grid().convert_position(i)};
19,623✔
102
    if (tile_type != TileType::Wall && tile_type != TileType::Floor) {
19,623✔
103
      registry_->create_game_object(GameObjectType::Floor, cpv(x, y),
446✔
104
                                    get_game_object_components(GameObjectType::Floor));
446✔
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,623✔
108
    if (tile_type == TileType::Player) {
19,623✔
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::generate_enemy(const double /*delta_time*/) {
18✔
142
  if (static_cast<int>(registry_->get_game_object_ids(GameObjectType::Enemy).size()) >= generator_.get_enemy_limit()) {
18✔
143
    return;
6✔
144
  }
145

146
  // Collect all floor positions and shuffle them
147
  const auto &grid{*generator_.get_grid().grid};
12✔
148
  std::vector<cpVect> floor_positions;
12✔
149
  for (auto i{0}; std::cmp_less(i, grid.size()); i++) {
7,212✔
150
    if (grid[i] == TileType::Floor) {
7,200✔
151
      const auto [x, y]{generator_.get_grid().convert_position(i)};
4,584✔
152
      floor_positions.push_back(cpv(x, y));
4,584✔
153
    }
154
  }
155
  std::ranges::shuffle(floor_positions, random_generator_);
12✔
156

157
  // Determine which floor to place the enemy on only trying ENEMY_RETRY_ATTEMPTS times
158
  for (auto attempt{0}; attempt < std::min(static_cast<int>(floor_positions.size()), ENEMY_RETRY_ATTEMPTS); attempt++) {
12✔
159
    const auto position{floor_positions[attempt]};
12✔
160
    if (const auto player_position{cpBodyGetPosition(*registry_->get_component<KinematicComponent>(player_id_)->body)};
12✔
161
        cpSpacePointQueryNearest(registry_->get_space(), position, 0.0,
10✔
162
                                 {CP_NO_GROUP, CP_ALL_CATEGORIES, static_cast<cpBitmask>(GameObjectType::Enemy)},
163
                                 nullptr) != nullptr ||
20✔
164
        cpvdist(position, player_position) < ENEMY_GENERATION_DISTANCE * SPRITE_SIZE) {
10✔
165
      continue;
×
166
    }
167

168
    // Create the enemy and set its required data
169
    const auto enemy_id{registry_->create_game_object(GameObjectType::Enemy, position,
20✔
170
                                                      get_game_object_components(GameObjectType::Enemy))};
20✔
171
    registry_->get_component<SteeringMovement>(enemy_id)->target_id = player_id_;
10✔
172
    return;
10✔
173
  }
174
}
12✔
175

176
void GameEngine::on_update(const double /*delta_time*/) {
5✔
177
  nearest_item_ = registry_->get_system<PhysicsSystem>()->get_nearest_item(player_id_);
5✔
178
  if (nearest_item_ != -1 && registry_->get_game_object_type(nearest_item_) == GameObjectType::Goal) {
5✔
179
    registry_->delete_game_object(player_id_);
1✔
180
  }
181
}
5✔
182

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

185
void GameEngine::on_key_press(const int symbol, const int /*modifiers*/) const {
9✔
186
  const auto player_movement{registry_->get_component<KeyboardMovement>(player_id_)};
9✔
187
  switch (symbol) {
9✔
188
    case KEY_W:
2✔
189
      player_movement->moving_north = true;
2✔
190
      break;
2✔
191
    case KEY_A:
2✔
192
      player_movement->moving_west = true;
2✔
193
      break;
2✔
194
    case KEY_S:
2✔
195
      player_movement->moving_south = true;
2✔
196
      break;
2✔
197
    case KEY_D:
2✔
198
      player_movement->moving_east = true;
2✔
199
      break;
2✔
200
    default:
1✔
201
      break;
1✔
202
  }
203
}
18✔
204

205
void GameEngine::on_key_release(const int symbol, const int /*modifiers*/) const {
10✔
206
  const auto player_movement{registry_->get_component<KeyboardMovement>(player_id_)};
10✔
207
  switch (symbol) {
10✔
208
    case KEY_W:
1✔
209
      player_movement->moving_north = false;
1✔
210
      break;
1✔
211
    case KEY_A:
1✔
212
      player_movement->moving_west = false;
1✔
213
      break;
1✔
214
    case KEY_S:
1✔
215
      player_movement->moving_south = false;
1✔
216
      break;
1✔
217
    case KEY_D:
1✔
218
      player_movement->moving_east = false;
1✔
219
      break;
1✔
220
    case KEY_C:
1✔
221
      registry_->get_system<InventorySystem>()->add_item_to_inventory(player_id_, nearest_item_);
1✔
222
      break;
1✔
223
    case KEY_E:
1✔
224
      use_item(player_id_, nearest_item_);
1✔
225
      break;
1✔
226
    case KEY_Z:
1✔
227
      registry_->get_system<AttackSystem>()->previous_ranged_attack(player_id_);
1✔
228
      break;
1✔
229
    case KEY_X:
2✔
230
      registry_->get_system<AttackSystem>()->next_ranged_attack(player_id_);
2✔
231
      break;
2✔
232
    default:
1✔
233
      break;
1✔
234
  }
235
}
20✔
236

237
auto GameEngine::on_mouse_press(const double /*x*/, const double /*y*/, const int button, const int /*modifiers*/) const
2✔
238
    -> bool {
239
  if (button == MOUSE_BUTTON_LEFT) {
2✔
240
    return registry_->get_system<AttackSystem>()->do_attack(player_id_, AttackType::Ranged);
1✔
241
  }
242
  return false;
1✔
243
}
244

245
void GameEngine::use_item(const GameObjectID target_id, const GameObjectID item_id) const {
6✔
246
  // Check if the item is a valid game object or not
247
  if (!registry_->has_game_object(item_id)) {
6✔
248
    return;
1✔
249
  }
250

251
  // Use the item if it can be used
252
  bool used{false};
5✔
253
  if (registry_->has_component(item_id, typeid(EffectApplier))) {
5✔
254
    used = registry_->get_system<EffectSystem>()->apply_effects(item_id, target_id);
5✔
255
  }
256

257
  // If the item has been used, remove it from the inventory or the dungeon
258
  if (used) {
4✔
259
    if (const auto inventory_system{registry_->get_system<InventorySystem>()};
3✔
260
        inventory_system->has_item_in_inventory(target_id, item_id)) {
3✔
261
      inventory_system->remove_item_from_inventory(target_id, item_id);
1✔
262
    } else {
263
      registry_->delete_game_object(item_id);
2✔
264
    }
3✔
265
  }
266
}
267

268
auto GameEngine::get_game_object_components(const GameObjectType game_object_type)
19,856✔
269
    -> std::vector<std::shared_ptr<ComponentBase>> {
270
  const auto &factories{get_factories()};
19,856✔
271
  return factories.at(game_object_type)(std::max(0, static_cast<int>(level_distribution_(random_generator_))));
19,856✔
272
}
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