overlord_event_system/
attributes.rs1use configs::game_config;
2use essences::{class, entity, entity::EntityState, items, talent_tree::TalentTemplate};
3
4use super::{ScriptRunner, event::OverlordEvent, state::OverlordState};
5use crate::game_config_helpers::GameConfigLookup;
6
7#[derive(Default)]
8pub struct EntityStats {
9 pub attributes: entity::EntityAttributes,
10 pub max_hp: u64,
11}
12
13pub type AttributeDeltas = std::collections::HashMap<items::AttributeId, i64>;
14
15pub fn calculate_entity_stats(
16 game_config: &game_config::GameConfig,
17 attributes_deltas: AttributeDeltas,
18) -> anyhow::Result<EntityStats> {
19 let mut attributes = entity::EntityAttributes::default();
20 let mut max_hp = 0;
21
22 for attribute_delta in attributes_deltas {
23 let Some(config_attribute) = game_config.attribute(attribute_delta.0) else {
24 anyhow::bail!("Couldn't find attribute with id = {}", attribute_delta.0);
25 };
26
27 if attribute_delta.0 == game_config.game_settings.hp_attribute_id {
28 max_hp += attribute_delta.1 as u64;
29 }
30
31 attributes.add(&config_attribute.code.clone(), attribute_delta.1);
32 }
33
34 Ok(EntityStats { attributes, max_hp })
35}
36
37pub fn calculate_player_entity_stats_with_zeroes(
38 entity_state: &EntityState,
39 game_config: &game_config::GameConfig,
40 script_runner: &ScriptRunner<OverlordEvent, OverlordState>,
41) -> anyhow::Result<EntityStats> {
42 let mut attributes_deltas = AttributeDeltas::new();
43
44 for attribute in &game_config.attributes {
45 attributes_deltas.insert(attribute.id, 0);
46 }
47
48 let Some(level_attributes) = game_config.character_level(entity_state.level()) else {
49 anyhow::bail!(
50 "Couldn't find character level attributes for level={}",
51 entity_state.level()
52 );
53 };
54
55 for attribute in &level_attributes.attributes {
56 *attributes_deltas.entry(attribute.attribute_id).or_insert(0) += attribute.value as i64;
57 }
58
59 for item in entity_state.inventory() {
60 if !item.is_equipped {
61 continue;
62 }
63
64 for attribute in &item.attributes {
65 *attributes_deltas.entry(attribute.attr_id).or_insert(0) += attribute.value as i64;
66 }
67 }
68
69 let Some(class) = game_config.class(entity_state.class()) else {
70 anyhow::bail!("Couldn't find class with id={}", entity_state.class());
71 };
72
73 for class_attribute in &class.attributes {
74 match class_attribute {
75 class::ClassAttribute::EntityAttribute(entity_attribute) => {
76 *attributes_deltas
77 .entry(entity_attribute.attribute_id)
78 .or_insert(0) += entity_attribute.value as i64;
79 }
80 class::ClassAttribute::ScriptAttributes(script) => {
81 let script_deltas = script_runner
82 .run_entity_attributes_calculate(|scope_setter| scope_setter, script);
83
84 for (id, value) in script_deltas {
85 *attributes_deltas.entry(id).or_insert(0) += value;
86 }
87 }
88 }
89 }
90
91 aggregate_pet_stats(entity_state, game_config, &mut attributes_deltas);
92 aggregate_talent_attribute_bonuses(entity_state, &game_config.talents, &mut attributes_deltas);
93 aggregate_statue_attribute_bonuses(entity_state, game_config, &mut attributes_deltas);
94
95 calculate_entity_stats(game_config, attributes_deltas)
96}
97
98fn aggregate_pet_stats(
99 entity_state: &EntityState,
100 game_config: &game_config::GameConfig,
101 attributes_deltas: &mut AttributeDeltas,
102) {
103 let Some(equipped_pets) = entity_state.equipped_pets() else {
104 return;
105 };
106
107 for pet in equipped_pets.slotted.values() {
108 let template = match game_config.pet_template(pet.template_id) {
110 Some(t) => t,
111 None => continue,
112 };
113
114 for stat in &template.stats {
115 let value = stat.base_value + stat.per_level_value * (pet.level - 1);
116 *attributes_deltas.entry(stat.attribute_id).or_insert(0) += value;
117 }
118 }
119}
120
121fn aggregate_talent_attribute_bonuses(
123 entity_state: &EntityState,
124 talents: &[TalentTemplate],
125 attributes_deltas: &mut AttributeDeltas,
126) {
127 let EntityState::Character(character_state) = entity_state else {
128 return;
129 };
130 for talent in talents {
131 let Some(&level) = character_state.talent_levels.get(&talent.id) else {
132 continue;
133 };
134 for level_config in &talent.levels {
135 if level_config.level > level {
136 break;
137 }
138 for bonus in &level_config.attribute_bonuses {
139 *attributes_deltas.entry(bonus.attribute_id).or_insert(0) += bonus.value;
140 }
141 }
142 }
143}
144
145fn aggregate_statue_attribute_bonuses(
147 entity_state: &EntityState,
148 game_config: &game_config::GameConfig,
149 attributes_deltas: &mut AttributeDeltas,
150) {
151 let EntityState::Character(character_state) = entity_state else {
152 return;
153 };
154 let statue = &character_state.statue_state;
155 let active_index = statue.active_set_index as usize;
156 let Some(active_set) = statue.sets.get(active_index) else {
157 return;
158 };
159 for slot in active_set.slots.0.values() {
160 let Some(bonus_type) = game_config
161 .statue_bonus_type_configs
162 .iter()
163 .find(|bt| bt.attribute_id == slot.attribute_id)
164 else {
165 continue;
166 };
167 let Some(grade_value) = bonus_type
168 .grade_values
169 .iter()
170 .find(|gv| gv.grade_id == slot.grade_id)
171 else {
172 continue;
173 };
174 *attributes_deltas.entry(slot.attribute_id).or_insert(0) += grade_value.value.get() as i64;
175 }
176}
177
178pub fn calculate_player_entity_stats_without_zeroes(
179 entity_state: &EntityState,
180 game_config: &game_config::GameConfig,
181 script_runner: &ScriptRunner<OverlordEvent, OverlordState>,
182) -> anyhow::Result<EntityStats> {
183 let mut stats =
184 calculate_player_entity_stats_with_zeroes(entity_state, game_config, script_runner)?;
185 stats.attributes.remove_zeroes();
186
187 Ok(stats)
188}