Skip to main content

dotzuki_engine/items/
equip.rs

1//! # Equipment system
2//!
3//! Generic equipment types for equipping and unequipping items in
4//! slot-based inventories.
5//!
6//! | Type | Purpose |
7//! |------|---------|
8//! | [`EquipSlot<Id>`] | Slot identifier with standard + custom variants |
9//! | [`EquipmentSlots<I, S>`] | Slot → item mapping with mutation methods |
10//! | [`EquipError`] | Error conditions for equipment operations |
11
12use core::fmt::Debug;
13use core::hash::Hash;
14
15// ── EquipSlot ──────────────────────────────────────────────────────────────
16
17/// Represents a slot where an item can be equipped.
18///
19/// Provides a set of standard equipment slots ([`Weapon`](EquipSlot::Weapon),
20/// [`Head`](EquipSlot::Head), [`Body`](EquipSlot::Body),
21/// [`Accessory1`](EquipSlot::Accessory1), [`Accessory2`](EquipSlot::Accessory2),
22/// [`HeldItem`](EquipSlot::HeldItem)) plus a [`Custom`](EquipSlot::Custom) variant
23/// for game-specific slots.
24#[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    /// Returns a static slice of the six standard equipment slots (all variants
37    /// except [`Custom`](EquipSlot::Custom)).
38    pub fn standard() -> &'static [Self] {
39        use EquipSlot::*;
40        &[Weapon, Head, Body, Accessory1, Accessory2, HeldItem]
41    }
42
43    /// Returns a human-readable label for this slot.
44    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// ── EquipError ─────────────────────────────────────────────────────────────
58
59/// Error that can occur when equipping an item.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum EquipError {
62    /// The target slot already contains an item (unequip it first).
63    SlotFull,
64    /// The target slot is not valid for this equipment set.
65    InvalidSlot,
66}
67
68// ── EquipmentSlots ─────────────────────────────────────────────────────────
69
70/// A collection of equipment slots that tracks which item (if any) is equipped
71/// in each slot.
72///
73/// # Type parameters
74///
75/// * `I` — Item identifier type.
76/// * `S` — Slot identifier type (typically [`EquipSlot<Id>`] or a game-specific
77///   enum).
78#[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
83// ── Constructors ───────────────────────────────────────────────────────────
84
85impl<I: Copy + Eq + Hash + Debug, S: Copy + Eq + Hash + Debug> EquipmentSlots<I, S> {
86    /// Create an empty equipment set from the given slots.
87    ///
88    /// All slots start with no item equipped.
89    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    /// Create an equipment set from a list of slot-item pairs.
99    ///
100    /// If a slot appears multiple times the last associated item wins. Any
101    /// slot not present in `pairs` will not exist in the resulting set.
102    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
115// ── Queries ────────────────────────────────────────────────────────────────
116
117impl<I: Copy + Eq + Hash + Debug, S: Copy + Eq + Hash + Debug> EquipmentSlots<I, S> {
118    /// Returns a reference to the item equipped in `slot`, or `None` if the
119    /// slot is empty or does not exist.
120    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    /// Returns all occupied slot-item pairs.
128    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    /// Returns `true` if `item` is equipped in any slot.
140    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    /// Returns the total number of slots (both occupied and empty).
147    pub fn slot_count(&self) -> usize {
148        self.slots.len()
149    }
150
151    /// Returns an iterator over all slot entries.
152    pub fn iter(&self) -> impl Iterator<Item = &(S, Option<I>)> {
153        self.slots.iter()
154    }
155}
156
157// ── Mutations ──────────────────────────────────────────────────────────────
158
159impl<I: Copy + Eq + Hash + Debug, S: Copy + Eq + Hash + Debug> EquipmentSlots<I, S> {
160    /// Equip `item` into `slot`.
161    ///
162    /// # Errors
163    ///
164    /// * [`EquipError::SlotFull`] — the slot already has an item.
165    /// * [`EquipError::InvalidSlot`] — `slot` does not exist in this set.
166    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    /// Remove and return the item equipped in `slot`, or `None` if the slot is
180    /// empty or does not exist.
181    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    /// Swap the items in two slots (including empty ↔ occupied).
189    ///
190    /// # Errors
191    ///
192    /// * [`EquipError::InvalidSlot`] — either `a` or `b` does not exist.
193    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    /// Unequip all slots and return the previously-equipped items.
210    ///
211    /// After this call every slot is empty.
212    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
223// ── EquipProvider ──────────────────────────────────────────────────────────
224
225/// Optional provider trait for games with an equipment system.
226///
227/// Split from [`ItemProvider`](super::ItemProvider) so games without
228/// equipment (e.g. early JRPGs) never declare slot or stat placeholder
229/// types. Games with equipment implement this *in addition to*
230/// [`ItemProvider`](super::ItemProvider); the engine's equipment flows bound
231/// on `EquipProvider` only where they actually need slot/bonus data.
232///
233/// The `Stat` associated type fixes the stat key at impl time, so
234/// [`stat_bonuses`](EquipProvider::stat_bonuses) can return real per-item
235/// data (a generic `<Stat>` method parameter would let the *caller* pick the
236/// type, which no impl could satisfy with anything but an empty slice).
237pub trait EquipProvider: super::ItemProvider {
238    /// Game-specific equipment slot identifier. Use an uninhabited enum if
239    /// the standard [`EquipSlot`] variants suffice.
240    type CustomSlot: Copy + Eq + Hash + Debug;
241    /// The game's stat identifier for equipment bonuses (typically the same
242    /// type as the game's `MonsterProvider::Stat`).
243    type Stat: Copy;
244
245    /// Which slots this item can be equipped into. Empty means the item is
246    /// not equipment.
247    fn equip_slots(&self, item: &Self::Item) -> Vec<EquipSlot<Self::CustomSlot>>;
248
249    /// Additive stat bonuses granted while this item is equipped. The game
250    /// applies these to its own monster stats; the engine never computes
251    /// totals itself. Defaults to no bonuses.
252    fn stat_bonuses(&self, item: &Self::Item) -> &[(Self::Stat, i16)] {
253        let _ = item;
254        &[]
255    }
256}
257
258// ── Tests ──────────────────────────────────────────────────────────────────
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    /// Minimal item id for equipment tests.
265    #[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    // -- EquipSlot ---------------------------------------------------------
277
278    #[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    // -- EquipmentSlots: construction ---------------------------------------
308
309    #[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), // overwrites
347        ]);
348        assert_eq!(slots.slot_count(), 1);
349        assert_eq!(
350            slots.equipped_in(&EquipSlot::Weapon),
351            Some(&TestItem::Potion)
352        );
353    }
354
355    // -- EquipmentSlots: equip ----------------------------------------------
356
357    #[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        // Original item should remain
376        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    // -- EquipmentSlots: unequip --------------------------------------------
392
393    #[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    // -- EquipmentSlots: is_equipped ----------------------------------------
418
419    #[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    // -- EquipmentSlots: swap -----------------------------------------------
444
445    #[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    // -- EquipmentSlots: clear ----------------------------------------------
484
485    #[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        // All slots should be empty now
503        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    // -- EquipmentSlots: all_equipped ---------------------------------------
518
519    #[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        // Accessory1, Accessory2, Body, HeldItem remain empty
526
527        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    // -- EquipmentSlots: slot_count -----------------------------------------
541
542    #[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    // -- EquipmentSlots: iter -----------------------------------------------
561
562    #[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        // Weapon slot should have the item
570        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    // -- EquipProvider -------------------------------------------------------
578
579    use crate::items::{ItemKind, ItemProvider, ItemResult};
580
581    /// Stat key for the EquipProvider test game.
582    #[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}