overlord_event_system/
entities.rs

1use configs::game_config;
2use essences::{
3    abilities, character_state,
4    entity::{self, EntityAction, EntityActionsQueue, EntityAttributes, EntityId, EntityState},
5    fighting,
6};
7use event_system::event::EventPluginized;
8
9use crate::attributes::{
10    AttributeDeltas, EntityStats, calculate_entity_stats,
11    calculate_player_entity_stats_without_zeroes,
12};
13use crate::game_config_helpers::GameConfigLookup;
14
15use super::{ScriptRunner, TICKER_UNIT_DURATION_MS, event::OverlordEvent, state::OverlordState};
16
17fn make_ability_deadline(cooldown: u64) -> chrono::DateTime<chrono::Utc> {
18    ::time::utc_now()
19        + chrono::TimeDelta::milliseconds((cooldown as u128 * TICKER_UNIT_DURATION_MS) as i64)
20}
21
22pub fn sort_active_abilities(abilities: &mut [abilities::ActiveAbility]) {
23    abilities.sort_by(|a, b| {
24        a.slot_id
25            .cmp(&b.slot_id)
26            .then(a.ability.template_id.cmp(&b.ability.template_id))
27    });
28}
29
30pub fn make_active_abilities_from_equipped(
31    equipped_abilities: &abilities::EquippedAbilities,
32    game_config: &game_config::GameConfig,
33    begin_in_cooldown: bool,
34) -> Vec<abilities::ActiveAbility> {
35    let deadline_for = |template_id| {
36        if begin_in_cooldown {
37            let cooldown = game_config
38                .ability_template(template_id)
39                .map(|t| t.cooldown)
40                .unwrap_or(0);
41            Some(make_ability_deadline(cooldown))
42        } else {
43            None
44        }
45    };
46
47    let mut abilities: Vec<abilities::ActiveAbility> =
48        equipped_abilities
49            .unslotted
50            .iter()
51            .map(|a| abilities::ActiveAbility {
52                deadline: deadline_for(a.template_id),
53                ability: a.clone(),
54                slot_id: None,
55            })
56            .chain(equipped_abilities.slotted.iter().map(|(&slot_id, a)| {
57                abilities::ActiveAbility {
58                    deadline: deadline_for(a.template_id),
59                    ability: a.clone(),
60                    slot_id: Some(slot_id),
61                }
62            }))
63            .collect();
64
65    sort_active_abilities(&mut abilities);
66
67    abilities
68}
69
70pub fn create_player_entity(
71    character_state: &character_state::CharacterState,
72    entity_id: uuid::Uuid,
73    game_config: &game_config::GameConfig,
74    script_runner: &ScriptRunner<OverlordEvent, OverlordState>,
75) -> anyhow::Result<entity::Entity> {
76    let entity_stats = calculate_player_entity_stats_without_zeroes(
77        &EntityState::Character(character_state),
78        game_config,
79        script_runner,
80    )?;
81
82    let mut entity_abilities = make_active_abilities_from_equipped(
83        &character_state.equipped_abilities,
84        game_config,
85        false,
86    );
87
88    // Add leader pet's active ability if present
89    if let Some(leader_pet) = character_state.equipped_pets.leader()
90        && let Some(ability_template) = leader_pet
91            .active_ability_id
92            .and_then(|id| game_config.ability_template(id))
93    {
94        let pet_ability = abilities::Ability::from_template(ability_template, None, None);
95        entity_abilities.push(abilities::ActiveAbility {
96            ability: pet_ability,
97            deadline: None,
98            slot_id: None,
99        });
100    }
101
102    Ok(entity::Entity {
103        id: entity_id,
104        max_hp: entity_stats.max_hp,
105        hp: entity_stats.max_hp,
106        abilities: entity_abilities,
107        actions_queue: EntityActionsQueue::new(entity_id),
108        attributes: entity_stats.attributes,
109        effect_ids: Default::default(),
110        coordinates: game_config.fight_settings.player_start_position.clone(),
111        being_moved: false,
112        width: 1, // TODO idk
113        rewards: None,
114        class_id: Some(character_state.character.class),
115        team: fighting::EntityTeam::Ally,
116        has_big_hp_bar: false,
117        entity_template_id: None,
118    })
119}
120
121pub fn create_party_entity(
122    character_state: &character_state::CharacterState,
123    entity_id: uuid::Uuid,
124    game_config: &game_config::GameConfig,
125    script_runner: &ScriptRunner<OverlordEvent, OverlordState>,
126) -> anyhow::Result<entity::Entity> {
127    let mut entity = create_player_entity(character_state, entity_id, game_config, script_runner)?;
128    entity.coordinates = game_config.fight_settings.party_start_position.clone();
129    Ok(entity)
130}
131
132pub fn create_pve_entity(
133    entity_id: uuid::Uuid,
134    fight_entity: &fighting::FightEntity,
135    game_config: &game_config::GameConfig,
136    entity_attributes: Option<EntityAttributes>,
137) -> anyhow::Result<entity::Entity> {
138    let entity_template_id = match fight_entity.entity_type {
139        fighting::EntityType::PVEEntity { entity_template_id } => entity_template_id,
140        fighting::EntityType::PVPEntity => {
141            anyhow::bail!(
142                "Wanted to create a PVE entity, but got a PVP entity = {:?}",
143                fight_entity
144            );
145        }
146    };
147
148    let Some(entity) = game_config.entity_template(entity_template_id).cloned() else {
149        anyhow::bail!(
150            "Failed to find entity_template with id={}",
151            entity_template_id
152        )
153    };
154
155    let entity_abilities = entity
156        .ability_ids
157        .iter()
158        .filter_map(|&ability_id| {
159            let Some(entity_ability_template) = game_config.ability_template(ability_id) else {
160                tracing::error!("Failed to get ability with ability_id={}", ability_id);
161                return None;
162            };
163
164            let enemy_ability = abilities::Ability::from_template(
165                entity_ability_template,
166                /*level=*/ None,
167                /*shards_amount=*/ None,
168            );
169
170            Some(abilities::ActiveAbility {
171                ability: enemy_ability,
172                deadline: None,
173                slot_id: None,
174            })
175        })
176        .collect();
177
178    let entity_stats = if let Some(attributes) = entity_attributes {
179        let mut max_hp = 0;
180        let Some(config_max_hp_attribute) =
181            game_config.attribute(game_config.game_settings.hp_attribute_id)
182        else {
183            anyhow::bail!(
184                "Couldn't find max_hp attribute with id = {}",
185                game_config.game_settings.hp_attribute_id
186            );
187        };
188
189        for (code, value) in &attributes.0 {
190            if *code == config_max_hp_attribute.code {
191                max_hp += *value as u64
192            }
193        }
194        EntityStats { attributes, max_hp }
195    } else {
196        let mut attributes_deltas = AttributeDeltas::new();
197        for attribute in entity.attributes {
198            *attributes_deltas.entry(attribute.attribute_id).or_insert(0) += attribute.value as i64;
199        }
200
201        calculate_entity_stats(game_config, attributes_deltas)?
202    };
203
204    Ok(entity::Entity {
205        id: entity_id,
206        max_hp: entity_stats.max_hp,
207        hp: entity_stats.max_hp,
208        abilities: entity_abilities,
209        actions_queue: EntityActionsQueue::new(entity_id),
210        attributes: entity_stats.attributes,
211        effect_ids: Default::default(),
212        coordinates: fight_entity.position.clone(),
213        being_moved: false,
214        width: entity.width,
215        rewards: Some(entity.rewards),
216        class_id: None,
217        team: fight_entity.team.clone(),
218        has_big_hp_bar: fight_entity.has_big_hp_bar,
219        entity_template_id: Some(entity_template_id),
220    })
221}
222
223pub fn create_pvp_entity(
224    entity_state: &EntityState,
225    fight_entity: &fighting::FightEntity,
226    game_config: &game_config::GameConfig,
227    script_runner: &ScriptRunner<OverlordEvent, OverlordState>,
228) -> anyhow::Result<entity::Entity> {
229    if fight_entity.entity_type != fighting::EntityType::PVPEntity {
230        anyhow::bail!(
231            "Wanted to create a PVP entity, but got a PVE entity = {:?}",
232            fight_entity
233        );
234    };
235
236    let entity_stats =
237        calculate_player_entity_stats_without_zeroes(entity_state, game_config, script_runner)?;
238
239    Ok(entity::Entity {
240        id: entity_state.id(),
241        max_hp: entity_stats.max_hp,
242        hp: entity_stats.max_hp,
243        abilities: make_active_abilities_from_equipped(
244            entity_state.equipped_abilities(),
245            game_config,
246            false,
247        ),
248        actions_queue: EntityActionsQueue::new(entity_state.id()),
249        attributes: entity_stats.attributes,
250        effect_ids: Default::default(),
251        coordinates: fight_entity.position.clone(),
252        being_moved: false,
253        width: 1, // TODO idk
254        rewards: None,
255        class_id: Some(entity_state.class()),
256        team: fight_entity.team.clone(),
257        has_big_hp_bar: fight_entity.has_big_hp_bar,
258        entity_template_id: None,
259    })
260}
261
262pub fn event_from_entity_action(
263    action: EntityAction,
264    entity_id: EntityId,
265) -> EventPluginized<OverlordEvent, OverlordState> {
266    match action {
267        EntityAction::CastEffect {
268            entity_id,
269            effect_id,
270        } => EventPluginized::now(OverlordEvent::CastEffect {
271            entity_id,
272            effect_id,
273        }),
274        EntityAction::CastAbility {
275            ability_id,
276            target_entity_id,
277        } => EventPluginized::now(OverlordEvent::CastAbility {
278            by_entity_id: entity_id,
279            to_entity_id: target_entity_id,
280            ability_id,
281        }),
282        EntityAction::CastBasicAbility {
283            ability_id,
284            target_entity_id,
285        } => EventPluginized::now(OverlordEvent::CastAbility {
286            by_entity_id: entity_id,
287            to_entity_id: target_entity_id,
288            ability_id,
289        }),
290        EntityAction::StartCastAbility {
291            ability_id,
292            by_entity_id,
293            pet_id,
294        } => EventPluginized::now(OverlordEvent::StartCastAbility {
295            by_entity_id,
296            ability_id,
297            pet_id,
298        }),
299    }
300}