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

JackAshwell11 / Hades / 17012280069

16 Aug 2025 07:51PM UTC coverage: 93.521% (-0.9%) from 94.372%
17012280069

Pull #348

github

web-flow
Merge 356dc6e13 into 883c3507a
Pull Request #348: Implement a save system allowing the player to save, load, delete, and start new games

618 of 676 new or added lines in 34 files covered. (91.42%)

2 existing lines in 1 file now uncovered.

2165 of 2315 relevant lines covered (93.52%)

20587.59 hits per line

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

98.98
/src/hades_extensions/src/game_state.cpp
1
// Std headers
2
#include <utility>
3

4
// Related header
5
#include "game_state.hpp"
6

7
// Local headers
8
#include "ecs/registry.hpp"
9
#include "ecs/systems/inventory.hpp"
10
#include "ecs/systems/level.hpp"
11
#include "ecs/systems/movements.hpp"
12
#include "ecs/systems/physics.hpp"
13
#include "events.hpp"
14
#include "factories.hpp"
15
#include "generation/map.hpp"
16
#include "generation/primitives.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 hasher for the seed string.
29
constexpr std::hash<std::string> seed_hasher;
30
}  // namespace
31

32
GameState::GameState(const std::shared_ptr<Registry> &registry) : registry_(registry) {
66✔
33
  if (get_player_id() == -1) {
66✔
34
    game_state_.game.player_id = registry_->create_game_object(GameObjectType::Player, cpvzero,
132✔
35
                                                               get_game_object_components(GameObjectType::Player));
132✔
36
  }
37
  initialise_dungeon_run();
66✔
38
}
66✔
39

40
auto GameState::get_player_id() const -> GameObjectID { return game_state_.game.player_id; }
534✔
41

42
auto GameState::get_nearest_item() const -> GameObjectID { return game_state_.current_level.nearest_item; }
36✔
43

44
void GameState::set_nearest_item(const GameObjectID item_id) { game_state_.current_level.nearest_item = item_id; }
20✔
45

46
auto GameState::get_dungeon_level() const -> LevelType { return game_state_.current_level.dungeon_level; }
77✔
47

48
auto GameState::get_game_level() const -> int { return game_state_.dungeon_run.game_level; }
58✔
49

50
auto GameState::is_lobby() const -> bool { return game_state_.current_level.dungeon_level == LevelType::Lobby; }
16✔
51

52
auto GameState::is_boss() const -> bool { return game_state_.current_level.dungeon_level == LevelType::Boss; }
3✔
53

54
auto GameState::get_enemy_generation_timer() const -> double {
19✔
55
  return game_state_.current_level.enemy_generation_timer;
19✔
56
}
57

58
void GameState::set_enemy_generation_timer(const double value) {
9✔
59
  game_state_.current_level.enemy_generation_timer = value;
9✔
60
}
9✔
61

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

66
void GameState::set_seed(const std::string &seed) {
1✔
67
  game_state_.dungeon_run.random_generator.seed(static_cast<unsigned int>(seed_hasher(seed)));
1✔
68
}
1✔
69

70
void GameState::initialise_dungeon_run() {
88✔
71
  const auto player_level{registry_->get_component<PlayerLevel>(get_player_id())->level};
88✔
72
  game_state_.dungeon_run = {
264✔
73
      .game_level = player_level,
74
      .level_distribution = std::normal_distribution<>(player_level, LEVEL_DISTRIBUTION_DEVIATION)};
176✔
75
}
88✔
76

77
void GameState::reset_level(const LevelType level_type) {
73✔
78
  // Reset the game state while preserving inventory items if necessary
79
  std::unordered_set preserved_ids{get_player_id()};
219✔
80
  if (level_type != LevelType::Lobby) {
73✔
81
    const auto inventory{registry_->get_component<Inventory>(get_player_id())};
52✔
82
    preserved_ids.insert(inventory->items.begin(), inventory->items.end());
52✔
83
  } else {
52✔
84
    for (const auto &component : registry_->get_game_object_components(get_player_id())) {
315✔
85
      component->reset();
294✔
86
    }
21✔
87
  }
88
  registry_->clear_game_objects(preserved_ids);
73✔
89

90
  // Set up the player and game state
91
  game_state_.current_level = {.floor_positions = {}, .dungeon_level = level_type};
73✔
92
  if (level_type == LevelType::Lobby) {
73✔
93
    initialise_dungeon_run();
21✔
94
  }
95

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

115
  // Notify the Python module of the level reset
116
  if (level_type == LevelType::Lobby) {
73✔
117
    notify<EventType::InventoryUpdate>(std::vector<GameObjectID>{});
21✔
118
    notify<EventType::RangedAttackSwitch>(0);
21✔
119
    notify<EventType::AttackCooldownUpdate>(get_player_id(), 0.0, 0.0, 0.0);
21✔
120
    notify<EventType::StatusEffectUpdate>(std::unordered_map<EffectType, double>{});
21✔
121
  }
122
  notify<EventType::GameOpen>();
73✔
123
}
146✔
124

125
void GameState::generate_enemy() {
3✔
126
  // Get a random floor position and check if it is valid for enemy generation
127
  const auto &current_level{game_state_.current_level};
3✔
128
  if (current_level.floor_positions.empty()) {
3✔
129
    return;
1✔
130
  }
131
  auto dist{std::uniform_int_distribution<size_t>(0, current_level.floor_positions.size() - 1)};
2✔
132
  const auto grid_position{current_level.floor_positions.at(dist(game_state_.dungeon_run.random_generator))};
2✔
133
  const auto world_position{grid_pos_to_pixel(grid_position)};
2✔
134
  const bool intersecting_enemies{
135
      cpSpacePointQueryNearest(registry_->get_space(), world_position, 0.0,
2✔
136
                               {CP_NO_GROUP, CP_ALL_CATEGORIES, static_cast<cpBitmask>(GameObjectType::Enemy)},
137
                               nullptr) != nullptr};
2✔
138
  if (const auto player_position{
2✔
139
          cpBodyGetPosition(*registry_->get_component<KinematicComponent>(get_player_id())->body)};
2✔
140
      intersecting_enemies || cpvdist(world_position, player_position) < ENEMY_GENERATION_DISTANCE) {
2✔
NEW
141
    return;
×
142
  }
143

144
  // Generate the enemy at the position
145
  const int enemy_level{
2✔
146
      static_cast<int>(game_state_.dungeon_run.level_distribution(game_state_.dungeon_run.random_generator))};
2✔
147
  const auto enemy_id{
148
      registry_->create_game_object(GameObjectType::Enemy, grid_position,
4✔
149
                                    get_game_object_components(GameObjectType::Enemy, std::max(0, enemy_level)))};
4✔
150
  registry_->get_component<SteeringMovement>(enemy_id)->target_id = get_player_id();
2✔
151
}
152

153
void GameState::create_game_objects(const Grid &grid, const bool store_floor_positions) {
73✔
154
  for (auto i{0}; std::cmp_less(i, grid.grid.size()); i++) {
48,466✔
155
    const auto game_object_type{grid.grid[i]};
48,393✔
156
    if (game_object_type == GameObjectType::Empty || game_object_type == GameObjectType::Obstacle) {
48,393✔
157
      continue;
6,163✔
158
    }
159

160
    // Get the game object's position
161
    const auto [x, y]{grid.convert_position(i)};
42,230✔
162
    const auto position{cpv(x, y)};
42,230✔
163

164
    // Store the floor position for enemy generation
165
    if (game_object_type != GameObjectType::Wall) {
42,230✔
166
      if (store_floor_positions) {
31,170✔
167
        game_state_.current_level.floor_positions.emplace_back(position);
28,713✔
168
      }
169
      if (game_object_type != GameObjectType::Floor) {
31,170✔
170
        registry_->create_game_object(GameObjectType::Floor, position,
950✔
171
                                      get_game_object_components(GameObjectType::Floor));
950✔
172
      }
173
    }
174

175
    // Handle game object creation
176
    if (game_object_type == GameObjectType::Player) {
42,230✔
177
      cpBodySetPosition(*registry_->get_component<KinematicComponent>(get_player_id())->body,
73✔
178
                        grid_pos_to_pixel(position));
179
    } else {
180
      registry_->create_game_object(game_object_type, position, get_game_object_components(game_object_type));
42,157✔
181
    }
182
  }
183
}
73✔
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