1use crate::{
2 comp::{
3 Item,
4 inventory::{
5 InvSlot,
6 item::{self, Hands, ItemDefinitionIdOwned, ItemKind, tool::Tool},
7 slot::{ArmorSlot, EquipSlot},
8 },
9 },
10 resources::Time,
11};
12use hashbrown::HashMap;
13use serde::{Deserialize, Serialize};
14use std::ops::Range;
15use tracing::warn;
16
17pub(super) const UNEQUIP_TRACKING_DURATION: f64 = 60.0;
18
19#[derive(Clone, Debug, Serialize, Deserialize)]
20pub struct Loadout {
21 slots: Vec<LoadoutSlot>,
22 #[serde(skip)]
24 pub(super) recently_unequipped_items: HashMap<ItemDefinitionIdOwned, (Time, u8)>,
27}
28
29#[derive(Clone, Debug, Serialize, Deserialize)]
31pub struct LoadoutSlot {
32 pub(super) equip_slot: EquipSlot,
34 slot: InvSlot,
36 #[serde(skip)]
39 persistence_key: String,
40}
41
42impl LoadoutSlot {
43 fn new(equip_slot: EquipSlot, persistence_key: String) -> LoadoutSlot {
44 LoadoutSlot {
45 equip_slot,
46 slot: None,
47 persistence_key,
48 }
49 }
50}
51
52pub(super) struct LoadoutSlotId {
53 pub loadout_idx: usize,
55 pub slot_idx: usize,
57}
58
59pub enum LoadoutError {
60 InvalidPersistenceKey,
61 NoParentAtSlot,
62}
63
64impl Loadout {
65 pub(super) fn new_empty() -> Self {
66 Self {
67 slots: vec![
68 (EquipSlot::Lantern, "lantern".to_string()),
69 (EquipSlot::Glider, "glider".to_string()),
70 (
71 EquipSlot::Armor(ArmorSlot::Shoulders),
72 "shoulder".to_string(),
73 ),
74 (EquipSlot::Armor(ArmorSlot::Chest), "chest".to_string()),
75 (EquipSlot::Armor(ArmorSlot::Belt), "belt".to_string()),
76 (EquipSlot::Armor(ArmorSlot::Hands), "hand".to_string()),
77 (EquipSlot::Armor(ArmorSlot::Legs), "pants".to_string()),
78 (EquipSlot::Armor(ArmorSlot::Feet), "foot".to_string()),
79 (EquipSlot::Armor(ArmorSlot::Back), "back".to_string()),
80 (EquipSlot::Armor(ArmorSlot::Ring1), "ring1".to_string()),
81 (EquipSlot::Armor(ArmorSlot::Ring2), "ring2".to_string()),
82 (EquipSlot::Armor(ArmorSlot::Neck), "neck".to_string()),
83 (EquipSlot::Armor(ArmorSlot::Head), "head".to_string()),
84 (EquipSlot::Armor(ArmorSlot::Tabard), "tabard".to_string()),
85 (EquipSlot::Armor(ArmorSlot::Bag1), "bag1".to_string()),
86 (EquipSlot::Armor(ArmorSlot::Bag2), "bag2".to_string()),
87 (EquipSlot::Armor(ArmorSlot::Bag3), "bag3".to_string()),
88 (EquipSlot::Armor(ArmorSlot::Bag4), "bag4".to_string()),
89 (EquipSlot::ActiveMainhand, "active_mainhand".to_string()),
90 (EquipSlot::ActiveOffhand, "active_offhand".to_string()),
91 (EquipSlot::InactiveMainhand, "inactive_mainhand".to_string()),
92 (EquipSlot::InactiveOffhand, "inactive_offhand".to_string()),
93 ]
94 .into_iter()
95 .map(|(equip_slot, persistence_key)| LoadoutSlot::new(equip_slot, persistence_key))
96 .collect(),
97 recently_unequipped_items: HashMap::new(),
98 }
99 }
100
101 pub(super) fn swap(
104 &mut self,
105 equip_slot: EquipSlot,
106 item: Option<Item>,
107 time: Time,
108 ) -> Option<Item> {
109 if let Some(item_def_id) = item.as_ref().map(|item| item.item_definition_id()) {
110 if let Some((_unequip_time, count)) =
111 self.recently_unequipped_items.get_mut(&item_def_id)
112 {
113 *count = count.saturating_sub(1);
114 }
115 }
116 self.cull_recently_unequipped_items(time);
117 let unequipped_item = self
118 .slots
119 .iter_mut()
120 .find(|x| x.equip_slot == equip_slot)
121 .and_then(|x| core::mem::replace(&mut x.slot, item));
122 if let Some(unequipped_item) = unequipped_item.as_ref() {
123 let entry = self
124 .recently_unequipped_items
125 .entry_ref(&unequipped_item.item_definition_id())
126 .or_insert((time, 0));
127 *entry = (time, entry.1.saturating_add(1));
128 }
129 unequipped_item
130 }
131
132 pub(super) fn equipped(&self, equip_slot: EquipSlot) -> Option<&Item> {
134 self.slot(equip_slot).and_then(|x| x.slot.as_ref())
135 }
136
137 fn slot(&self, equip_slot: EquipSlot) -> Option<&LoadoutSlot> {
138 self.slots
139 .iter()
140 .find(|loadout_slot| loadout_slot.equip_slot == equip_slot)
141 }
142
143 pub(super) fn loadout_idx_for_equip_slot(&self, equip_slot: EquipSlot) -> Option<usize> {
144 self.slots
145 .iter()
146 .position(|loadout_slot| loadout_slot.equip_slot == equip_slot)
147 }
148
149 pub(super) fn items_with_persistence_key(&self) -> impl Iterator<Item = (&str, Option<&Item>)> {
151 self.slots
152 .iter()
153 .map(|x| (x.persistence_key.as_str(), x.slot.as_ref()))
154 }
155
156 pub fn set_item_at_slot_using_persistence_key(
159 &mut self,
160 persistence_key: &str,
161 item: Item,
162 ) -> Result<(), LoadoutError> {
163 if let Some(slot) = self
164 .slots
165 .iter_mut()
166 .find(|x| x.persistence_key == persistence_key)
167 {
168 slot.slot = Some(item);
169 Ok(())
170 } else {
171 Err(LoadoutError::InvalidPersistenceKey)
172 }
173 }
174
175 pub fn get_mut_item_at_slot_using_persistence_key(
176 &mut self,
177 persistence_key: &str,
178 ) -> Result<&mut Item, LoadoutError> {
179 self.slots
180 .iter_mut()
181 .find(|loadout_slot| loadout_slot.persistence_key == persistence_key)
182 .map_or(Err(LoadoutError::InvalidPersistenceKey), |loadout_slot| {
183 loadout_slot
184 .slot
185 .as_mut()
186 .ok_or(LoadoutError::NoParentAtSlot)
187 })
188 }
189
190 pub(super) fn swap_slots(
192 &mut self,
193 equip_slot_a: EquipSlot,
194 equip_slot_b: EquipSlot,
195 time: Time,
196 ) {
197 if self.slot(equip_slot_b).is_none() || self.slot(equip_slot_b).is_none() {
198 warn!("Cannot swap slots for non-existent equip slot");
202 return;
203 }
204
205 let item_a = self.swap(equip_slot_a, None, time);
206 let item_b = self.swap(equip_slot_b, item_a, time);
207 assert_eq!(self.swap(equip_slot_a, item_b, time), None);
208
209 if !self.slot_can_hold(
211 equip_slot_a,
212 self.equipped(equip_slot_a).map(|x| x.kind()).as_deref(),
213 ) || !self.slot_can_hold(
214 equip_slot_b,
215 self.equipped(equip_slot_b).map(|x| x.kind()).as_deref(),
216 ) {
217 let item_a = self.swap(equip_slot_a, None, time);
219 let item_b = self.swap(equip_slot_b, item_a, time);
220 assert_eq!(self.swap(equip_slot_a, item_b, time), None);
221 }
222 }
223
224 pub(super) fn get_slot_to_equip_into(&self, item_kind: &ItemKind) -> Option<EquipSlot> {
230 let mut suitable_slots = self
231 .slots
232 .iter()
233 .filter(|s| self.slot_can_hold(s.equip_slot, Some(item_kind)));
234
235 let first = suitable_slots.next();
236
237 first
238 .into_iter()
239 .chain(suitable_slots)
240 .find(|loadout_slot| loadout_slot.slot.is_none())
241 .map(|x| x.equip_slot)
242 .or_else(|| first.map(|x| x.equip_slot))
243 }
244
245 pub(super) fn equipped_items_replaceable_by<'a>(
248 &'a self,
249 item_kind: &'a ItemKind,
250 ) -> impl Iterator<Item = &'a Item> {
251 self.slots
252 .iter()
253 .filter(move |s| self.slot_can_hold(s.equip_slot, Some(item_kind)))
254 .filter_map(|s| s.slot.as_ref())
255 }
256
257 pub(super) fn inv_slot(&self, loadout_slot_id: LoadoutSlotId) -> Option<&InvSlot> {
259 self.slots
260 .get(loadout_slot_id.loadout_idx)
261 .and_then(|loadout_slot| loadout_slot.slot.as_ref())
262 .and_then(|item| item.slot(loadout_slot_id.slot_idx))
263 }
264
265 pub(super) fn inv_slot_mut(&mut self, loadout_slot_id: LoadoutSlotId) -> Option<&mut InvSlot> {
267 self.slots
268 .get_mut(loadout_slot_id.loadout_idx)
269 .and_then(|loadout_slot| loadout_slot.slot.as_mut())
270 .and_then(|item| item.slot_mut(loadout_slot_id.slot_idx))
271 }
272
273 pub(super) fn inv_slots_with_id(&self) -> impl Iterator<Item = (LoadoutSlotId, &InvSlot)> {
276 self.slots
277 .iter()
278 .enumerate()
279 .filter_map(|(i, loadout_slot)| {
280 loadout_slot.slot.as_ref().map(|item| (i, item.slots()))
281 })
282 .flat_map(|(loadout_slot_index, loadout_slots)| {
283 loadout_slots
284 .iter()
285 .enumerate()
286 .map(move |(item_slot_index, inv_slot)| {
287 (
288 LoadoutSlotId {
289 loadout_idx: loadout_slot_index,
290 slot_idx: item_slot_index,
291 },
292 inv_slot,
293 )
294 })
295 })
296 }
297
298 pub(super) fn inv_slots_mut(&mut self) -> impl Iterator<Item = &mut InvSlot> {
300 self.slots.iter_mut()
301 .filter_map(|x| x.slot.as_mut().map(|item| item.slots_mut())) .flat_map(|loadout_slots| loadout_slots.iter_mut()) }
304
305 pub(super) fn inv_slots_mut_with_mutable_recently_unequipped_items(
306 &mut self,
307 ) -> (
308 impl Iterator<Item = &mut InvSlot>,
309 &mut HashMap<ItemDefinitionIdOwned, (Time, u8)>,
310 ) {
311 let slots_mut = self.slots.iter_mut()
312 .filter_map(|x| x.slot.as_mut().map(|item| item.slots_mut())) .flat_map(|loadout_slots| loadout_slots.iter_mut()); (slots_mut, &mut self.recently_unequipped_items)
315 }
316
317 pub(super) fn slot_range_for_equip_slot(&self, equip_slot: EquipSlot) -> Option<Range<usize>> {
320 self.slots
321 .iter()
322 .map(|loadout_slot| {
323 (
324 loadout_slot.equip_slot,
325 loadout_slot
326 .slot
327 .as_ref()
328 .map_or(0, |item| item.slots().len()),
329 )
330 })
331 .scan(0, |acc_len, (equip_slot, len)| {
332 let res = Some((equip_slot, len, *acc_len));
333 *acc_len += len;
334 res
335 })
336 .find(|(e, len, _)| *e == equip_slot && len > &0)
337 .map(|(_, slot_len, start)| start..start + slot_len)
338 }
339
340 #[must_use = "Returned item will be lost if not used"]
343 pub(super) fn try_equip(&mut self, item: Item) -> Result<(), Item> {
344 let loadout_slot = self
345 .slots
346 .iter()
347 .find(|s| s.slot.is_none() && self.slot_can_hold(s.equip_slot, Some(&*item.kind())))
348 .map(|s| s.equip_slot);
349 if let Some(slot) = self
350 .slots
351 .iter_mut()
352 .find(|s| Some(s.equip_slot) == loadout_slot)
353 {
354 slot.slot = Some(item);
355 Ok(())
356 } else {
357 Err(item)
358 }
359 }
360
361 pub(super) fn items(&self) -> impl Iterator<Item = &Item> {
362 self.slots.iter().filter_map(|x| x.slot.as_ref())
363 }
364
365 pub(super) fn items_with_slot(&self) -> impl Iterator<Item = (EquipSlot, &Item)> {
366 self.slots
367 .iter()
368 .filter_map(|x| x.slot.as_ref().map(|i| (x.equip_slot, i)))
369 }
370
371 pub(super) fn slot_can_hold(
373 &self,
374 equip_slot: EquipSlot,
375 item_kind: Option<&ItemKind>,
376 ) -> bool {
377 if !(match equip_slot {
380 EquipSlot::ActiveMainhand => Loadout::is_valid_weapon_pair(
381 item_kind,
382 self.equipped(EquipSlot::ActiveOffhand)
383 .map(|x| x.kind())
384 .as_deref(),
385 ),
386 EquipSlot::ActiveOffhand => Loadout::is_valid_weapon_pair(
387 self.equipped(EquipSlot::ActiveMainhand)
388 .map(|x| x.kind())
389 .as_deref(),
390 item_kind,
391 ),
392 EquipSlot::InactiveMainhand => Loadout::is_valid_weapon_pair(
393 item_kind,
394 self.equipped(EquipSlot::InactiveOffhand)
395 .map(|x| x.kind())
396 .as_deref(),
397 ),
398 EquipSlot::InactiveOffhand => Loadout::is_valid_weapon_pair(
399 self.equipped(EquipSlot::InactiveMainhand)
400 .map(|x| x.kind())
401 .as_deref(),
402 item_kind,
403 ),
404 _ => true,
405 }) {
406 return false;
407 }
408
409 item_kind.is_none_or(|item| equip_slot.can_hold(item))
410 }
411
412 #[rustfmt::skip]
413 fn is_valid_weapon_pair(main_hand: Option<&ItemKind>, off_hand: Option<&ItemKind>) -> bool {
414 matches!((main_hand, off_hand),
415 (Some(ItemKind::Tool(Tool { hands: Hands::One, .. })), None) |
416 (Some(ItemKind::Tool(Tool { hands: Hands::Two, .. })), None) |
417 (Some(ItemKind::Tool(Tool { hands: Hands::One, .. })), Some(ItemKind::Tool(Tool { hands: Hands::One, .. }))) |
418 (None, None))
419 }
420
421 pub(super) fn swap_equipped_weapons(&mut self, time: Time) {
422 let valid_slot = |equip_slot| {
425 self.equipped(equip_slot)
426 .is_none_or(|i| self.slot_can_hold(equip_slot, Some(&*i.kind())))
427 };
428
429 if valid_slot(EquipSlot::ActiveMainhand)
434 && valid_slot(EquipSlot::ActiveOffhand)
435 && valid_slot(EquipSlot::InactiveMainhand)
436 && valid_slot(EquipSlot::InactiveOffhand)
437 {
438 let active_mainhand = self.swap(EquipSlot::ActiveMainhand, None, time);
440 let active_offhand = self.swap(EquipSlot::ActiveOffhand, None, time);
441 let inactive_mainhand = self.swap(EquipSlot::InactiveMainhand, None, time);
442 let inactive_offhand = self.swap(EquipSlot::InactiveOffhand, None, time);
443 assert!(
445 self.swap(EquipSlot::ActiveMainhand, inactive_mainhand, time)
446 .is_none()
447 );
448 assert!(
449 self.swap(EquipSlot::ActiveOffhand, inactive_offhand, time)
450 .is_none()
451 );
452 assert!(
453 self.swap(EquipSlot::InactiveMainhand, active_mainhand, time)
454 .is_none()
455 );
456 assert!(
457 self.swap(EquipSlot::InactiveOffhand, active_offhand, time)
458 .is_none()
459 );
460 }
461 }
462
463 pub fn persistence_update_all_item_states(
466 &mut self,
467 ability_map: &item::tool::AbilityMap,
468 msm: &item::MaterialStatManifest,
469 ) {
470 self.slots.iter_mut().for_each(|slot| {
471 if let Some(item) = &mut slot.slot {
472 item.update_item_state(ability_map, msm);
473 }
474 });
475 }
476
477 pub(super) fn damage_items(
479 &mut self,
480 ability_map: &item::tool::AbilityMap,
481 msm: &item::MaterialStatManifest,
482 ) {
483 self.slots
484 .iter_mut()
485 .filter_map(|slot| slot.slot.as_mut())
486 .filter(|item| item.has_durability())
487 .for_each(|item| item.increment_damage(ability_map, msm));
488 }
489
490 pub(super) fn repair_item_at_slot(
492 &mut self,
493 equip_slot: EquipSlot,
494 ability_map: &item::tool::AbilityMap,
495 msm: &item::MaterialStatManifest,
496 ) {
497 if let Some(item) = self
498 .slots
499 .iter_mut()
500 .find(|slot| slot.equip_slot == equip_slot)
501 .and_then(|slot| slot.slot.as_mut())
502 {
503 item.reset_durability(ability_map, msm);
504 }
505 }
506
507 pub(super) fn cull_recently_unequipped_items(&mut self, time: Time) {
508 self.recently_unequipped_items
509 .retain(|_def, (unequip_time, count)| {
510 if time.0 < unequip_time.0 {
513 *unequip_time = time;
514 }
515
516 (time.0 - unequip_time.0 < UNEQUIP_TRACKING_DURATION) && *count > 0
517 });
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use crate::{
524 comp::{
525 Item,
526 inventory::{
527 item::{
528 ItemKind,
529 armor::{Armor, ArmorKind, Protection},
530 },
531 loadout::Loadout,
532 slot::{ArmorSlot, EquipSlot},
533 test_helpers::get_test_bag,
534 },
535 },
536 resources::Time,
537 };
538
539 #[test]
540 fn test_slot_range_for_equip_slot() {
541 let mut loadout = Loadout::new_empty();
542
543 let bag1_slot = EquipSlot::Armor(ArmorSlot::Bag1);
544 let bag = get_test_bag(18);
545 loadout.swap(bag1_slot, Some(bag), Time(0.0));
546
547 let result = loadout.slot_range_for_equip_slot(bag1_slot).unwrap();
548
549 assert_eq!(0..18, result);
550 }
551
552 #[test]
553 fn test_slot_range_for_equip_slot_no_item() {
554 let loadout = Loadout::new_empty();
555 let result = loadout.slot_range_for_equip_slot(EquipSlot::Armor(ArmorSlot::Bag1));
556
557 assert_eq!(None, result);
558 }
559
560 #[test]
561 fn test_slot_range_for_equip_slot_item_without_slots() {
562 let mut loadout = Loadout::new_empty();
563
564 let feet_slot = EquipSlot::Armor(ArmorSlot::Feet);
565 let boots = Item::new_from_asset_expect("common.items.testing.test_boots");
566 loadout.swap(feet_slot, Some(boots), Time(0.0));
567 let result = loadout.slot_range_for_equip_slot(feet_slot);
568
569 assert_eq!(None, result);
570 }
571
572 #[test]
573 fn test_get_slot_to_equip_into_second_bag_slot_free() {
574 let mut loadout = Loadout::new_empty();
575
576 loadout.swap(
577 EquipSlot::Armor(ArmorSlot::Bag1),
578 Some(get_test_bag(1)),
579 Time(0.0),
580 );
581
582 let result = loadout
583 .get_slot_to_equip_into(&ItemKind::Armor(Armor::test_armor(
584 ArmorKind::Bag,
585 Protection::Normal(0.0),
586 Protection::Normal(0.0),
587 )))
588 .unwrap();
589
590 assert_eq!(EquipSlot::Armor(ArmorSlot::Bag2), result);
591 }
592
593 #[test]
594 fn test_get_slot_to_equip_into_no_bag_slots_free() {
595 let mut loadout = Loadout::new_empty();
596
597 loadout.swap(
598 EquipSlot::Armor(ArmorSlot::Bag1),
599 Some(get_test_bag(1)),
600 Time(0.0),
601 );
602 loadout.swap(
603 EquipSlot::Armor(ArmorSlot::Bag2),
604 Some(get_test_bag(1)),
605 Time(0.0),
606 );
607 loadout.swap(
608 EquipSlot::Armor(ArmorSlot::Bag3),
609 Some(get_test_bag(1)),
610 Time(0.0),
611 );
612 loadout.swap(
613 EquipSlot::Armor(ArmorSlot::Bag4),
614 Some(get_test_bag(1)),
615 Time(0.0),
616 );
617
618 let result = loadout
619 .get_slot_to_equip_into(&ItemKind::Armor(Armor::test_armor(
620 ArmorKind::Bag,
621 Protection::Normal(0.0),
622 Protection::Normal(0.0),
623 )))
624 .unwrap();
625
626 assert_eq!(EquipSlot::Armor(ArmorSlot::Bag1), result);
627 }
628}