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 && let Some((_unequip_time, count)) =
111 self.recently_unequipped_items.get_mut(&item_def_id)
112 {
113 *count = count.saturating_sub(1);
114 }
115 self.cull_recently_unequipped_items(time);
116 let unequipped_item = self
117 .slots
118 .iter_mut()
119 .find(|x| x.equip_slot == equip_slot)
120 .and_then(|x| core::mem::replace(&mut x.slot, item));
121 if let Some(unequipped_item) = unequipped_item.as_ref() {
122 let entry = self
123 .recently_unequipped_items
124 .entry_ref(&unequipped_item.item_definition_id())
125 .or_insert((time, 0));
126 *entry = (time, entry.1.saturating_add(1));
127 }
128 unequipped_item
129 }
130
131 pub(super) fn equipped(&self, equip_slot: EquipSlot) -> Option<&Item> {
133 self.slot(equip_slot).and_then(|x| x.slot.as_ref())
134 }
135
136 fn slot(&self, equip_slot: EquipSlot) -> Option<&LoadoutSlot> {
137 self.slots
138 .iter()
139 .find(|loadout_slot| loadout_slot.equip_slot == equip_slot)
140 }
141
142 pub(super) fn loadout_idx_for_equip_slot(&self, equip_slot: EquipSlot) -> Option<usize> {
143 self.slots
144 .iter()
145 .position(|loadout_slot| loadout_slot.equip_slot == equip_slot)
146 }
147
148 pub(super) fn items_with_persistence_key(&self) -> impl Iterator<Item = (&str, Option<&Item>)> {
150 self.slots
151 .iter()
152 .map(|x| (x.persistence_key.as_str(), x.slot.as_ref()))
153 }
154
155 pub fn set_item_at_slot_using_persistence_key(
158 &mut self,
159 persistence_key: &str,
160 item: Item,
161 ) -> Result<(), LoadoutError> {
162 if let Some(slot) = self
163 .slots
164 .iter_mut()
165 .find(|x| x.persistence_key == persistence_key)
166 {
167 slot.slot = Some(item);
168 Ok(())
169 } else {
170 Err(LoadoutError::InvalidPersistenceKey)
171 }
172 }
173
174 pub fn get_mut_item_at_slot_using_persistence_key(
175 &mut self,
176 persistence_key: &str,
177 ) -> Result<&mut Item, LoadoutError> {
178 self.slots
179 .iter_mut()
180 .find(|loadout_slot| loadout_slot.persistence_key == persistence_key)
181 .map_or(Err(LoadoutError::InvalidPersistenceKey), |loadout_slot| {
182 loadout_slot
183 .slot
184 .as_mut()
185 .ok_or(LoadoutError::NoParentAtSlot)
186 })
187 }
188
189 pub(super) fn swap_slots(
191 &mut self,
192 equip_slot_a: EquipSlot,
193 equip_slot_b: EquipSlot,
194 time: Time,
195 ) {
196 if self.slot(equip_slot_b).is_none() || self.slot(equip_slot_b).is_none() {
197 warn!("Cannot swap slots for non-existent equip slot");
201 return;
202 }
203
204 let item_a = self.swap(equip_slot_a, None, time);
205 let item_b = self.swap(equip_slot_b, item_a, time);
206 assert_eq!(self.swap(equip_slot_a, item_b, time), None);
207
208 if !self.slot_can_hold(
210 equip_slot_a,
211 self.equipped(equip_slot_a).map(|x| x.kind()).as_deref(),
212 ) || !self.slot_can_hold(
213 equip_slot_b,
214 self.equipped(equip_slot_b).map(|x| x.kind()).as_deref(),
215 ) {
216 let item_a = self.swap(equip_slot_a, None, time);
218 let item_b = self.swap(equip_slot_b, item_a, time);
219 assert_eq!(self.swap(equip_slot_a, item_b, time), None);
220 }
221 }
222
223 pub(super) fn get_slot_to_equip_into(&self, item: &Item) -> Option<EquipSlot> {
228 let mut suitable_slots = self
229 .slots
230 .iter()
231 .filter(|s| self.slot_can_hold(s.equip_slot, Some(&*item.kind())));
232
233 let first_suitable = suitable_slots.next();
234
235 let mut differing_suitable_slots =
236 first_suitable
237 .into_iter()
238 .chain(suitable_slots)
239 .filter(|loadout_slot| {
240 loadout_slot.slot.as_ref().is_none_or(|equipped_item| {
241 equipped_item.item_definition_id() != item.item_definition_id()
242 || equipped_item.durability_lost() != item.durability_lost()
243 })
244 });
245
246 let first_differing = differing_suitable_slots.next();
247
248 first_differing
249 .into_iter()
250 .chain(differing_suitable_slots)
251 .find(|loadout_slot| loadout_slot.slot.is_none())
252 .map(|x| x.equip_slot)
253 .or_else(|| first_differing.map(|x| x.equip_slot))
254 .or_else(|| first_suitable.map(|x| x.equip_slot))
255 }
256
257 pub(super) fn equipped_items_replaceable_by<'a>(
260 &'a self,
261 item_kind: &'a ItemKind,
262 ) -> impl Iterator<Item = &'a Item> {
263 self.slots
264 .iter()
265 .filter(move |s| self.slot_can_hold(s.equip_slot, Some(item_kind)))
266 .filter_map(|s| s.slot.as_ref())
267 }
268
269 pub(super) fn inv_slot(&self, loadout_slot_id: LoadoutSlotId) -> Option<&InvSlot> {
271 self.slots
272 .get(loadout_slot_id.loadout_idx)
273 .and_then(|loadout_slot| loadout_slot.slot.as_ref())
274 .and_then(|item| item.slot(loadout_slot_id.slot_idx))
275 }
276
277 pub(super) fn inv_slot_mut(&mut self, loadout_slot_id: LoadoutSlotId) -> Option<&mut InvSlot> {
279 self.slots
280 .get_mut(loadout_slot_id.loadout_idx)
281 .and_then(|loadout_slot| loadout_slot.slot.as_mut())
282 .and_then(|item| item.slot_mut(loadout_slot_id.slot_idx))
283 }
284
285 pub(super) fn inv_slots_with_id(&self) -> impl Iterator<Item = (LoadoutSlotId, &InvSlot)> {
288 self.slots
289 .iter()
290 .enumerate()
291 .filter_map(|(i, loadout_slot)| {
292 loadout_slot.slot.as_ref().map(|item| (i, item.slots()))
293 })
294 .flat_map(|(loadout_slot_index, loadout_slots)| {
295 loadout_slots
296 .iter()
297 .enumerate()
298 .map(move |(item_slot_index, inv_slot)| {
299 (
300 LoadoutSlotId {
301 loadout_idx: loadout_slot_index,
302 slot_idx: item_slot_index,
303 },
304 inv_slot,
305 )
306 })
307 })
308 }
309
310 pub(super) fn inv_slots_mut(&mut self) -> impl Iterator<Item = &mut InvSlot> {
312 self.slots.iter_mut()
313 .filter_map(|x| x.slot.as_mut().map(|item| item.slots_mut())) .flat_map(|loadout_slots| loadout_slots.iter_mut()) }
316
317 pub(super) fn inv_slots_mut_with_mutable_recently_unequipped_items(
318 &mut self,
319 ) -> (
320 impl Iterator<Item = &mut InvSlot>,
321 &mut HashMap<ItemDefinitionIdOwned, (Time, u8)>,
322 ) {
323 let slots_mut = self.slots.iter_mut()
324 .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)
327 }
328
329 pub(super) fn slot_range_for_equip_slot(&self, equip_slot: EquipSlot) -> Option<Range<usize>> {
332 self.slots
333 .iter()
334 .map(|loadout_slot| {
335 (
336 loadout_slot.equip_slot,
337 loadout_slot
338 .slot
339 .as_ref()
340 .map_or(0, |item| item.slots().len()),
341 )
342 })
343 .scan(0, |acc_len, (equip_slot, len)| {
344 let res = Some((equip_slot, len, *acc_len));
345 *acc_len += len;
346 res
347 })
348 .find(|(e, len, _)| *e == equip_slot && len > &0)
349 .map(|(_, slot_len, start)| start..start + slot_len)
350 }
351
352 #[must_use = "Returned item will be lost if not used"]
355 pub(super) fn try_equip(&mut self, item: Item) -> Result<(), Item> {
356 let loadout_slot = self
357 .slots
358 .iter()
359 .find(|s| s.slot.is_none() && self.slot_can_hold(s.equip_slot, Some(&*item.kind())))
360 .map(|s| s.equip_slot);
361 if let Some(slot) = self
362 .slots
363 .iter_mut()
364 .find(|s| Some(s.equip_slot) == loadout_slot)
365 {
366 slot.slot = Some(item);
367 Ok(())
368 } else {
369 Err(item)
370 }
371 }
372
373 pub(super) fn items(&self) -> impl Iterator<Item = &Item> {
374 self.slots.iter().filter_map(|x| x.slot.as_ref())
375 }
376
377 pub(super) fn items_with_slot(&self) -> impl Iterator<Item = (EquipSlot, &Item)> {
378 self.slots
379 .iter()
380 .filter_map(|x| x.slot.as_ref().map(|i| (x.equip_slot, i)))
381 }
382
383 pub(super) fn slot_can_hold(
385 &self,
386 equip_slot: EquipSlot,
387 item_kind: Option<&ItemKind>,
388 ) -> bool {
389 if !(match equip_slot {
392 EquipSlot::ActiveMainhand => Loadout::is_valid_weapon_pair(
393 item_kind,
394 self.equipped(EquipSlot::ActiveOffhand)
395 .map(|x| x.kind())
396 .as_deref(),
397 ),
398 EquipSlot::ActiveOffhand => Loadout::is_valid_weapon_pair(
399 self.equipped(EquipSlot::ActiveMainhand)
400 .map(|x| x.kind())
401 .as_deref(),
402 item_kind,
403 ),
404 EquipSlot::InactiveMainhand => Loadout::is_valid_weapon_pair(
405 item_kind,
406 self.equipped(EquipSlot::InactiveOffhand)
407 .map(|x| x.kind())
408 .as_deref(),
409 ),
410 EquipSlot::InactiveOffhand => Loadout::is_valid_weapon_pair(
411 self.equipped(EquipSlot::InactiveMainhand)
412 .map(|x| x.kind())
413 .as_deref(),
414 item_kind,
415 ),
416 _ => true,
417 }) {
418 return false;
419 }
420
421 item_kind.is_none_or(|item| equip_slot.can_hold(item))
422 }
423
424 #[rustfmt::skip]
425 fn is_valid_weapon_pair(main_hand: Option<&ItemKind>, off_hand: Option<&ItemKind>) -> bool {
426 matches!((main_hand, off_hand),
427 (Some(ItemKind::Tool(Tool { hands: Hands::One, .. })), None) |
428 (Some(ItemKind::Tool(Tool { hands: Hands::Two, .. })), None) |
429 (Some(ItemKind::Tool(Tool { hands: Hands::One, .. })), Some(ItemKind::Tool(Tool { hands: Hands::One, .. }))) |
430 (None, None))
431 }
432
433 pub(super) fn swap_equipped_weapons(&mut self, time: Time) {
434 let valid_slot = |equip_slot| {
437 self.equipped(equip_slot)
438 .is_none_or(|i| self.slot_can_hold(equip_slot, Some(&*i.kind())))
439 };
440
441 if valid_slot(EquipSlot::ActiveMainhand)
446 && valid_slot(EquipSlot::ActiveOffhand)
447 && valid_slot(EquipSlot::InactiveMainhand)
448 && valid_slot(EquipSlot::InactiveOffhand)
449 {
450 let active_mainhand = self.swap(EquipSlot::ActiveMainhand, None, time);
452 let active_offhand = self.swap(EquipSlot::ActiveOffhand, None, time);
453 let inactive_mainhand = self.swap(EquipSlot::InactiveMainhand, None, time);
454 let inactive_offhand = self.swap(EquipSlot::InactiveOffhand, None, time);
455 assert!(
457 self.swap(EquipSlot::ActiveMainhand, inactive_mainhand, time)
458 .is_none()
459 );
460 assert!(
461 self.swap(EquipSlot::ActiveOffhand, inactive_offhand, time)
462 .is_none()
463 );
464 assert!(
465 self.swap(EquipSlot::InactiveMainhand, active_mainhand, time)
466 .is_none()
467 );
468 assert!(
469 self.swap(EquipSlot::InactiveOffhand, active_offhand, time)
470 .is_none()
471 );
472 }
473 }
474
475 pub fn persistence_update_all_item_states(
478 &mut self,
479 ability_map: &item::tool::AbilityMap,
480 msm: &item::MaterialStatManifest,
481 ) {
482 self.slots.iter_mut().for_each(|slot| {
483 if let Some(item) = &mut slot.slot {
484 item.update_item_state(ability_map, msm);
485 }
486 });
487 }
488
489 pub(super) fn damage_items(
491 &mut self,
492 ability_map: &item::tool::AbilityMap,
493 msm: &item::MaterialStatManifest,
494 ) {
495 self.slots
496 .iter_mut()
497 .filter_map(|slot| slot.slot.as_mut())
498 .filter(|item| item.has_durability())
499 .for_each(|item| item.increment_damage(ability_map, msm));
500 }
501
502 pub(super) fn repair_item_at_slot(
504 &mut self,
505 equip_slot: EquipSlot,
506 ability_map: &item::tool::AbilityMap,
507 msm: &item::MaterialStatManifest,
508 ) {
509 if let Some(item) = self
510 .slots
511 .iter_mut()
512 .find(|slot| slot.equip_slot == equip_slot)
513 .and_then(|slot| slot.slot.as_mut())
514 {
515 item.reset_durability(ability_map, msm);
516 }
517 }
518
519 pub(super) fn cull_recently_unequipped_items(&mut self, time: Time) {
520 self.recently_unequipped_items
521 .retain(|_def, (unequip_time, count)| {
522 if time.0 < unequip_time.0 {
525 *unequip_time = time;
526 }
527
528 (time.0 - unequip_time.0 < UNEQUIP_TRACKING_DURATION) && *count > 0
529 });
530 }
531}
532
533#[cfg(test)]
534mod tests {
535 use crate::{
536 comp::{
537 Item,
538 inventory::{
539 loadout::Loadout,
540 slot::{ArmorSlot, EquipSlot},
541 test_helpers::get_test_bag,
542 },
543 },
544 resources::Time,
545 };
546
547 #[test]
548 fn test_slot_range_for_equip_slot() {
549 let mut loadout = Loadout::new_empty();
550
551 let bag1_slot = EquipSlot::Armor(ArmorSlot::Bag1);
552 let bag = get_test_bag(18);
553 loadout.swap(bag1_slot, Some(bag), Time(0.0));
554
555 let result = loadout.slot_range_for_equip_slot(bag1_slot).unwrap();
556
557 assert_eq!(0..18, result);
558 }
559
560 #[test]
561 fn test_slot_range_for_equip_slot_no_item() {
562 let loadout = Loadout::new_empty();
563 let result = loadout.slot_range_for_equip_slot(EquipSlot::Armor(ArmorSlot::Bag1));
564
565 assert_eq!(None, result);
566 }
567
568 #[test]
569 fn test_slot_range_for_equip_slot_item_without_slots() {
570 let mut loadout = Loadout::new_empty();
571
572 let feet_slot = EquipSlot::Armor(ArmorSlot::Feet);
573 let boots = Item::new_from_asset_expect("common.items.testing.test_boots");
574 loadout.swap(feet_slot, Some(boots), Time(0.0));
575 let result = loadout.slot_range_for_equip_slot(feet_slot);
576
577 assert_eq!(None, result);
578 }
579
580 #[test]
581 fn test_get_slot_to_equip_into_second_bag_slot_free() {
582 let mut loadout = Loadout::new_empty();
583
584 loadout.swap(
585 EquipSlot::Armor(ArmorSlot::Bag1),
586 Some(get_test_bag(1)),
587 Time(0.0),
588 );
589
590 let result = loadout.get_slot_to_equip_into(&get_test_bag(1)).unwrap();
591
592 assert_eq!(EquipSlot::Armor(ArmorSlot::Bag2), result);
593 }
594
595 #[test]
596 fn test_get_slot_to_equip_into_no_bag_slots_free() {
597 let mut loadout = Loadout::new_empty();
598
599 loadout.swap(
600 EquipSlot::Armor(ArmorSlot::Bag1),
601 Some(get_test_bag(1)),
602 Time(0.0),
603 );
604 loadout.swap(
605 EquipSlot::Armor(ArmorSlot::Bag2),
606 Some(get_test_bag(1)),
607 Time(0.0),
608 );
609 loadout.swap(
610 EquipSlot::Armor(ArmorSlot::Bag3),
611 Some(get_test_bag(1)),
612 Time(0.0),
613 );
614 loadout.swap(
615 EquipSlot::Armor(ArmorSlot::Bag4),
616 Some(get_test_bag(1)),
617 Time(0.0),
618 );
619
620 let result = loadout.get_slot_to_equip_into(&get_test_bag(1)).unwrap();
621
622 assert_eq!(EquipSlot::Armor(ArmorSlot::Bag1), result);
623 }
624}