overlord_event_system/async_handler/
pets.rs

1use crate::{
2    async_handler::handler::OverlordAsyncEventHandler,
3    attributes::calculate_player_entity_stats_without_zeroes,
4    entities::{make_active_abilities_from_equipped, sort_active_abilities},
5    event::OverlordEvent,
6    game_config_helpers::GameConfigLookup,
7    state::OverlordState,
8};
9
10use essences::entity::{ActionWithDeadline, EntityState};
11use essences::fighting::ActivePetAbility;
12use essences::pets::{EquippedPets, PetId, PetSlotId};
13use event_system::system::EventHandleResult;
14
15impl OverlordAsyncEventHandler {
16    pub fn handle_equip_pet(
17        &self,
18        slot_id: u64,
19        pet_id: PetId,
20        current_tick: u64,
21        mut state: OverlordState,
22    ) -> EventHandleResult<OverlordEvent, OverlordState> {
23        let game_config = self.game_config.get();
24
25        // Check that the pet is not already equipped in another slot
26        if let Some((existing_slot, _)) = state
27            .character_state
28            .equipped_pets
29            .slotted
30            .iter()
31            .find(|(_, p)| p.template_id == pet_id)
32        {
33            tracing::error!("Pet_id = {pet_id} is already equipped in slot_id={existing_slot}");
34            return EventHandleResult::fail(state);
35        }
36
37        // Find the pet in all_pets
38        let Some(pet) = state
39            .character_state
40            .all_pets
41            .iter()
42            .find(|p| p.template_id == pet_id)
43            .cloned()
44        else {
45            tracing::error!("No pet with id = {} in state", pet_id);
46            return EventHandleResult::fail(state);
47        };
48
49        // Check that the slot is unlocked
50        let Some(pet_slots) = game_config
51            .pet_slots_for_chapter_level(state.character_state.character.current_chapter_level)
52        else {
53            tracing::error!(
54                "No pet slots for current_chapter_level = {}",
55                state.character_state.character.current_chapter_level
56            );
57            return EventHandleResult::fail(state);
58        };
59
60        if slot_id >= pet_slots {
61            tracing::error!(
62                "Slot_id is too high = {}, current max pet slots = {}",
63                slot_id,
64                pet_slots,
65            );
66            return EventHandleResult::fail(state);
67        }
68
69        state
70            .character_state
71            .equipped_pets
72            .slotted
73            .insert(slot_id as PetSlotId, pet);
74
75        self.refresh_player_entity_in_active_fight(&mut state, current_tick);
76
77        EventHandleResult::ok(state)
78    }
79
80    pub fn build_pet_combat_state(
81        &self,
82        state: &OverlordState,
83    ) -> (Option<ActivePetAbility>, Option<PetId>) {
84        let leader = state.character_state.equipped_pets.leader();
85        match leader {
86            Some(pet) => {
87                let game_config = self.game_config.get();
88                let template = game_config.pet_template(pet.template_id);
89                let pet_combat_state = template.and_then(|t| {
90                    t.active_ability_id.map(|ability_id| ActivePetAbility {
91                        pet_template_id: pet.template_id,
92                        ability_id,
93                        charge: 0,
94                        max_charge: t.max_charge,
95                    })
96                });
97                (pet_combat_state, Some(pet.template_id))
98            }
99            None => (None, None),
100        }
101    }
102
103    pub fn handle_unequip_pet(
104        &self,
105        slot_id: PetSlotId,
106        current_tick: u64,
107        mut state: OverlordState,
108    ) -> EventHandleResult<OverlordEvent, OverlordState> {
109        state.character_state.equipped_pets.slotted.remove(&slot_id);
110
111        self.refresh_player_entity_in_active_fight(&mut state, current_tick);
112
113        EventHandleResult::ok(state)
114    }
115
116    pub fn handle_equip_pets(
117        &self,
118        equipped_pets: EquippedPets,
119        current_tick: u64,
120        mut state: OverlordState,
121    ) -> EventHandleResult<OverlordEvent, OverlordState> {
122        state.character_state.equipped_pets = equipped_pets;
123
124        self.refresh_player_entity_in_active_fight(&mut state, current_tick);
125
126        EventHandleResult::ok(state)
127    }
128
129    /// Recalculates the player entity's attributes and pet combat state
130    /// in the active fight to reflect the current equipped pets.
131    fn refresh_player_entity_in_active_fight(&self, state: &mut OverlordState, current_tick: u64) {
132        if state.active_fight.is_none() {
133            return;
134        }
135
136        let game_config = self.game_config.get();
137
138        // Recalculate player entity stats with updated pet equipment
139        let entity_stats = match calculate_player_entity_stats_without_zeroes(
140            &EntityState::Character(&state.character_state),
141            &game_config,
142            &self.script_runner,
143        ) {
144            Ok(stats) => stats,
145            Err(err) => {
146                tracing::error!("Failed to recalculate player stats after pet change: {err}");
147                return;
148            }
149        };
150
151        // Build new abilities list
152        let mut new_abilities = make_active_abilities_from_equipped(
153            &state.character_state.equipped_abilities,
154            &game_config,
155            true,
156        );
157        if let Some(leader_pet) = state.character_state.equipped_pets.leader()
158            && let Some(ability_template) = leader_pet
159                .active_ability_id
160                .and_then(|id| game_config.ability_template(id))
161        {
162            let pet_ability =
163                essences::abilities::Ability::from_template(ability_template, None, None);
164            new_abilities.push(essences::abilities::ActiveAbility {
165                ability: pet_ability,
166                deadline: None,
167                slot_id: None,
168            });
169        }
170        sort_active_abilities(&mut new_abilities);
171
172        // Rebuild pet combat state
173        let (pet_combat_state, leader_pet_template_id) = self.build_pet_combat_state(state);
174
175        // Apply all changes to active fight
176        let active_fight = state.active_fight.as_mut().unwrap();
177        if let Some(player) = active_fight
178            .entities
179            .iter_mut()
180            .find(|e| e.id == active_fight.player_id)
181        {
182            let hp_delta = entity_stats.max_hp as i64 - player.max_hp as i64;
183            player.max_hp = entity_stats.max_hp;
184            player.hp = (player.hp as i64 + hp_delta).max(1) as u64;
185            player.attributes = entity_stats.attributes;
186
187            // Remove old ability actions from the queue
188            for ability in &player.abilities {
189                player
190                    .actions_queue
191                    .remove_start_cast_ability_action(ability.ability.template_id);
192            }
193
194            player.abilities = new_abilities;
195
196            // Push new ability actions into the queue
197            for ability in &player.abilities {
198                let Some(ability_template) =
199                    game_config.ability_template(ability.ability.template_id)
200                else {
201                    tracing::error!(
202                        "Couldn't find template for ability_id = {}",
203                        ability.ability.template_id
204                    );
205                    continue;
206                };
207                player.actions_queue.push(&ActionWithDeadline {
208                    action: self
209                        .make_start_cast_ability_action(player.id, ability.ability.template_id),
210                    deadline_tick: current_tick + ability_template.cooldown,
211                });
212            }
213        }
214
215        active_fight.pet_combat_state = pet_combat_state;
216        active_fight.leader_pet_template_id = leader_pet_template_id;
217    }
218}