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 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 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 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 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
588pub 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}