overlord_event_system/
attributes.rs

1use 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        // TODO fix this shit
109        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
121/// Accumulate flat attribute bonuses from completed talent levels.
122fn 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
145/// Accumulate flat attribute bonuses from the active statue set.
146fn 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}