1use core::fmt::Debug;
13use core::hash::Hash;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25pub enum EquipSlot<Id: Copy + Eq + Hash + Debug> {
26 Weapon,
27 Head,
28 Body,
29 Accessory1,
30 Accessory2,
31 HeldItem,
32 Custom(Id),
33}
34
35impl<Id: Copy + Eq + Hash + Debug> EquipSlot<Id> {
36 pub fn standard() -> &'static [Self] {
39 use EquipSlot::*;
40 &[Weapon, Head, Body, Accessory1, Accessory2, HeldItem]
41 }
42
43 pub fn label(&self) -> &str {
45 match self {
46 EquipSlot::Weapon => "Weapon",
47 EquipSlot::Head => "Head",
48 EquipSlot::Body => "Body",
49 EquipSlot::Accessory1 => "Accessory 1",
50 EquipSlot::Accessory2 => "Accessory 2",
51 EquipSlot::HeldItem => "Held Item",
52 EquipSlot::Custom(_) => "Custom",
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum EquipError {
62 SlotFull,
64 InvalidSlot,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct EquipmentSlots<I: Copy + Eq + Hash + Debug, S: Copy + Eq + Hash + Debug> {
80 slots: Vec<(S, Option<I>)>,
81}
82
83impl<I: Copy + Eq + Hash + Debug, S: Copy + Eq + Hash + Debug> EquipmentSlots<I, S> {
86 pub fn new(slots: &[S]) -> Self
90 where
91 S: Clone,
92 {
93 Self {
94 slots: slots.iter().map(|s| (s.clone(), None)).collect(),
95 }
96 }
97
98 pub fn from_pairs(pairs: Vec<(S, I)>) -> Self {
103 let mut slots: Vec<(S, Option<I>)> = Vec::with_capacity(pairs.len());
104 for (slot, item) in pairs {
105 if let Some(existing) = slots.iter_mut().find(|(s, _)| *s == slot) {
106 existing.1 = Some(item);
107 } else {
108 slots.push((slot, Some(item)));
109 }
110 }
111 Self { slots }
112 }
113}
114
115impl<I: Copy + Eq + Hash + Debug, S: Copy + Eq + Hash + Debug> EquipmentSlots<I, S> {
118 pub fn equipped_in(&self, slot: &S) -> Option<&I> {
121 self.slots
122 .iter()
123 .find(|(s, _)| s == slot)
124 .and_then(|(_, item)| item.as_ref())
125 }
126
127 pub fn all_equipped(&self) -> Vec<(S, I)>
129 where
130 S: Clone,
131 I: Clone,
132 {
133 self.slots
134 .iter()
135 .filter_map(|(s, item)| item.as_ref().map(|i| (s.clone(), i.clone())))
136 .collect()
137 }
138
139 pub fn is_equipped(&self, item: &I) -> bool {
141 self.slots
142 .iter()
143 .any(|(_, slot_item)| matches!(slot_item, Some(i) if i == item))
144 }
145
146 pub fn slot_count(&self) -> usize {
148 self.slots.len()
149 }
150
151 pub fn iter(&self) -> impl Iterator<Item = &(S, Option<I>)> {
153 self.slots.iter()
154 }
155}
156
157impl<I: Copy + Eq + Hash + Debug, S: Copy + Eq + Hash + Debug> EquipmentSlots<I, S> {
160 pub fn equip(&mut self, slot: S, item: I) -> Result<(), EquipError> {
167 for (s, current) in self.slots.iter_mut() {
168 if *s == slot {
169 if current.is_some() {
170 return Err(EquipError::SlotFull);
171 }
172 *current = Some(item);
173 return Ok(());
174 }
175 }
176 Err(EquipError::InvalidSlot)
177 }
178
179 pub fn unequip(&mut self, slot: &S) -> Option<I> {
182 self.slots
183 .iter_mut()
184 .find(|(s, _)| s == slot)
185 .and_then(|(_, item)| item.take())
186 }
187
188 pub fn swap(&mut self, a: &S, b: &S) -> Result<(), EquipError> {
194 let a_pos = self.slots.iter().position(|(s, _)| s == a);
195 let b_pos = self.slots.iter().position(|(s, _)| s == b);
196
197 match (a_pos, b_pos) {
198 (Some(ai), Some(bi)) => {
199 let a_item = self.slots[ai].1.take();
200 let b_item = self.slots[bi].1.take();
201 self.slots[ai].1 = b_item;
202 self.slots[bi].1 = a_item;
203 Ok(())
204 }
205 (None, _) | (_, None) => Err(EquipError::InvalidSlot),
206 }
207 }
208
209 pub fn clear(&mut self) -> Vec<I> {
213 let mut items = Vec::new();
214 for (_, slot_item) in self.slots.iter_mut() {
215 if let Some(item) = slot_item.take() {
216 items.push(item);
217 }
218 }
219 items
220 }
221}
222
223pub trait EquipProvider: super::ItemProvider {
238 type CustomSlot: Copy + Eq + Hash + Debug;
241 type Stat: Copy;
244
245 fn equip_slots(&self, item: &Self::Item) -> Vec<EquipSlot<Self::CustomSlot>>;
248
249 fn stat_bonuses(&self, item: &Self::Item) -> &[(Self::Stat, i16)] {
253 let _ = item;
254 &[]
255 }
256}
257
258#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
266 enum TestItem {
267 IronSword,
268 SteelHelm,
269 LeatherArmor,
270 RingOfPower,
271 Potion,
272 }
273
274 type Slot = EquipSlot<&'static str>;
275
276 #[test]
279 fn standard_returns_six_slots() {
280 let slots = EquipSlot::<&str>::standard();
281 assert_eq!(slots.len(), 6);
282 assert!(slots.contains(&EquipSlot::<&str>::Weapon));
283 assert!(slots.contains(&EquipSlot::<&str>::Head));
284 assert!(slots.contains(&EquipSlot::<&str>::Body));
285 assert!(slots.contains(&EquipSlot::<&str>::Accessory1));
286 assert!(slots.contains(&EquipSlot::<&str>::Accessory2));
287 assert!(slots.contains(&EquipSlot::<&str>::HeldItem));
288 }
289
290 #[test]
291 fn standard_excludes_custom() {
292 let slots = EquipSlot::<&str>::standard();
293 assert!(!slots.contains(&EquipSlot::<&str>::Custom("ring")));
294 }
295
296 #[test]
297 fn label_returns_human_readable_name() {
298 assert_eq!(EquipSlot::<&str>::Weapon.label(), "Weapon");
299 assert_eq!(EquipSlot::<&str>::Head.label(), "Head");
300 assert_eq!(EquipSlot::<&str>::Body.label(), "Body");
301 assert_eq!(EquipSlot::<&str>::Accessory1.label(), "Accessory 1");
302 assert_eq!(EquipSlot::<&str>::Accessory2.label(), "Accessory 2");
303 assert_eq!(EquipSlot::<&str>::HeldItem.label(), "Held Item");
304 assert_eq!(EquipSlot::<&str>::Custom("ring").label(), "Custom");
305 }
306
307 #[test]
310 fn new_creates_empty_slots() {
311 let slots: EquipmentSlots<TestItem, Slot> =
312 EquipmentSlots::new(EquipSlot::<&str>::standard());
313 assert_eq!(slots.slot_count(), 6);
314 for slot in EquipSlot::<&str>::standard() {
315 assert!(slots.equipped_in(slot).is_none());
316 }
317 }
318
319 #[test]
320 fn new_empty_slice_creates_no_slots() {
321 let slots: EquipmentSlots<TestItem, Slot> = EquipmentSlots::new(&[]);
322 assert_eq!(slots.slot_count(), 0);
323 }
324
325 #[test]
326 fn from_pairs_initializes_with_items() {
327 let slots: EquipmentSlots<TestItem, Slot> = EquipmentSlots::from_pairs(vec![
328 (EquipSlot::Weapon, TestItem::IronSword),
329 (EquipSlot::Head, TestItem::SteelHelm),
330 ]);
331 assert_eq!(slots.slot_count(), 2);
332 assert_eq!(
333 slots.equipped_in(&EquipSlot::Weapon),
334 Some(&TestItem::IronSword)
335 );
336 assert_eq!(
337 slots.equipped_in(&EquipSlot::Head),
338 Some(&TestItem::SteelHelm)
339 );
340 }
341
342 #[test]
343 fn from_pairs_deduplicates_slots() {
344 let slots: EquipmentSlots<TestItem, Slot> = EquipmentSlots::from_pairs(vec![
345 (EquipSlot::Weapon, TestItem::IronSword),
346 (EquipSlot::Weapon, TestItem::Potion), ]);
348 assert_eq!(slots.slot_count(), 1);
349 assert_eq!(
350 slots.equipped_in(&EquipSlot::Weapon),
351 Some(&TestItem::Potion)
352 );
353 }
354
355 #[test]
358 fn equip_succeeds_on_empty_slot() {
359 let mut slots: EquipmentSlots<TestItem, Slot> =
360 EquipmentSlots::new(EquipSlot::<&str>::standard());
361 assert_eq!(slots.equip(EquipSlot::Weapon, TestItem::IronSword), Ok(()));
362 assert_eq!(
363 slots.equipped_in(&EquipSlot::Weapon),
364 Some(&TestItem::IronSword)
365 );
366 }
367
368 #[test]
369 fn equip_into_occupied_slot_fails_slot_full() {
370 let mut slots: EquipmentSlots<TestItem, Slot> =
371 EquipmentSlots::new(EquipSlot::<&str>::standard());
372 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
373 let result = slots.equip(EquipSlot::Weapon, TestItem::SteelHelm);
374 assert_eq!(result, Err(EquipError::SlotFull));
375 assert_eq!(
377 slots.equipped_in(&EquipSlot::Weapon),
378 Some(&TestItem::IronSword)
379 );
380 }
381
382 #[test]
383 fn equip_invalid_slot_fails_invalid_slot() {
384 let mut slots: EquipmentSlots<TestItem, Slot> =
385 EquipmentSlots::new(EquipSlot::<&str>::standard());
386 let custom_slot = EquipSlot::Custom("ring");
387 let result = slots.equip(custom_slot, TestItem::RingOfPower);
388 assert_eq!(result, Err(EquipError::InvalidSlot));
389 }
390
391 #[test]
394 fn unequip_returns_item() {
395 let mut slots: EquipmentSlots<TestItem, Slot> =
396 EquipmentSlots::new(EquipSlot::<&str>::standard());
397 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
398 let item = slots.unequip(&EquipSlot::Weapon);
399 assert_eq!(item, Some(TestItem::IronSword));
400 assert!(slots.equipped_in(&EquipSlot::Weapon).is_none());
401 }
402
403 #[test]
404 fn unequip_empty_slot_returns_none() {
405 let mut slots: EquipmentSlots<TestItem, Slot> =
406 EquipmentSlots::new(EquipSlot::<&str>::standard());
407 assert!(slots.unequip(&EquipSlot::Weapon).is_none());
408 }
409
410 #[test]
411 fn unequip_invalid_slot_returns_none() {
412 let mut slots: EquipmentSlots<TestItem, Slot> =
413 EquipmentSlots::new(EquipSlot::<&str>::standard());
414 assert!(slots.unequip(&EquipSlot::Custom("ring")).is_none());
415 }
416
417 #[test]
420 fn is_equipped_returns_true_when_item_is_equipped() {
421 let mut slots: EquipmentSlots<TestItem, Slot> =
422 EquipmentSlots::new(EquipSlot::<&str>::standard());
423 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
424 assert!(slots.is_equipped(&TestItem::IronSword));
425 }
426
427 #[test]
428 fn is_equipped_returns_false_when_item_not_equipped() {
429 let slots: EquipmentSlots<TestItem, Slot> =
430 EquipmentSlots::new(EquipSlot::<&str>::standard());
431 assert!(!slots.is_equipped(&TestItem::IronSword));
432 }
433
434 #[test]
435 fn is_equipped_returns_false_after_unequip() {
436 let mut slots: EquipmentSlots<TestItem, Slot> =
437 EquipmentSlots::new(EquipSlot::<&str>::standard());
438 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
439 slots.unequip(&EquipSlot::Weapon);
440 assert!(!slots.is_equipped(&TestItem::IronSword));
441 }
442
443 #[test]
446 fn swap_two_occupied_slots() {
447 let mut slots: EquipmentSlots<TestItem, Slot> =
448 EquipmentSlots::new(EquipSlot::<&str>::standard());
449 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
450 slots.equip(EquipSlot::Head, TestItem::SteelHelm).unwrap();
451 slots.swap(&EquipSlot::Weapon, &EquipSlot::Head).unwrap();
452 assert_eq!(
453 slots.equipped_in(&EquipSlot::Weapon),
454 Some(&TestItem::SteelHelm)
455 );
456 assert_eq!(
457 slots.equipped_in(&EquipSlot::Head),
458 Some(&TestItem::IronSword)
459 );
460 }
461
462 #[test]
463 fn swap_occupied_with_empty() {
464 let mut slots: EquipmentSlots<TestItem, Slot> =
465 EquipmentSlots::new(EquipSlot::<&str>::standard());
466 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
467 slots.swap(&EquipSlot::Weapon, &EquipSlot::Head).unwrap();
468 assert!(slots.equipped_in(&EquipSlot::Weapon).is_none());
469 assert_eq!(
470 slots.equipped_in(&EquipSlot::Head),
471 Some(&TestItem::IronSword)
472 );
473 }
474
475 #[test]
476 fn swap_invalid_slot_fails() {
477 let mut slots: EquipmentSlots<TestItem, Slot> =
478 EquipmentSlots::new(EquipSlot::<&str>::standard());
479 let result = slots.swap(&EquipSlot::Weapon, &EquipSlot::Custom("ring"));
480 assert_eq!(result, Err(EquipError::InvalidSlot));
481 }
482
483 #[test]
486 fn clear_returns_all_items() {
487 let mut slots: EquipmentSlots<TestItem, Slot> =
488 EquipmentSlots::new(EquipSlot::<&str>::standard());
489 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
490 slots.equip(EquipSlot::Head, TestItem::SteelHelm).unwrap();
491 slots
492 .equip(EquipSlot::Body, TestItem::LeatherArmor)
493 .unwrap();
494
495 let mut items = slots.clear();
496 items.sort_by_key(|i| format!("{:?}", i));
497 assert_eq!(items.len(), 3);
498 assert!(items.contains(&TestItem::IronSword));
499 assert!(items.contains(&TestItem::SteelHelm));
500 assert!(items.contains(&TestItem::LeatherArmor));
501
502 assert!(slots.equipped_in(&EquipSlot::Weapon).is_none());
504 assert!(slots.equipped_in(&EquipSlot::Head).is_none());
505 assert!(slots.equipped_in(&EquipSlot::Body).is_none());
506 }
507
508 #[test]
509 fn clear_on_empty_slots_returns_empty_vec() {
510 let mut slots: EquipmentSlots<TestItem, Slot> =
511 EquipmentSlots::new(EquipSlot::<&str>::standard());
512 let items = slots.clear();
513 assert!(items.is_empty());
514 assert_eq!(slots.slot_count(), 6);
515 }
516
517 #[test]
520 fn all_equipped_returns_only_occupied_slots() {
521 let mut slots: EquipmentSlots<TestItem, Slot> =
522 EquipmentSlots::new(EquipSlot::<&str>::standard());
523 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
524 slots.equip(EquipSlot::Head, TestItem::SteelHelm).unwrap();
525 let equipped = slots.all_equipped();
528 assert_eq!(equipped.len(), 2);
529 assert!(equipped.contains(&(EquipSlot::Weapon, TestItem::IronSword)));
530 assert!(equipped.contains(&(EquipSlot::Head, TestItem::SteelHelm)));
531 }
532
533 #[test]
534 fn all_equipped_returns_empty_when_nothing_equipped() {
535 let slots: EquipmentSlots<TestItem, Slot> =
536 EquipmentSlots::new(EquipSlot::<&str>::standard());
537 assert!(slots.all_equipped().is_empty());
538 }
539
540 #[test]
543 fn slot_count_returns_total_slots() {
544 let slots: EquipmentSlots<TestItem, Slot> =
545 EquipmentSlots::new(EquipSlot::<&str>::standard());
546 assert_eq!(slots.slot_count(), 6);
547 }
548
549 #[test]
550 fn slot_count_unchanged_by_equip_or_unequip() {
551 let mut slots: EquipmentSlots<TestItem, Slot> =
552 EquipmentSlots::new(EquipSlot::<&str>::standard());
553 assert_eq!(slots.slot_count(), 6);
554 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
555 assert_eq!(slots.slot_count(), 6);
556 slots.unequip(&EquipSlot::Weapon);
557 assert_eq!(slots.slot_count(), 6);
558 }
559
560 #[test]
563 fn iter_yields_all_slots() {
564 let mut slots: EquipmentSlots<TestItem, Slot> =
565 EquipmentSlots::new(EquipSlot::<&str>::standard());
566 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
567 let entries: Vec<_> = slots.iter().collect();
568 assert_eq!(entries.len(), 6);
569 let weapon_entry = entries
571 .iter()
572 .find(|(s, _)| *s == EquipSlot::Weapon)
573 .unwrap();
574 assert_eq!(weapon_entry.1, Some(TestItem::IronSword));
575 }
576
577 use crate::items::{ItemKind, ItemProvider, ItemResult};
580
581 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
583 enum TestStat {
584 Attack,
585 Defense,
586 }
587
588 struct TestGame;
589
590 impl ItemProvider for TestGame {
591 type Item = TestItem;
592 type Effect = ();
593 type Monster = ();
594 type CustomKind = ();
595
596 fn item_name(&self, _item: &TestItem) -> &str {
597 "X"
598 }
599 fn item_description(&self, _item: &TestItem) -> &str {
600 "X"
601 }
602 fn item_effect(&self, _item: &TestItem) {}
603 fn item_price(&self, _item: &TestItem) -> u32 {
604 0
605 }
606 fn can_use_outside_battle(&self, _item: &TestItem) -> bool {
607 false
608 }
609 fn can_use_in_battle(&self, _item: &TestItem) -> bool {
610 false
611 }
612 fn use_on_monster(&self, _item: &TestItem, _m: &mut ()) -> ItemResult {
613 ItemResult::NoEffect
614 }
615 fn consume(&self, _item: &TestItem) -> bool {
616 false
617 }
618 fn item_kind(&self, item: &TestItem) -> ItemKind<()> {
619 match item {
620 TestItem::Potion => ItemKind::Consumable,
621 _ => ItemKind::Equipment,
622 }
623 }
624 }
625
626 impl EquipProvider for TestGame {
627 type CustomSlot = &'static str;
628 type Stat = TestStat;
629
630 fn equip_slots(&self, item: &TestItem) -> Vec<Slot> {
631 match item {
632 TestItem::IronSword => vec![EquipSlot::Weapon],
633 TestItem::SteelHelm => vec![EquipSlot::Head],
634 TestItem::RingOfPower => vec![EquipSlot::Accessory1, EquipSlot::Accessory2],
635 _ => Vec::new(),
636 }
637 }
638
639 fn stat_bonuses(&self, item: &TestItem) -> &[(TestStat, i16)] {
640 match item {
641 TestItem::IronSword => &[(TestStat::Attack, 5)],
642 TestItem::SteelHelm => &[(TestStat::Defense, 3)],
643 TestItem::RingOfPower => &[(TestStat::Attack, 2), (TestStat::Defense, 2)],
644 _ => &[],
645 }
646 }
647 }
648
649 #[test]
650 fn equip_provider_returns_real_stat_bonuses() {
651 let game = TestGame;
652 assert_eq!(
653 game.stat_bonuses(&TestItem::IronSword),
654 &[(TestStat::Attack, 5)]
655 );
656 assert_eq!(
657 game.stat_bonuses(&TestItem::RingOfPower),
658 &[(TestStat::Attack, 2), (TestStat::Defense, 2)]
659 );
660 assert!(game.stat_bonuses(&TestItem::Potion).is_empty());
661 }
662
663 #[test]
664 fn equip_provider_slots_gate_equippability() {
665 let game = TestGame;
666 assert_eq!(game.equip_slots(&TestItem::IronSword), vec![Slot::Weapon]);
667 assert_eq!(
668 game.equip_slots(&TestItem::RingOfPower),
669 vec![Slot::Accessory1, Slot::Accessory2]
670 );
671 assert!(game.equip_slots(&TestItem::Potion).is_empty());
672 }
673}