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: &Item) -> Option<EquipSlot> {
229 let mut suitable_slots = self
230 .slots
231 .iter()
232 .filter(|s| self.slot_can_hold(s.equip_slot, Some(&*item.kind())));
233
234 let first_suitable = suitable_slots.next();
235
236 let mut differing_suitable_slots =
237 first_suitable
238 .into_iter()
239 .chain(suitable_slots)
240 .filter(|loadout_slot| {
241 loadout_slot.slot.as_ref().is_none_or(|equipped_item| {
242 equipped_item.item_definition_id() != item.item_definition_id()
243 || equipped_item.durability_lost() != item.durability_lost()
244 })
245 });
246
247 let first_differing = differing_suitable_slots.next();
248
249 first_differing
250 .into_iter()
251 .chain(differing_suitable_slots)
252 .find(|loadout_slot| loadout_slot.slot.is_none())
253 .map(|x| x.equip_slot)
254 .or_else(|| first_differing.map(|x| x.equip_slot))
255 .or_else(|| first_suitable.map(|x| x.equip_slot))
256 }
257
258 pub(super) fn equipped_items_replaceable_by<'a>(
261 &'a self,
262 item_kind: &'a ItemKind,
263 ) -> impl Iterator<Item = &'a Item> {
264 self.slots
265 .iter()
266 .filter(move |s| self.slot_can_hold(s.equip_slot, Some(item_kind)))
267 .filter_map(|s| s.slot.as_ref())
268 }
269
270 pub(super) fn inv_slot(&self, loadout_slot_id: LoadoutSlotId) -> Option<&InvSlot> {
272 self.slots
273 .get(loadout_slot_id.loadout_idx)
274 .and_then(|loadout_slot| loadout_slot.slot.as_ref())
275 .and_then(|item| item.slot(loadout_slot_id.slot_idx))
276 }
277
278 pub(super) fn inv_slot_mut(&mut self, loadout_slot_id: LoadoutSlotId) -> Option<&mut InvSlot> {
280 self.slots
281 .get_mut(loadout_slot_id.loadout_idx)
282 .and_then(|loadout_slot| loadout_slot.slot.as_mut())
283 .and_then(|item| item.slot_mut(loadout_slot_id.slot_idx))
284 }
285
286 pub(super) fn inv_slots_with_id(&self) -> impl Iterator<Item = (LoadoutSlotId, &InvSlot)> {
289 self.slots
290 .iter()
291 .enumerate()
292 .filter_map(|(i, loadout_slot)| {
293 loadout_slot.slot.as_ref().map(|item| (i, item.slots()))
294 })
295 .flat_map(|(loadout_slot_index, loadout_slots)| {
296 loadout_slots
297 .iter()
298 .enumerate()
299 .map(move |(item_slot_index, inv_slot)| {
300 (
301 LoadoutSlotId {
302 loadout_idx: loadout_slot_index,
303 slot_idx: item_slot_index,
304 },
305 inv_slot,
306 )
307 })
308 })
309 }
310
311 pub(super) fn inv_slots_mut(&mut self) -> impl Iterator<Item = &mut InvSlot> {
313 self.slots.iter_mut()
314 .filter_map(|x| x.slot.as_mut().map(|item| item.slots_mut())) .flat_map(|loadout_slots| loadout_slots.iter_mut()) }
317
318 pub(super) fn inv_slots_mut_with_mutable_recently_unequipped_items(
319 &mut self,
320 ) -> (
321 impl Iterator<Item = &mut InvSlot>,
322 &mut HashMap<ItemDefinitionIdOwned, (Time, u8)>,
323 ) {
324 let slots_mut = self.slots.iter_mut()
325 .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)
328 }
329
330 pub(super) fn slot_range_for_equip_slot(&self, equip_slot: EquipSlot) -> Option<Range<usize>> {
333 self.slots
334 .iter()
335 .map(|loadout_slot| {
336 (
337 loadout_slot.equip_slot,
338 loadout_slot
339 .slot
340 .as_ref()
341 .map_or(0, |item| item.slots().len()),
342 )
343 })
344 .scan(0, |acc_len, (equip_slot, len)| {
345 let res = Some((equip_slot, len, *acc_len));
346 *acc_len += len;
347 res
348 })
349 .find(|(e, len, _)| *e == equip_slot && len > &0)
350 .map(|(_, slot_len, start)| start..start + slot_len)
351 }
352
353 #[must_use = "Returned item will be lost if not used"]
356 pub(super) fn try_equip(&mut self, item: Item) -> Result<(), Item> {
357 let loadout_slot = self
358 .slots
359 .iter()
360 .find(|s| s.slot.is_none() && self.slot_can_hold(s.equip_slot, Some(&*item.kind())))
361 .map(|s| s.equip_slot);
362 if let Some(slot) = self
363 .slots
364 .iter_mut()
365 .find(|s| Some(s.equip_slot) == loadout_slot)
366 {
367 slot.slot = Some(item);
368 Ok(())
369 } else {
370 Err(item)
371 }
372 }
373
374 pub(super) fn items(&self) -> impl Iterator<Item = &Item> {
375 self.slots.iter().filter_map(|x| x.slot.as_ref())
376 }
377
378 pub(super) fn items_with_slot(&self) -> impl Iterator<Item = (EquipSlot, &Item)> {
379 self.slots
380 .iter()
381 .filter_map(|x| x.slot.as_ref().map(|i| (x.equip_slot, i)))
382 }
383
384 pub(super) fn slot_can_hold(
386 &self,
387 equip_slot: EquipSlot,
388 item_kind: Option<&ItemKind>,
389 ) -> bool {
390 if !(match equip_slot {
393 EquipSlot::ActiveMainhand => Loadout::is_valid_weapon_pair(
394 item_kind,
395 self.equipped(EquipSlot::ActiveOffhand)
396 .map(|x| x.kind())
397 .as_deref(),
398 ),
399 EquipSlot::ActiveOffhand => Loadout::is_valid_weapon_pair(
400 self.equipped(EquipSlot::ActiveMainhand)
401 .map(|x| x.kind())
402 .as_deref(),
403 item_kind,
404 ),
405 EquipSlot::InactiveMainhand => Loadout::is_valid_weapon_pair(
406 item_kind,
407 self.equipped(EquipSlot::InactiveOffhand)
408 .map(|x| x.kind())
409 .as_deref(),
410 ),
411 EquipSlot::InactiveOffhand => Loadout::is_valid_weapon_pair(
412 self.equipped(EquipSlot::InactiveMainhand)
413 .map(|x| x.kind())
414 .as_deref(),
415 item_kind,
416 ),
417 _ => true,
418 }) {
419 return false;
420 }
421
422 item_kind.is_none_or(|item| equip_slot.can_hold(item))
423 }
424
425 #[rustfmt::skip]
426 fn is_valid_weapon_pair(main_hand: Option<&ItemKind>, off_hand: Option<&ItemKind>) -> bool {
427 matches!((main_hand, off_hand),
428 (Some(ItemKind::Tool(Tool { hands: Hands::One, .. })), None) |
429 (Some(ItemKind::Tool(Tool { hands: Hands::Two, .. })), None) |
430 (Some(ItemKind::Tool(Tool { hands: Hands::One, .. })), Some(ItemKind::Tool(Tool { hands: Hands::One, .. }))) |
431 (None, None))
432 }
433
434 pub(super) fn swap_equipped_weapons(&mut self, time: Time) {
435 let valid_slot = |equip_slot| {
438 self.equipped(equip_slot)
439 .is_none_or(|i| self.slot_can_hold(equip_slot, Some(&*i.kind())))
440 };
441
442 if valid_slot(EquipSlot::ActiveMainhand)
447 && valid_slot(EquipSlot::ActiveOffhand)
448 && valid_slot(EquipSlot::InactiveMainhand)
449 && valid_slot(EquipSlot::InactiveOffhand)
450 {
451 let active_mainhand = self.swap(EquipSlot::ActiveMainhand, None, time);
453 let active_offhand = self.swap(EquipSlot::ActiveOffhand, None, time);
454 let inactive_mainhand = self.swap(EquipSlot::InactiveMainhand, None, time);
455 let inactive_offhand = self.swap(EquipSlot::InactiveOffhand, None, time);
456 assert!(
458 self.swap(EquipSlot::ActiveMainhand, inactive_mainhand, time)
459 .is_none()
460 );
461 assert!(
462 self.swap(EquipSlot::ActiveOffhand, inactive_offhand, time)
463 .is_none()
464 );
465 assert!(
466 self.swap(EquipSlot::InactiveMainhand, active_mainhand, time)
467 .is_none()
468 );
469 assert!(
470 self.swap(EquipSlot::InactiveOffhand, active_offhand, time)
471 .is_none()
472 );
473 }
474 }
475
476 pub fn persistence_update_all_item_states(
479 &mut self,
480 ability_map: &item::tool::AbilityMap,
481 msm: &item::MaterialStatManifest,
482 ) {
483 self.slots.iter_mut().for_each(|slot| {
484 if let Some(item) = &mut slot.slot {
485 item.update_item_state(ability_map, msm);
486 }
487 });
488 }
489
490 pub(super) fn damage_items(
492 &mut self,
493 ability_map: &item::tool::AbilityMap,
494 msm: &item::MaterialStatManifest,
495 ) {
496 self.slots
497 .iter_mut()
498 .filter_map(|slot| slot.slot.as_mut())
499 .filter(|item| item.has_durability())
500 .for_each(|item| item.increment_damage(ability_map, msm));
501 }
502
503 pub(super) fn repair_item_at_slot(
505 &mut self,
506 equip_slot: EquipSlot,
507 ability_map: &item::tool::AbilityMap,
508 msm: &item::MaterialStatManifest,
509 ) {
510 if let Some(item) = self
511 .slots
512 .iter_mut()
513 .find(|slot| slot.equip_slot == equip_slot)
514 .and_then(|slot| slot.slot.as_mut())
515 {
516 item.reset_durability(ability_map, msm);
517 }
518 }
519
520 pub(super) fn cull_recently_unequipped_items(&mut self, time: Time) {
521 self.recently_unequipped_items
522 .retain(|_def, (unequip_time, count)| {
523 if time.0 < unequip_time.0 {
526 *unequip_time = time;
527 }
528
529 (time.0 - unequip_time.0 < UNEQUIP_TRACKING_DURATION) && *count > 0
530 });
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use crate::{
537 comp::{
538 Item,
539 inventory::{
540 loadout::Loadout,
541 slot::{ArmorSlot, EquipSlot},
542 test_helpers::get_test_bag,
543 },
544 },
545 resources::Time,
546 };
547
548 #[test]
549 fn test_slot_range_for_equip_slot() {
550 let mut loadout = Loadout::new_empty();
551
552 let bag1_slot = EquipSlot::Armor(ArmorSlot::Bag1);
553 let bag = get_test_bag(18);
554 loadout.swap(bag1_slot, Some(bag), Time(0.0));
555
556 let result = loadout.slot_range_for_equip_slot(bag1_slot).unwrap();
557
558 assert_eq!(0..18, result);
559 }
560
561 #[test]
562 fn test_slot_range_for_equip_slot_no_item() {
563 let loadout = Loadout::new_empty();
564 let result = loadout.slot_range_for_equip_slot(EquipSlot::Armor(ArmorSlot::Bag1));
565
566 assert_eq!(None, result);
567 }
568
569 #[test]
570 fn test_slot_range_for_equip_slot_item_without_slots() {
571 let mut loadout = Loadout::new_empty();
572
573 let feet_slot = EquipSlot::Armor(ArmorSlot::Feet);
574 let boots = Item::new_from_asset_expect("common.items.testing.test_boots");
575 loadout.swap(feet_slot, Some(boots), Time(0.0));
576 let result = loadout.slot_range_for_equip_slot(feet_slot);
577
578 assert_eq!(None, result);
579 }
580
581 #[test]
582 fn test_get_slot_to_equip_into_second_bag_slot_free() {
583 let mut loadout = Loadout::new_empty();
584
585 loadout.swap(
586 EquipSlot::Armor(ArmorSlot::Bag1),
587 Some(get_test_bag(1)),
588 Time(0.0),
589 );
590
591 let result = loadout.get_slot_to_equip_into(&get_test_bag(1)).unwrap();
592
593 assert_eq!(EquipSlot::Armor(ArmorSlot::Bag2), result);
594 }
595
596 #[test]
597 fn test_get_slot_to_equip_into_no_bag_slots_free() {
598 let mut loadout = Loadout::new_empty();
599
600 loadout.swap(
601 EquipSlot::Armor(ArmorSlot::Bag1),
602 Some(get_test_bag(1)),
603 Time(0.0),
604 );
605 loadout.swap(
606 EquipSlot::Armor(ArmorSlot::Bag2),
607 Some(get_test_bag(1)),
608 Time(0.0),
609 );
610 loadout.swap(
611 EquipSlot::Armor(ArmorSlot::Bag3),
612 Some(get_test_bag(1)),
613 Time(0.0),
614 );
615 loadout.swap(
616 EquipSlot::Armor(ArmorSlot::Bag4),
617 Some(get_test_bag(1)),
618 Time(0.0),
619 );
620
621 let result = loadout.get_slot_to_equip_into(&get_test_bag(1)).unwrap();
622
623 assert_eq!(EquipSlot::Armor(ArmorSlot::Bag1), result);
624 }
625}