1use std::fmt::Debug;
13use std::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!(
305 EquipSlot::<&str>::Custom("ring").label(),
306 "Custom"
307 );
308 }
309
310 #[test]
313 fn new_creates_empty_slots() {
314 let slots: EquipmentSlots<TestItem, Slot> =
315 EquipmentSlots::new(EquipSlot::<&str>::standard());
316 assert_eq!(slots.slot_count(), 6);
317 for slot in EquipSlot::<&str>::standard() {
318 assert!(slots.equipped_in(slot).is_none());
319 }
320 }
321
322 #[test]
323 fn new_empty_slice_creates_no_slots() {
324 let slots: EquipmentSlots<TestItem, Slot> = EquipmentSlots::new(&[]);
325 assert_eq!(slots.slot_count(), 0);
326 }
327
328 #[test]
329 fn from_pairs_initializes_with_items() {
330 let slots: EquipmentSlots<TestItem, Slot> = EquipmentSlots::from_pairs(vec![
331 (EquipSlot::Weapon, TestItem::IronSword),
332 (EquipSlot::Head, TestItem::SteelHelm),
333 ]);
334 assert_eq!(slots.slot_count(), 2);
335 assert_eq!(
336 slots.equipped_in(&EquipSlot::Weapon),
337 Some(&TestItem::IronSword)
338 );
339 assert_eq!(
340 slots.equipped_in(&EquipSlot::Head),
341 Some(&TestItem::SteelHelm)
342 );
343 }
344
345 #[test]
346 fn from_pairs_deduplicates_slots() {
347 let slots: EquipmentSlots<TestItem, Slot> = EquipmentSlots::from_pairs(vec![
348 (EquipSlot::Weapon, TestItem::IronSword),
349 (EquipSlot::Weapon, TestItem::Potion), ]);
351 assert_eq!(slots.slot_count(), 1);
352 assert_eq!(
353 slots.equipped_in(&EquipSlot::Weapon),
354 Some(&TestItem::Potion)
355 );
356 }
357
358 #[test]
361 fn equip_succeeds_on_empty_slot() {
362 let mut slots: EquipmentSlots<TestItem, Slot> =
363 EquipmentSlots::new(EquipSlot::<&str>::standard());
364 assert_eq!(slots.equip(EquipSlot::Weapon, TestItem::IronSword), Ok(()));
365 assert_eq!(
366 slots.equipped_in(&EquipSlot::Weapon),
367 Some(&TestItem::IronSword)
368 );
369 }
370
371 #[test]
372 fn equip_into_occupied_slot_fails_slot_full() {
373 let mut slots: EquipmentSlots<TestItem, Slot> =
374 EquipmentSlots::new(EquipSlot::<&str>::standard());
375 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
376 let result = slots.equip(EquipSlot::Weapon, TestItem::SteelHelm);
377 assert_eq!(result, Err(EquipError::SlotFull));
378 assert_eq!(
380 slots.equipped_in(&EquipSlot::Weapon),
381 Some(&TestItem::IronSword)
382 );
383 }
384
385 #[test]
386 fn equip_invalid_slot_fails_invalid_slot() {
387 let mut slots: EquipmentSlots<TestItem, Slot> =
388 EquipmentSlots::new(EquipSlot::<&str>::standard());
389 let custom_slot = EquipSlot::Custom("ring");
390 let result = slots.equip(custom_slot, TestItem::RingOfPower);
391 assert_eq!(result, Err(EquipError::InvalidSlot));
392 }
393
394 #[test]
397 fn unequip_returns_item() {
398 let mut slots: EquipmentSlots<TestItem, Slot> =
399 EquipmentSlots::new(EquipSlot::<&str>::standard());
400 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
401 let item = slots.unequip(&EquipSlot::Weapon);
402 assert_eq!(item, Some(TestItem::IronSword));
403 assert!(slots.equipped_in(&EquipSlot::Weapon).is_none());
404 }
405
406 #[test]
407 fn unequip_empty_slot_returns_none() {
408 let mut slots: EquipmentSlots<TestItem, Slot> =
409 EquipmentSlots::new(EquipSlot::<&str>::standard());
410 assert!(slots.unequip(&EquipSlot::Weapon).is_none());
411 }
412
413 #[test]
414 fn unequip_invalid_slot_returns_none() {
415 let mut slots: EquipmentSlots<TestItem, Slot> =
416 EquipmentSlots::new(EquipSlot::<&str>::standard());
417 assert!(slots.unequip(&EquipSlot::Custom("ring")).is_none());
418 }
419
420 #[test]
423 fn is_equipped_returns_true_when_item_is_equipped() {
424 let mut slots: EquipmentSlots<TestItem, Slot> =
425 EquipmentSlots::new(EquipSlot::<&str>::standard());
426 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
427 assert!(slots.is_equipped(&TestItem::IronSword));
428 }
429
430 #[test]
431 fn is_equipped_returns_false_when_item_not_equipped() {
432 let slots: EquipmentSlots<TestItem, Slot> =
433 EquipmentSlots::new(EquipSlot::<&str>::standard());
434 assert!(!slots.is_equipped(&TestItem::IronSword));
435 }
436
437 #[test]
438 fn is_equipped_returns_false_after_unequip() {
439 let mut slots: EquipmentSlots<TestItem, Slot> =
440 EquipmentSlots::new(EquipSlot::<&str>::standard());
441 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
442 slots.unequip(&EquipSlot::Weapon);
443 assert!(!slots.is_equipped(&TestItem::IronSword));
444 }
445
446 #[test]
449 fn swap_two_occupied_slots() {
450 let mut slots: EquipmentSlots<TestItem, Slot> =
451 EquipmentSlots::new(EquipSlot::<&str>::standard());
452 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
453 slots.equip(EquipSlot::Head, TestItem::SteelHelm).unwrap();
454 slots.swap(&EquipSlot::Weapon, &EquipSlot::Head).unwrap();
455 assert_eq!(
456 slots.equipped_in(&EquipSlot::Weapon),
457 Some(&TestItem::SteelHelm)
458 );
459 assert_eq!(
460 slots.equipped_in(&EquipSlot::Head),
461 Some(&TestItem::IronSword)
462 );
463 }
464
465 #[test]
466 fn swap_occupied_with_empty() {
467 let mut slots: EquipmentSlots<TestItem, Slot> =
468 EquipmentSlots::new(EquipSlot::<&str>::standard());
469 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
470 slots.swap(&EquipSlot::Weapon, &EquipSlot::Head).unwrap();
471 assert!(slots.equipped_in(&EquipSlot::Weapon).is_none());
472 assert_eq!(
473 slots.equipped_in(&EquipSlot::Head),
474 Some(&TestItem::IronSword)
475 );
476 }
477
478 #[test]
479 fn swap_invalid_slot_fails() {
480 let mut slots: EquipmentSlots<TestItem, Slot> =
481 EquipmentSlots::new(EquipSlot::<&str>::standard());
482 let result = slots.swap(&EquipSlot::Weapon, &EquipSlot::Custom("ring"));
483 assert_eq!(result, Err(EquipError::InvalidSlot));
484 }
485
486 #[test]
489 fn clear_returns_all_items() {
490 let mut slots: EquipmentSlots<TestItem, Slot> =
491 EquipmentSlots::new(EquipSlot::<&str>::standard());
492 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
493 slots.equip(EquipSlot::Head, TestItem::SteelHelm).unwrap();
494 slots.equip(EquipSlot::Body, TestItem::LeatherArmor).unwrap();
495
496 let mut items = slots.clear();
497 items.sort_by_key(|i| format!("{:?}", i));
498 assert_eq!(items.len(), 3);
499 assert!(items.contains(&TestItem::IronSword));
500 assert!(items.contains(&TestItem::SteelHelm));
501 assert!(items.contains(&TestItem::LeatherArmor));
502
503 assert!(slots.equipped_in(&EquipSlot::Weapon).is_none());
505 assert!(slots.equipped_in(&EquipSlot::Head).is_none());
506 assert!(slots.equipped_in(&EquipSlot::Body).is_none());
507 }
508
509 #[test]
510 fn clear_on_empty_slots_returns_empty_vec() {
511 let mut slots: EquipmentSlots<TestItem, Slot> =
512 EquipmentSlots::new(EquipSlot::<&str>::standard());
513 let items = slots.clear();
514 assert!(items.is_empty());
515 assert_eq!(slots.slot_count(), 6);
516 }
517
518 #[test]
521 fn all_equipped_returns_only_occupied_slots() {
522 let mut slots: EquipmentSlots<TestItem, Slot> =
523 EquipmentSlots::new(EquipSlot::<&str>::standard());
524 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
525 slots.equip(EquipSlot::Head, TestItem::SteelHelm).unwrap();
526 let equipped = slots.all_equipped();
529 assert_eq!(equipped.len(), 2);
530 assert!(equipped.contains(&(EquipSlot::Weapon, TestItem::IronSword)));
531 assert!(equipped.contains(&(EquipSlot::Head, TestItem::SteelHelm)));
532 }
533
534 #[test]
535 fn all_equipped_returns_empty_when_nothing_equipped() {
536 let slots: EquipmentSlots<TestItem, Slot> =
537 EquipmentSlots::new(EquipSlot::<&str>::standard());
538 assert!(slots.all_equipped().is_empty());
539 }
540
541 #[test]
544 fn slot_count_returns_total_slots() {
545 let slots: EquipmentSlots<TestItem, Slot> =
546 EquipmentSlots::new(EquipSlot::<&str>::standard());
547 assert_eq!(slots.slot_count(), 6);
548 }
549
550 #[test]
551 fn slot_count_unchanged_by_equip_or_unequip() {
552 let mut slots: EquipmentSlots<TestItem, Slot> =
553 EquipmentSlots::new(EquipSlot::<&str>::standard());
554 assert_eq!(slots.slot_count(), 6);
555 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
556 assert_eq!(slots.slot_count(), 6);
557 slots.unequip(&EquipSlot::Weapon);
558 assert_eq!(slots.slot_count(), 6);
559 }
560
561 #[test]
564 fn iter_yields_all_slots() {
565 let mut slots: EquipmentSlots<TestItem, Slot> =
566 EquipmentSlots::new(EquipSlot::<&str>::standard());
567 slots.equip(EquipSlot::Weapon, TestItem::IronSword).unwrap();
568 let entries: Vec<_> = slots.iter().collect();
569 assert_eq!(entries.len(), 6);
570 let weapon_entry = entries
572 .iter()
573 .find(|(s, _)| *s == EquipSlot::Weapon)
574 .unwrap();
575 assert_eq!(weapon_entry.1, Some(TestItem::IronSword));
576 }
577
578 use crate::items::{ItemKind, ItemProvider, ItemResult};
581
582 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
584 enum TestStat {
585 Attack,
586 Defense,
587 }
588
589 struct TestGame;
590
591 impl ItemProvider for TestGame {
592 type Item = TestItem;
593 type Effect = ();
594 type Monster = ();
595 type CustomKind = ();
596
597 fn item_name(&self, _item: &TestItem) -> &str {
598 "X"
599 }
600 fn item_description(&self, _item: &TestItem) -> &str {
601 "X"
602 }
603 fn item_effect(&self, _item: &TestItem) {}
604 fn item_price(&self, _item: &TestItem) -> u32 {
605 0
606 }
607 fn can_use_outside_battle(&self, _item: &TestItem) -> bool {
608 false
609 }
610 fn can_use_in_battle(&self, _item: &TestItem) -> bool {
611 false
612 }
613 fn use_on_monster(&self, _item: &TestItem, _m: &mut ()) -> ItemResult {
614 ItemResult::NoEffect
615 }
616 fn consume(&self, _item: &TestItem) -> bool {
617 false
618 }
619 fn item_kind(&self, item: &TestItem) -> ItemKind<()> {
620 match item {
621 TestItem::Potion => ItemKind::Consumable,
622 _ => ItemKind::Equipment,
623 }
624 }
625 }
626
627 impl EquipProvider for TestGame {
628 type CustomSlot = &'static str;
629 type Stat = TestStat;
630
631 fn equip_slots(&self, item: &TestItem) -> Vec<Slot> {
632 match item {
633 TestItem::IronSword => vec![EquipSlot::Weapon],
634 TestItem::SteelHelm => vec![EquipSlot::Head],
635 TestItem::RingOfPower => vec![EquipSlot::Accessory1, EquipSlot::Accessory2],
636 _ => Vec::new(),
637 }
638 }
639
640 fn stat_bonuses(&self, item: &TestItem) -> &[(TestStat, i16)] {
641 match item {
642 TestItem::IronSword => &[(TestStat::Attack, 5)],
643 TestItem::SteelHelm => &[(TestStat::Defense, 3)],
644 TestItem::RingOfPower => &[(TestStat::Attack, 2), (TestStat::Defense, 2)],
645 _ => &[],
646 }
647 }
648 }
649
650 #[test]
651 fn equip_provider_returns_real_stat_bonuses() {
652 let game = TestGame;
653 assert_eq!(
654 game.stat_bonuses(&TestItem::IronSword),
655 &[(TestStat::Attack, 5)]
656 );
657 assert_eq!(
658 game.stat_bonuses(&TestItem::RingOfPower),
659 &[(TestStat::Attack, 2), (TestStat::Defense, 2)]
660 );
661 assert!(game.stat_bonuses(&TestItem::Potion).is_empty());
662 }
663
664 #[test]
665 fn equip_provider_slots_gate_equippability() {
666 let game = TestGame;
667 assert_eq!(game.equip_slots(&TestItem::IronSword), vec![Slot::Weapon]);
668 assert_eq!(
669 game.equip_slots(&TestItem::RingOfPower),
670 vec![Slot::Accessory1, Slot::Accessory2]
671 );
672 assert!(game.equip_slots(&TestItem::Potion).is_empty());
673 }
674}