overlord_event_system/logic/
items.rs

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