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, script::random::GameRng, system::EventHandleResult};
16
17use uuid::Uuid;
18
19impl OverlordLogic {
20 pub(crate) fn open_item_case(
24 &self,
25 batch_size: i64,
26 max_batch: i64,
27 rng: &mut rand::rngs::StdRng,
28 state: OverlordState,
29 ) -> EventHandleResult<OverlordEvent, OverlordState> {
30 let game_config = self.game_config.get();
31
32 tracing::debug!("Open case handler !frontend");
33
34 let character_item_case_level = state.character_state.character.item_case_level;
35 let Ok(case_settings) =
36 game_config.require_item_case_settings_by_level(character_item_case_level)
37 else {
38 tracing::error!(
39 "Couldn't find item_case_settings with level = {character_item_case_level}"
40 );
41 return EventHandleResult::fail(state);
42 };
43 if batch_size > max_batch {
44 tracing::error!("Batch size {batch_size} is bigger than max allowed {max_batch}");
45 return EventHandleResult::fail(state);
46 }
47
48 use crate::{cases, gacha};
49
50 let mut items: Vec<Item> = Vec::with_capacity(batch_size.max(0) as usize);
55 for _ in 0..batch_size {
56 let case_seed: u64 = rand::RngExt::random(rng);
61 let mut item = match gacha::item_case::try_open_item_case(
62 &state.character_state,
63 &game_config,
64 Some(case_seed),
65 &self.behaviors,
66 &items,
67 ) {
68 Ok(x) => x,
69 Err(e) => {
70 tracing::error!("Couldn't open case: {e}");
71 continue;
72 }
73 };
74
75 let attr_seed: u64 = rand::RngExt::random(rng);
79 let attr_rng = GameRng::new(rand::SeedableRng::seed_from_u64(attr_seed));
80 match cases::try_finalize_item(&mut item, &game_config, &self.behaviors, &attr_rng) {
81 Ok(()) => items.push(item),
82 Err(e) => {
83 tracing::error!("Failed to finalize item: {}", e);
84 }
85 }
86 }
87
88 let ball_events: Vec<_> = items
98 .iter()
99 .filter_map(|item| {
100 match crate::mechanics::plinko::roll_path_for_rarity(
101 &game_config.plinko_settings,
102 character_item_case_level,
103 &case_settings.rarity_weights,
104 item.rarity.id,
105 rng,
106 ) {
107 Ok(path) => Some(EventPluginized::now(OverlordEvent::PlinkoBallDropped {
108 item_id: item.id,
109 path,
110 })),
111 Err(e) => {
112 tracing::error!("Couldn't roll plinko path for item {}: {e}", item.id);
113 None
114 }
115 }
116 })
117 .collect();
118
119 let mut events = vec![EventPluginized::now(OverlordEvent::PlayerNewItems {
124 items,
125 })];
126 events.extend(ball_events);
127
128 EventHandleResult::ok_events(state, events)
129 }
130
131 pub fn handle_plinko_ball_dropped(
140 &self,
141 item_id: uuid::Uuid,
142 path: &[essences::plinko::PlinkoDirection],
143 mut state: OverlordState,
144 ) -> EventHandleResult<OverlordEvent, OverlordState> {
145 let game_config = self.game_config.get();
146 let settings = &game_config.plinko_settings;
147
148 if path.len() as i64 != settings.rows {
149 tracing::error!(
150 "Plinko path for item {item_id} has {} steps, expected {}",
151 path.len(),
152 settings.rows
153 );
154 return EventHandleResult::fail(state);
155 }
156
157 let chest_level = state.character_state.character.item_case_level;
161 crate::mechanics::plinko::apply_pin_bonuses(
162 settings,
163 chest_level,
164 path,
165 &mut state.character_state.plinko_pin_bonuses,
166 );
167
168 EventHandleResult::ok(state)
169 }
170
171 pub fn handle_open_item_case(
172 &self,
173 batch_size: i64,
174 mut rng: rand::rngs::StdRng,
175 state: OverlordState,
176 ) -> EventHandleResult<OverlordEvent, OverlordState> {
177 let game_config = self.game_config.get();
178
179 if state.character_state.character.auto_chest_enabled {
180 tracing::error!("Can't open item case when auto_chest is enabled");
181 return EventHandleResult::fail(state);
182 }
183
184 let required_currency = vec![CurrencyUnit {
185 currency_id: game_config.game_settings.item_case_currency_id,
186 amount: batch_size,
187 }];
188
189 if !check_can_decrease_currencies(&state.character_state.currencies, &required_currency) {
190 tracing::error!(
191 "Not enough currency for open_item_case opening\nGot = {:?}\nRequired = {:?}",
192 state.character_state.currencies,
193 required_currency
194 );
195
196 return EventHandleResult::fail(state);
197 };
198
199 if !self.frontend {
200 let max_batch = game_config
203 .item_case_settings_by_level(state.character_state.character.item_case_level)
204 .map(|s| s.auto_chest_settings.max_batch_size)
205 .unwrap_or(0);
206 return self.open_item_case(batch_size, max_batch, &mut rng, state);
207 }
208 EventHandleResult::ok(state)
209 }
210
211 pub fn handle_auto_chest_open_item_case(
212 &self,
213 batch_size: i64,
214 mut rng: rand::rngs::StdRng,
215 state: OverlordState,
216 ) -> EventHandleResult<OverlordEvent, OverlordState> {
217 let game_config = self.game_config.get();
218
219 if !state.character_state.character.auto_chest_enabled {
220 tracing::error!("Can't open auto_chest item case when auto_chest is disabled");
221 return EventHandleResult::fail(state);
222 }
223
224 let required_currency = vec![CurrencyUnit {
225 currency_id: game_config.game_settings.item_case_currency_id,
226 amount: batch_size,
227 }];
228
229 if !check_can_decrease_currencies(&state.character_state.currencies, &required_currency) {
230 tracing::error!(
231 "Not enough currency for auto_chest_item_case opening\nGot = {:?}\nRequired = {:?}",
232 state.character_state.currencies,
233 required_currency
234 );
235
236 return EventHandleResult::ok_events(
237 state,
238 vec![EventPluginized::now(OverlordEvent::DisableAutoChest {})],
239 );
240 };
241
242 if !self.frontend {
243 let max_batch = game_config
246 .item_case_settings_by_level(state.character_state.character.item_case_level)
247 .map(|s| s.auto_chest_settings.auto_only_max_batch_size)
248 .unwrap_or(0);
249 return self.open_item_case(batch_size, max_batch, &mut rng, state);
250 }
251 EventHandleResult::ok(state)
252 }
253
254 pub fn handle_player_new_items(
255 &self,
256 items: &[Item],
257 mut state: OverlordState,
258 ) -> EventHandleResult<OverlordEvent, OverlordState> {
259 let game_config = self.game_config.get();
260
261 let required_currency = vec![CurrencyUnit {
262 currency_id: game_config.game_settings.item_case_currency_id,
263 amount: items.len() as i64,
264 }];
265
266 let mut events = vec![];
267
268 let Some(currency_event) =
269 Self::currency_decrease(&state, &required_currency, CurrencyConsumer::ItemCaseOpen)
270 else {
271 return EventHandleResult::fail(state);
272 };
273 events.push(currency_event);
274
275 let mut items = items.to_owned();
276
277 if state.character_state.character.auto_chest_enabled {
278 let filter = state.auto_chest.active_filter.clone();
279 let power_compare_enabled = state.auto_chest.power_compare_enabled;
280 let items_to_equip = match self.filter_new_items(
281 &filter,
282 power_compare_enabled,
283 &items,
284 &mut state,
285 &mut events,
286 ) {
287 Ok(items_to_equip) => items_to_equip,
288 Err(e) => {
289 tracing::error!("Something went wrong while filtering = {}", e);
290 return EventHandleResult::fail(state);
291 }
292 };
293
294 let items_to_equip =
298 match self.merge_auto_stack(items_to_equip, &mut state, &mut events) {
299 Ok(winners) => winners,
300 Err(e) => {
301 tracing::error!(
302 "Something went wrong while merging the auto stack = {}",
303 e
304 );
305 return EventHandleResult::fail(state);
306 }
307 };
308
309 let delivered_balls = items.len() as u64;
315 let pause_ticks = game_config
316 .game_settings
317 .auto_chest_pause_duration_ticks
318 .saturating_add(
319 delivered_balls
320 .saturating_mul(game_config.game_settings.auto_chest_per_ball_pause_ticks),
321 );
322 events.push(EventPluginized::delayed(
323 OverlordEvent::AutoChestOpenItemCase {
324 batch_size: state.auto_chest.batch_size,
325 },
326 pause_ticks,
327 ));
328
329 items = items_to_equip;
330 }
331
332 for item in &items {
333 if let Some(item_template) = game_config.item_template(item.item_template_id)
334 && let Some(skin_id) = item_template.skin_id
335 {
336 if game_config.require_skin(skin_id).is_err() {
337 tracing::error!(
338 "Skin with id={} not found in config for item_template_id={}",
339 skin_id,
340 item.item_template_id
341 );
342 return EventHandleResult::fail(state);
343 }
344
345 if !state
346 .character_state
347 .character_skins
348 .is_already_unlocked(skin_id)
349 {
350 state
351 .character_state
352 .character_skins
353 .blocked
354 .retain(|s| *s != skin_id);
355 state
356 .character_state
357 .character_skins
358 .available
359 .push(skin_id);
360 }
361 }
362 }
363
364 state.character_state.inventory.append(&mut items);
365
366 EventHandleResult::ok_events(state, events)
367 }
368
369 fn filter_new_items(
370 &self,
371 filter: &Option<AutoChestFilter>,
372 power_compare_enabled: bool,
373 items: &[Item],
374 state: &mut OverlordState,
375 events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
376 ) -> anyhow::Result<Vec<Item>> {
377 let game_config = self.game_config.get();
378
379 let (item_powers, equipped_item_powers) =
380 state.auto_chest_power_maps(items, &game_config, &self.behaviors)?;
381
382 let (items_to_sell, items_to_equip) = filter_items(
383 items,
384 filter,
385 power_compare_enabled,
386 &game_config.item_rarities,
387 &item_powers,
388 &equipped_item_powers,
389 )?;
390
391 let buff_mult = currency_exp_buff_multiplier(
392 &state.character_state.active_buffs,
393 &game_config.buff_templates,
394 ::time::utc_now(),
395 );
396
397 let mut gained_currencies = vec![];
398
399 for item in items_to_sell {
400 let scaled_price: Vec<CurrencyUnit> = item
401 .price
402 .iter()
403 .map(|c| CurrencyUnit {
404 currency_id: c.currency_id,
405 amount: apply_buff_multiplier(c.amount, buff_mult),
406 })
407 .collect();
408 increase_currencies(&mut gained_currencies, &scaled_price);
409 state.character_state.character.character_experience +=
410 apply_buff_multiplier(item.experience, buff_mult);
411 events.push(EventPluginized::now(OverlordEvent::ItemSold {
412 item_id: item.id,
413 }));
414 }
415
416 events.push(Self::currency_increase(
417 &gained_currencies,
418 CurrencySource::AutoItemSell,
419 ));
420
421 Ok(items_to_equip)
422 }
423
424 fn merge_auto_stack(
434 &self,
435 incoming: Vec<Item>,
436 state: &mut OverlordState,
437 events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
438 ) -> anyhow::Result<Vec<Item>> {
439 let game_config = self.game_config.get();
440
441 let mut winners: Vec<Item> = Vec::new();
442 let mut losers: Vec<Item> = Vec::new();
443
444 for item in incoming {
445 let key = item.equipment_slot_key();
446
447 let mut candidates: Vec<Item> = Vec::new();
448 let mut i = 0;
449 while i < winners.len() {
450 if winners[i].equipment_slot_key() == key {
451 candidates.push(winners.remove(i));
452 } else {
453 i += 1;
454 }
455 }
456 let mut i = 0;
457 while i < state.character_state.inventory.len() {
458 let inv_item = &state.character_state.inventory[i];
459 if !inv_item.is_equipped && inv_item.equipment_slot_key() == key {
460 candidates.push(state.character_state.inventory.remove(i));
461 } else {
462 i += 1;
463 }
464 }
465 candidates.push(item);
466
467 let mut best_idx = 0;
470 let mut best_power = state.calculate_power_with_candidate_equipped(
471 &candidates[0],
472 &game_config,
473 &self.behaviors,
474 )?;
475 for (idx, candidate) in candidates.iter().enumerate().skip(1) {
476 let power = state.calculate_power_with_candidate_equipped(
477 candidate,
478 &game_config,
479 &self.behaviors,
480 )?;
481 if power > best_power {
482 best_idx = idx;
483 best_power = power;
484 }
485 }
486
487 for (idx, candidate) in candidates.into_iter().enumerate() {
488 if idx == best_idx {
489 winners.push(candidate);
490 } else {
491 losers.push(candidate);
492 }
493 }
494 }
495
496 if !losers.is_empty() {
497 let buff_mult = currency_exp_buff_multiplier(
498 &state.character_state.active_buffs,
499 &game_config.buff_templates,
500 ::time::utc_now(),
501 );
502 let mut gained_currencies = vec![];
503 for item in losers {
504 let scaled_price: Vec<CurrencyUnit> = item
505 .price
506 .iter()
507 .map(|c| CurrencyUnit {
508 currency_id: c.currency_id,
509 amount: apply_buff_multiplier(c.amount, buff_mult),
510 })
511 .collect();
512 increase_currencies(&mut gained_currencies, &scaled_price);
513 state.character_state.character.character_experience +=
514 apply_buff_multiplier(item.experience, buff_mult);
515 events.push(EventPluginized::now(OverlordEvent::ItemSold {
516 item_id: item.id,
517 }));
518 }
519 events.push(Self::currency_increase(
520 &gained_currencies,
521 CurrencySource::AutoItemSell,
522 ));
523 }
524
525 Ok(winners)
526 }
527
528 pub fn handle_player_equip_item(
529 &self,
530 item_id: Uuid,
531 mut state: OverlordState,
532 ) -> EventHandleResult<OverlordEvent, OverlordState> {
533 let game_config = self.game_config.get();
534
535 let Some(item) = state
536 .character_state
537 .inventory
538 .iter()
539 .find(|x| x.id == item_id)
540 .cloned()
541 else {
542 tracing::error!("Tried equiping non-existing item_id={}", item_id);
543 return EventHandleResult::fail(state);
544 };
545
546 let equipment_slot_key = item.equipment_slot_key();
547 let slot_is_unlocked = game_config
548 .inventory_levels
549 .iter()
550 .rev()
551 .find(|level| {
552 level.from_chapter_level <= state.character_state.character.current_chapter_level
553 })
554 .is_some_and(|level| {
555 level
556 .slots
557 .iter()
558 .any(|slot| slot.equipment_slot_key() == equipment_slot_key)
559 });
560 if !slot_is_unlocked {
561 tracing::warn!(
562 item_id = %item.id,
563 item_type = %item.item_type,
564 world_side = ?item.world_side,
565 chapter_level = state.character_state.character.current_chapter_level,
566 "Rejected equip into a locked logical equipment slot"
567 );
568 return EventHandleResult::fail(state);
569 }
570
571 let prev_item = state
572 .character_state
573 .inventory
574 .iter()
575 .find(|inv_item| {
576 inv_item.equipment_slot_key() == equipment_slot_key
577 && inv_item.id != item.id
578 && inv_item.is_equipped
579 })
580 .cloned();
581 let displaced_item_id = prev_item.as_ref().map(|prev| prev.id);
582
583 state
584 .character_state
585 .inventory
586 .iter_mut()
587 .map(|inv_item| {
588 if inv_item.equipment_slot_key() == equipment_slot_key {
589 inv_item.is_equipped = false;
590 }
591 if inv_item.id == item.id {
592 inv_item.is_equipped = true;
593 }
594 })
595 .count();
596
597 let mut equip_skin_ids = vec![];
598 let mut unequip_skin_ids = vec![];
599
600 let gear_override_enabled = state
601 .character_state
602 .character_settings
603 .gear_override_enabled_item_types
604 .contains(&item.item_type);
605
606 if !gear_override_enabled {
607 let currently_equipped = &state.character_state.character_skins.equipped;
608
609 if let Some(prev_item) = prev_item
612 && let Some(prev_item_template) =
613 game_config.item_template(prev_item.item_template_id)
614 && let Some(prev_skin_id) = prev_item_template.skin_id
615 && currently_equipped.contains(&prev_skin_id)
616 {
617 unequip_skin_ids.push(prev_skin_id);
618 }
619
620 if let Some(item_template) = game_config.item_template(item.item_template_id)
624 && let Some(skin_id) = item_template.skin_id
625 {
626 if game_config.require_skin(skin_id).is_err() {
627 tracing::error!(
628 "Skin with id={} not found in config for item_template_id={}",
629 skin_id,
630 item.item_template_id
631 );
632 return EventHandleResult::fail(state);
633 }
634
635 if currently_equipped.contains(&skin_id) {
636 unequip_skin_ids.retain(|id| *id != skin_id);
637 } else {
638 equip_skin_ids.push(skin_id);
639 }
640 }
641 }
642
643 let mut events = vec![];
644 if !equip_skin_ids.is_empty() || !unequip_skin_ids.is_empty() {
645 events.push(EventPluginized::now(OverlordEvent::EquipAndUnequipSkins {
646 equip_skin_ids,
647 unequip_skin_ids,
648 }));
649 }
650
651 if let Some(displaced) = displaced_item_id {
664 events.push(EventPluginized::now(OverlordEvent::SellItem {
665 item_id: displaced,
666 }));
667 }
668
669 EventHandleResult::ok_events(state, events)
673 }
674
675 pub fn handle_sell_item(
676 &self,
677 item_id: Uuid,
678 mut state: OverlordState,
679 ) -> EventHandleResult<OverlordEvent, OverlordState> {
680 let game_config = self.game_config.get();
681
682 let Some(inv_item_idx) = state
683 .character_state
684 .inventory
685 .iter()
686 .position(|x| x.id == item_id)
687 else {
688 tracing::error!("Tried selling undefined item: item_id={}", item_id);
689 return EventHandleResult::fail(state);
690 };
691
692 let removed_item = state.character_state.inventory.remove(inv_item_idx);
693
694 let mut events = Vec::new();
695
696 let buff_mult = currency_exp_buff_multiplier(
697 &state.character_state.active_buffs,
698 &game_config.buff_templates,
699 ::time::utc_now(),
700 );
701 let scaled_price: Vec<CurrencyUnit> = removed_item
702 .price
703 .iter()
704 .map(|c| CurrencyUnit {
705 currency_id: c.currency_id,
706 amount: apply_buff_multiplier(c.amount, buff_mult),
707 })
708 .collect();
709
710 events.push(Self::currency_increase(
711 &scaled_price,
712 CurrencySource::ItemSell,
713 ));
714 events.push(EventPluginized::now(OverlordEvent::ItemSold { item_id }));
715 state.character_state.character.character_experience +=
716 apply_buff_multiplier(removed_item.experience, buff_mult);
717
718 EventHandleResult::ok_events(state, events)
721 }
722
723 pub fn handle_enable_auto_sell(
724 &self,
725 mut state: OverlordState,
726 ) -> EventHandleResult<OverlordEvent, OverlordState> {
727 state.character_state.character_settings.auto_sell_enabled = true;
728
729 EventHandleResult::ok(state)
730 }
731
732 pub fn handle_disable_auto_sell(
733 &self,
734 mut state: OverlordState,
735 ) -> EventHandleResult<OverlordEvent, OverlordState> {
736 state.character_state.character_settings.auto_sell_enabled = false;
737
738 EventHandleResult::ok(state)
739 }
740
741 pub fn handle_set_gear_override_enabled(
742 &self,
743 item_type: ItemType,
744 enabled: bool,
745 mut state: OverlordState,
746 ) -> EventHandleResult<OverlordEvent, OverlordState> {
747 let overrides = &mut state
748 .character_state
749 .character_settings
750 .gear_override_enabled_item_types;
751
752 if enabled {
753 if !overrides.contains(&item_type) {
754 overrides.push(item_type);
755 }
756 return EventHandleResult::ok(state);
757 }
758
759 overrides.retain(|t| *t != item_type);
760
761 let game_config = self.game_config.get();
762 let skin_ids: Vec<Uuid> = state
763 .character_state
764 .inventory
765 .iter()
766 .filter(|item| item.is_equipped && item.item_type == item_type)
767 .filter_map(|item| {
768 game_config
769 .item_template(item.item_template_id)
770 .and_then(|template| template.skin_id)
771 })
772 .collect();
773
774 if skin_ids.is_empty() {
775 return EventHandleResult::ok(state);
776 }
777
778 EventHandleResult::ok_events(
779 state,
780 vec![EventPluginized::now(OverlordEvent::EquipAndUnequipSkins {
781 equip_skin_ids: skin_ids,
782 unequip_skin_ids: vec![],
783 })],
784 )
785 }
786
787 pub fn handle_enable_case_upgrade_pop_up(
788 &self,
789 mut state: OverlordState,
790 ) -> EventHandleResult<OverlordEvent, OverlordState> {
791 state
792 .character_state
793 .character_settings
794 .dont_show_case_upgrade_popup_today = false;
795
796 EventHandleResult::ok(state)
797 }
798
799 pub fn handle_disable_case_upgrade_pop_up(
800 &self,
801 mut state: OverlordState,
802 ) -> EventHandleResult<OverlordEvent, OverlordState> {
803 state
804 .character_state
805 .character_settings
806 .dont_show_case_upgrade_popup_today = true;
807
808 EventHandleResult::ok(state)
809 }
810}
811
812pub fn sweep_expired_items(
816 state: &mut OverlordState,
817 now: chrono::DateTime<chrono::Utc>,
818) -> Vec<EventPluginized<OverlordEvent, OverlordState>> {
819 let expired_ids: Vec<Uuid> = state
820 .character_state
821 .inventory
822 .iter()
823 .filter(|item| item.expires_at.is_some_and(|exp| exp <= now))
824 .map(|item| item.id)
825 .collect();
826
827 if expired_ids.is_empty() {
828 return Vec::new();
829 }
830
831 state
832 .character_state
833 .inventory
834 .retain(|item| !expired_ids.contains(&item.id));
835
836 vec![EventPluginized::now(OverlordEvent::ItemsExpired {
837 item_ids: expired_ids,
838 })]
839}