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

JackAshwell11 / Hades / 17012379893

16 Aug 2025 08:04PM UTC coverage: 93.521% (-0.9%) from 94.372%
17012379893

Pull #348

github

web-flow
Merge 2daba663a 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%)

20494.45 hits per line

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

96.74
/src/hades_extensions/src/ecs/systems/effects.cpp
1
// Related header
2
#include "ecs/systems/effects.hpp"
3

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

7
// Local headers
8
#include "ecs/registry.hpp"
9
#include "ecs/stats.hpp"
10
#include "events.hpp"
11

12
namespace {
13
/// A helper struct to provide the component type for each effect type.
14
template <EffectType>
15
struct EffectTraits;
16

17
/// Provides the component type for the Regeneration effect.
18
template <>
19
struct EffectTraits<EffectType::Regeneration> {
20
  /// Get the component type associated with the Regeneration effect.
21
  ///
22
  /// @return The type index of the component associated with the Regeneration effect.
23
  static std::type_index component_type() { return std::type_index(typeid(Health)); }
35✔
24
};
25

26
/// Provides the component type for the Poison effect.
27
template <>
28
struct EffectTraits<EffectType::Poison> {
29
  /// Get the component type associated with the Poison effect.
30
  ///
31
  /// @return The type index of the component associated with the Poison effect.
32
  static std::type_index component_type() { return std::type_index(typeid(Health)); }
6✔
33
};
34

35
/// Get the component type associated with a specific effect type.
36
///
37
/// @param effect_type - The type of the effect.
38
/// @return The type index of the component associated with the effect type.
39
auto get_component_type(const EffectType effect_type) -> std::type_index {
41✔
40
  switch (effect_type) {
41✔
41
    case EffectType::Regeneration:
35✔
42
      return EffectTraits<EffectType::Regeneration>::component_type();
35✔
43
    case EffectType::Poison:
6✔
44
      return EffectTraits<EffectType::Poison>::component_type();
6✔
NEW
45
    default:
×
NEW
46
      return std::type_index(typeid(void));
×
47
  }
48
}
49

50
/// Notifies the registry of a status effect update.
51
///
52
/// @param effects - The status effects that have been applied to the game object.
53
void notify_status_effect_update(const std::unordered_map<EffectType, StatusEffect> &effects) {
22✔
54
  std::unordered_map<EffectType, double> effect_data;
22✔
55
  for (const auto &[effect_type, effect] : effects) {
45✔
56
    effect_data.emplace(effect_type, effect.duration - effect.time_elapsed);
23✔
57
  }
58
  notify<EventType::StatusEffectUpdate>(effect_data);
22✔
59
}
44✔
60
}  // namespace
61

62
auto BaseEffect::apply(const Registry *registry, const GameObjectID game_object_id) const -> bool {
41✔
63
  const auto component{
41✔
64
      std::static_pointer_cast<Stat>(registry->get_component(game_object_id, get_component_type(effect_type)))};
41✔
65
  if (!component) {
41✔
66
    return false;
×
67
  }
68
  if (value > 0 && component->get_value() == component->get_max_value()) {
41✔
69
    return false;
2✔
70
  }
71
  if (value < 0 && component->get_value() == 0) {
39✔
72
    return false;
1✔
73
  }
74
  component->set_value(component->get_value() + value);
38✔
75
  return true;
38✔
76
}
41✔
77

78
auto StatusEffect::update(const Registry *registry, const GameObjectID target, const double delta_time) -> bool {
10✔
79
  interval_accumulator += std::clamp(duration - time_elapsed, 0.0, delta_time);
10✔
80
  time_elapsed += delta_time;
10✔
81
  while (interval_accumulator >= interval) {
27✔
82
    (void)apply(registry, target);
17✔
83
    interval_accumulator -= interval;
17✔
84
  }
85
  return time_elapsed >= duration;
10✔
86
}
87

88
void StatusEffects::reset() { active_effects.clear(); }
22✔
89

90
void StatusEffects::to_file(nlohmann::json &json) const {
62✔
91
  json["active_effects"] = nlohmann::json::object();
62✔
92
  for (const auto &[effect_type, effect] : active_effects) {
63✔
93
    json.at("active_effects")[std::to_string(static_cast<int>(effect_type))] = {
4✔
94
        {"value", effect.value},
1✔
95
        {"duration", effect.duration},
1✔
96
        {"interval", effect.interval},
1✔
97
        {"time_elapsed", effect.time_elapsed},
1✔
98
        {"interval_accumulator", effect.interval_accumulator},
1✔
99
    };
24✔
100
  }
101
}
83✔
102

103
void StatusEffects::from_file(const nlohmann::json &json) {
2✔
104
  for (const auto &[type, effect_data] : json.at("active_effects").items()) {
6✔
105
    const auto effect_type{static_cast<EffectType>(std::stoi(type))};
2✔
106
    StatusEffect status_effect{effect_type, effect_data["value"].get<double>(), effect_data["duration"].get<double>(),
6✔
107
                               effect_data["interval"].get<double>()};
6✔
108
    status_effect.time_elapsed = effect_data["time_elapsed"].get<double>();
2✔
109
    status_effect.interval_accumulator = effect_data["interval_accumulator"].get<double>();
2✔
110
    active_effects.emplace(effect_type, status_effect);
2✔
111
  }
4✔
112
}
2✔
113

114
void EffectSystem::update(const double delta_time) const {
12✔
115
  for (const auto &[game_object_id, component_tuple] : get_registry()->find_components<StatusEffects>()) {
36✔
116
    // Update and remove expired effects
117
    auto &active_effects{std::get<0>(component_tuple)->active_effects};
12✔
118
    if (active_effects.empty()) {
12✔
119
      continue;
3✔
120
    }
121
    for (auto it{active_effects.begin()}; it != active_effects.end();) {
19✔
122
      if (it->second.update(get_registry(), game_object_id, delta_time)) {
10✔
123
        it = active_effects.erase(it);
3✔
124
      } else {
125
        ++it;
7✔
126
      }
127
    }
128
    notify_status_effect_update(active_effects);
9✔
129
  }
12✔
130
}
12✔
131

132
auto EffectSystem::apply_effects(const GameObjectID source, const GameObjectID target) const -> bool {
25✔
133
  // Get the required components
134
  const auto effect_applier{get_registry()->get_component<EffectApplier>(source)};
25✔
135
  const auto target_status_effects{get_registry()->get_component<StatusEffects>(target)};
23✔
136

137
  // Apply instant effects
138
  bool any_effect_applied{false};
20✔
139
  for (const auto &effect : effect_applier->instant_effects) {
27✔
140
    if (effect.apply(get_registry(), target)) {
7✔
141
      any_effect_applied = true;
5✔
142
    }
143
  }
144

145
  // Apply status effects
146
  bool status_effects_applied{false};
20✔
147
  for (const auto &[effect_type, effect] : effect_applier->status_effects) {
38✔
148
    // Extend the duration if it is already applied
149
    if (target_status_effects->active_effects.contains(effect_type)) {
18✔
150
      any_effect_applied = true;
1✔
151
      status_effects_applied = true;
1✔
152
      target_status_effects->active_effects.at(effect_type).duration += effect.duration;
1✔
153
      continue;
1✔
154
    }
155

156
    // Otherwise, create a new effect and apply it
157
    if (auto new_effect{effect}; new_effect.apply(get_registry(), target)) {
17✔
158
      any_effect_applied = true;
16✔
159
      status_effects_applied = true;
16✔
160
      target_status_effects->active_effects.emplace(effect_type, std::move(new_effect));
16✔
161
    }
17✔
162
  }
163

164
  // Only notify if any status effects were applied
165
  if (status_effects_applied) {
20✔
166
    notify_status_effect_update(target_status_effects->active_effects);
13✔
167
  }
168
  return any_effect_applied;
20✔
169
}
23✔
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