overlord_event_system/async_handler/
items.rs

1use crate::{
2    async_handler::handler::OverlordAsyncEventHandler, event::OverlordEvent,
3    game_config_helpers::GameConfigLookup, state::OverlordState,
4};
5
6#[allow(unused_imports)]
7use essences::currency::check_can_decrease_currencies;
8
9use configs::buffs::{apply_buff_multiplier, currency_exp_buff_multiplier};
10use essences::{
11    autochest::{AutoChestFilter, filter_items},
12    currency::{CurrencyConsumer, CurrencySource, CurrencyUnit, increase_currencies},
13    items::{Item, ItemType},
14};
15
16use event_system::{event::EventPluginized, return_if_frontend, system::EventHandleResult};
17
18use uuid::Uuid;
19
20impl OverlordAsyncEventHandler {
21    #[allow(dead_code)]
22    fn open_item_case(
23        &self,
24        batch_size: i64,
25        state: OverlordState,
26    ) -> EventHandleResult<OverlordEvent, OverlordState> {
27        let game_config = self.game_config.get();
28
29        tracing::debug!("Open case handler !frontend");
30        if state
31            .character_state
32            .inventory
33            .iter()
34            .any(|x| !x.is_equipped)
35        {
36            tracing::error!("Can't open item_case, inventory contains unequipped items");
37            return EventHandleResult::fail(state);
38        }
39
40        let character_item_case_level = state.character_state.character.item_case_level;
41        let Ok(case_settings) =
42            game_config.require_item_case_settings_by_level(character_item_case_level)
43        else {
44            tracing::error!(
45                "Couldn't find item_case_settings with level = {character_item_case_level}"
46            );
47            return EventHandleResult::fail(state);
48        };
49        if batch_size > case_settings.auto_chest_settings.max_batch_size {
50            tracing::error!(
51                "Batch size {} is bigger than max allowed {}",
52                batch_size,
53                case_settings.auto_chest_settings.max_batch_size
54            );
55            return EventHandleResult::fail(state);
56        }
57
58        let items = (0..batch_size)
59            .filter_map(|_| {
60                use crate::{cases, gacha};
61
62                let mut item = match gacha::item_case::try_open_item_case(
63                    &state.character_state,
64                    &game_config,
65                    None,
66                    &self.script_runner,
67                ) {
68                    Ok(x) => x,
69                    Err(e) => {
70                        tracing::error!("Couldn't open case: {e}");
71                        return None;
72                    }
73                };
74
75                match cases::try_finalize_item(&mut item, &game_config, &self.script_runner) {
76                    Ok(()) => Some(item),
77                    Err(e) => {
78                        tracing::error!("Failed to finalize item: {}", e);
79                        None
80                    }
81                }
82            })
83            .collect();
84
85        EventHandleResult::ok_events(
86            state,
87            vec![EventPluginized::now(OverlordEvent::PlayerNewItems {
88                items,
89            })],
90        )
91    }
92
93    #[allow(unused_variables)]
94    pub fn handle_open_item_case(
95        &self,
96        batch_size: i64,
97        state: OverlordState,
98    ) -> EventHandleResult<OverlordEvent, OverlordState> {
99        return_if_frontend!(state);
100
101        #[cfg(not(feature = "frontend"))]
102        {
103            let game_config = self.game_config.get();
104
105            if state.character_state.character.auto_chest_enabled {
106                tracing::error!("Can't open item case when auto_chest is enabled");
107                return EventHandleResult::fail(state);
108            }
109
110            let required_currency = vec![CurrencyUnit {
111                currency_id: game_config.game_settings.item_case_currency_id,
112                amount: batch_size,
113            }];
114
115            if !check_can_decrease_currencies(&state.character_state.currencies, &required_currency)
116            {
117                tracing::error!(
118                    "Not enough currency for open_item_case opening\nGot = {:?}\nRequired = {:?}",
119                    state.character_state.currencies,
120                    required_currency
121                );
122
123                return EventHandleResult::fail(state);
124            };
125
126            if !self.frontend {
127                return self.open_item_case(batch_size, state);
128            }
129            EventHandleResult::ok(state)
130        }
131    }
132
133    #[allow(unused_variables)]
134    pub fn handle_auto_chest_open_item_case(
135        &self,
136        batch_size: i64,
137        state: OverlordState,
138    ) -> EventHandleResult<OverlordEvent, OverlordState> {
139        return_if_frontend!(state);
140
141        #[cfg(not(feature = "frontend"))]
142        {
143            let game_config = self.game_config.get();
144
145            if !state.character_state.character.auto_chest_enabled {
146                tracing::error!("Can't open auto_chest item case when auto_chest is disabled");
147                return EventHandleResult::fail(state);
148            }
149
150            let required_currency = vec![CurrencyUnit {
151                currency_id: game_config.game_settings.item_case_currency_id,
152                amount: batch_size,
153            }];
154
155            if !check_can_decrease_currencies(&state.character_state.currencies, &required_currency)
156            {
157                tracing::error!(
158                    "Not enough currency for auto_chest_item_case opening\nGot = {:?}\nRequired = {:?}",
159                    state.character_state.currencies,
160                    required_currency
161                );
162
163                return EventHandleResult::ok_events(
164                    state,
165                    vec![EventPluginized::now(OverlordEvent::DisableAutoChest {})],
166                );
167            };
168
169            if !self.frontend {
170                return self.open_item_case(batch_size, state);
171            }
172            EventHandleResult::ok(state)
173        }
174    }
175
176    pub fn handle_player_new_items(
177        &self,
178        items: &[Item],
179        mut state: OverlordState,
180    ) -> EventHandleResult<OverlordEvent, OverlordState> {
181        let game_config = self.game_config.get();
182
183        let required_currency = vec![CurrencyUnit {
184            currency_id: game_config.game_settings.item_case_currency_id,
185            amount: items.len() as i64,
186        }];
187
188        let mut events = vec![];
189
190        let Some(currency_event) =
191            Self::currency_decrease(&state, &required_currency, CurrencyConsumer::ItemCaseOpen)
192        else {
193            return EventHandleResult::fail(state);
194        };
195        events.push(currency_event);
196
197        let mut items = items.to_owned();
198
199        if state.character_state.character.auto_chest_enabled {
200            let filter = state.auto_chest.active_filter.clone();
201            let power_compare_enabled = state.auto_chest.power_compare_enabled;
202            let items_to_equip = match self.filter_new_items(
203                &filter,
204                power_compare_enabled,
205                &items,
206                &mut state,
207                &mut events,
208            ) {
209                Ok(items_to_equip) => items_to_equip,
210                Err(e) => {
211                    tracing::error!("Something went wrong while filtering = {}", e);
212                    return EventHandleResult::fail(state);
213                }
214            };
215
216            if items_to_equip.is_empty() {
217                events.push(EventPluginized::delayed(
218                    OverlordEvent::AutoChestOpenItemCase {
219                        batch_size: state.auto_chest.batch_size,
220                    },
221                    game_config.game_settings.auto_chest_pause_duration_ticks,
222                ));
223                return EventHandleResult::ok_events(state, events);
224            };
225
226            items = items_to_equip;
227        }
228
229        for item in &items {
230            if let Some(item_template) = game_config.item_template(item.item_template_id)
231                && let Some(skin_id) = item_template.skin_id
232            {
233                if game_config.require_skin(skin_id).is_err() {
234                    tracing::error!(
235                        "Skin with id={} not found in config for item_template_id={}",
236                        skin_id,
237                        item.item_template_id
238                    );
239                    return EventHandleResult::fail(state);
240                }
241
242                if !state
243                    .character_state
244                    .character_skins
245                    .is_already_unlocked(skin_id)
246                {
247                    state
248                        .character_state
249                        .character_skins
250                        .blocked
251                        .retain(|s| *s != skin_id);
252                    state
253                        .character_state
254                        .character_skins
255                        .available
256                        .push(skin_id);
257                }
258            }
259        }
260
261        state.character_state.inventory.append(&mut items);
262
263        EventHandleResult::ok_events(state, events)
264    }
265
266    fn filter_new_items(
267        &self,
268        filter: &Option<AutoChestFilter>,
269        power_compare_enabled: bool,
270        items: &[Item],
271        state: &mut OverlordState,
272        events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
273    ) -> anyhow::Result<Vec<Item>> {
274        let game_config = self.game_config.get();
275
276        // Calculate power for new items
277        let mut item_powers = std::collections::HashMap::new();
278        for item in items {
279            let power =
280                state.calculate_item_power(item.clone(), &game_config, &self.script_runner)?;
281            item_powers.insert(item.id, power);
282        }
283
284        // Build map of equipped item powers by type
285        let mut equipped_item_powers = std::collections::HashMap::new();
286        for item in &state.character_state.inventory {
287            if item.is_equipped {
288                let power =
289                    state.calculate_item_power(item.clone(), &game_config, &self.script_runner)?;
290                equipped_item_powers.insert(item.item_type, power);
291            }
292        }
293
294        let (items_to_sell, items_to_equip) = filter_items(
295            items,
296            filter,
297            power_compare_enabled,
298            &game_config.item_rarities,
299            &item_powers,
300            &equipped_item_powers,
301        )?;
302
303        let buff_mult = currency_exp_buff_multiplier(
304            &state.character_state.active_buffs,
305            &game_config.buff_templates,
306            ::time::utc_now(),
307        );
308
309        let mut gained_currencies = vec![];
310
311        for item in items_to_sell {
312            let scaled_price: Vec<CurrencyUnit> = item
313                .price
314                .iter()
315                .map(|c| CurrencyUnit {
316                    currency_id: c.currency_id,
317                    amount: apply_buff_multiplier(c.amount, buff_mult),
318                })
319                .collect();
320            increase_currencies(&mut gained_currencies, &scaled_price);
321            state.character_state.character.character_experience +=
322                apply_buff_multiplier(item.experience, buff_mult);
323            events.push(EventPluginized::now(OverlordEvent::ItemSold {
324                item_id: item.id,
325            }));
326        }
327
328        events.push(Self::currency_increase(
329            &gained_currencies,
330            CurrencySource::AutoItemSell,
331        ));
332
333        Ok(items_to_equip)
334    }
335
336    pub fn handle_player_equip_item(
337        &self,
338        item_id: Uuid,
339        mut state: OverlordState,
340    ) -> EventHandleResult<OverlordEvent, OverlordState> {
341        let game_config = self.game_config.get();
342
343        let Some(item) = state
344            .character_state
345            .inventory
346            .iter()
347            .find(|x| x.id == item_id)
348            .cloned()
349        else {
350            tracing::error!("Tried equiping non-existing item_id={}", item_id);
351            return EventHandleResult::fail(state);
352        };
353
354        let prev_item = state
355            .character_state
356            .inventory
357            .iter()
358            .find(|inv_item| {
359                inv_item.item_type == item.item_type
360                    && inv_item.id != item.id
361                    && inv_item.is_equipped
362            })
363            .cloned();
364
365        state
366            .character_state
367            .inventory
368            .iter_mut()
369            .map(|inv_item| {
370                if inv_item.item_type == item.item_type {
371                    inv_item.is_equipped = false;
372                }
373                if inv_item.id == item.id {
374                    inv_item.is_equipped = true;
375                }
376            })
377            .count();
378
379        let mut equip_skin_ids = vec![];
380        let mut unequip_skin_ids = vec![];
381
382        let gear_override_enabled = state
383            .character_state
384            .character_settings
385            .gear_override_enabled_item_types
386            .contains(&item.item_type);
387
388        if !gear_override_enabled {
389            let currently_equipped = &state.character_state.character_skins.equipped;
390
391            // Unequip the prev item's skin only if it is still actually equipped.
392            // If the player has customized away from it, there is nothing to unequip.
393            if let Some(prev_item) = prev_item
394                && let Some(prev_item_template) =
395                    game_config.item_template(prev_item.item_template_id)
396                && let Some(prev_skin_id) = prev_item_template.skin_id
397                && currently_equipped.contains(&prev_skin_id)
398            {
399                unequip_skin_ids.push(prev_skin_id);
400            }
401
402            // Equip the new item's skin unless it is already equipped. If it is,
403            // make sure we do not also unequip it via the prev-item path above
404            // (e.g. when two items share a skin_id and that skin is equipped).
405            if let Some(item_template) = game_config.item_template(item.item_template_id)
406                && let Some(skin_id) = item_template.skin_id
407            {
408                if game_config.require_skin(skin_id).is_err() {
409                    tracing::error!(
410                        "Skin with id={} not found in config for item_template_id={}",
411                        skin_id,
412                        item.item_template_id
413                    );
414                    return EventHandleResult::fail(state);
415                }
416
417                if currently_equipped.contains(&skin_id) {
418                    unequip_skin_ids.retain(|id| *id != skin_id);
419                } else {
420                    equip_skin_ids.push(skin_id);
421                }
422            }
423        }
424
425        let mut events = vec![];
426        if !equip_skin_ids.is_empty() || !unequip_skin_ids.is_empty() {
427            events.push(EventPluginized::now(OverlordEvent::EquipAndUnequipSkins {
428                equip_skin_ids,
429                unequip_skin_ids,
430            }));
431        }
432
433        if state.character_state.character.auto_chest_enabled {
434            if state
435                .character_state
436                .inventory
437                .iter()
438                .any(|item| !item.is_equipped)
439            {
440                return EventHandleResult::ok_events(state, events);
441            };
442            events.push(EventPluginized::delayed(
443                OverlordEvent::AutoChestOpenItemCase {
444                    batch_size: state.auto_chest.batch_size,
445                },
446                game_config.game_settings.auto_chest_pause_duration_ticks,
447            ));
448        }
449
450        EventHandleResult::ok_events(state, events)
451    }
452
453    pub fn handle_sell_item(
454        &self,
455        item_id: Uuid,
456        mut state: OverlordState,
457    ) -> EventHandleResult<OverlordEvent, OverlordState> {
458        let game_config = self.game_config.get();
459
460        let Some(inv_item_idx) = state
461            .character_state
462            .inventory
463            .iter()
464            .position(|x| x.id == item_id)
465        else {
466            tracing::error!("Tried selling undefined item: item_id={}", item_id);
467            return EventHandleResult::fail(state);
468        };
469
470        let removed_item = state.character_state.inventory.remove(inv_item_idx);
471
472        let mut events = Vec::new();
473
474        let buff_mult = currency_exp_buff_multiplier(
475            &state.character_state.active_buffs,
476            &game_config.buff_templates,
477            ::time::utc_now(),
478        );
479        let scaled_price: Vec<CurrencyUnit> = removed_item
480            .price
481            .iter()
482            .map(|c| CurrencyUnit {
483                currency_id: c.currency_id,
484                amount: apply_buff_multiplier(c.amount, buff_mult),
485            })
486            .collect();
487
488        events.push(Self::currency_increase(
489            &scaled_price,
490            CurrencySource::ItemSell,
491        ));
492        events.push(EventPluginized::now(OverlordEvent::ItemSold { item_id }));
493        state.character_state.character.character_experience +=
494            apply_buff_multiplier(removed_item.experience, buff_mult);
495
496        if state.character_state.character.auto_chest_enabled {
497            if state
498                .character_state
499                .inventory
500                .iter()
501                .any(|item| !item.is_equipped)
502            {
503                return EventHandleResult::ok_events(state, events);
504            };
505            events.push(EventPluginized::delayed(
506                OverlordEvent::AutoChestOpenItemCase {
507                    batch_size: state.auto_chest.batch_size,
508                },
509                game_config.game_settings.auto_chest_pause_duration_ticks,
510            ));
511        }
512
513        EventHandleResult::ok_events(state, events)
514    }
515
516    pub fn handle_enable_auto_sell(
517        &self,
518        mut state: OverlordState,
519    ) -> EventHandleResult<OverlordEvent, OverlordState> {
520        state.character_state.character_settings.auto_sell_enabled = true;
521
522        EventHandleResult::ok(state)
523    }
524
525    pub fn handle_disable_auto_sell(
526        &self,
527        mut state: OverlordState,
528    ) -> EventHandleResult<OverlordEvent, OverlordState> {
529        state.character_state.character_settings.auto_sell_enabled = false;
530
531        EventHandleResult::ok(state)
532    }
533
534    pub fn handle_set_gear_override_enabled(
535        &self,
536        item_type: ItemType,
537        enabled: bool,
538        mut state: OverlordState,
539    ) -> EventHandleResult<OverlordEvent, OverlordState> {
540        let overrides = &mut state
541            .character_state
542            .character_settings
543            .gear_override_enabled_item_types;
544
545        if enabled {
546            if !overrides.contains(&item_type) {
547                overrides.push(item_type);
548            }
549            return EventHandleResult::ok(state);
550        }
551
552        overrides.retain(|t| *t != item_type);
553
554        let game_config = self.game_config.get();
555        let skin_ids: Vec<Uuid> = state
556            .character_state
557            .inventory
558            .iter()
559            .filter(|item| item.is_equipped && item.item_type == item_type)
560            .filter_map(|item| {
561                game_config
562                    .item_template(item.item_template_id)
563                    .and_then(|template| template.skin_id)
564            })
565            .collect();
566
567        if skin_ids.is_empty() {
568            return EventHandleResult::ok(state);
569        }
570
571        EventHandleResult::ok_events(
572            state,
573            vec![EventPluginized::now(OverlordEvent::EquipAndUnequipSkins {
574                equip_skin_ids: skin_ids,
575                unequip_skin_ids: vec![],
576            })],
577        )
578    }
579
580    pub fn handle_enable_case_upgrade_pop_up(
581        &self,
582        mut state: OverlordState,
583    ) -> EventHandleResult<OverlordEvent, OverlordState> {
584        state
585            .character_state
586            .character_settings
587            .dont_show_case_upgrade_popup_today = false;
588
589        EventHandleResult::ok(state)
590    }
591
592    pub fn handle_disable_case_upgrade_pop_up(
593        &self,
594        mut state: OverlordState,
595    ) -> EventHandleResult<OverlordEvent, OverlordState> {
596        state
597            .character_state
598            .character_settings
599            .dont_show_case_upgrade_popup_today = true;
600
601        EventHandleResult::ok(state)
602    }
603}