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 std::fmt::Debug;
13use std::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!(
305            EquipSlot::<&str>::Custom("ring").label(),
306            "Custom"
307        );
308    }
309
310    // -- EquipmentSlots: construction ---------------------------------------
311
312    #[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), // overwrites
350        ]);
351        assert_eq!(slots.slot_count(), 1);
352        assert_eq!(
353            slots.equipped_in(&EquipSlot::Weapon),
354            Some(&TestItem::Potion)
355        );
356    }
357
358    // -- EquipmentSlots: equip ----------------------------------------------
359
360    #[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        // Original item should remain
379        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    // -- EquipmentSlots: unequip --------------------------------------------
395
396    #[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    // -- EquipmentSlots: is_equipped ----------------------------------------
421
422    #[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    // -- EquipmentSlots: swap -----------------------------------------------
447
448    #[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    // -- EquipmentSlots: clear ----------------------------------------------
487
488    #[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        // All slots should be empty now
504        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    // -- EquipmentSlots: all_equipped ---------------------------------------
519
520    #[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        // Accessory1, Accessory2, Body, HeldItem remain empty
527
528        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    // -- EquipmentSlots: slot_count -----------------------------------------
542
543    #[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    // -- EquipmentSlots: iter -----------------------------------------------
562
563    #[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        // Weapon slot should have the item
571        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    // -- EquipProvider -------------------------------------------------------
579
580    use crate::items::{ItemKind, ItemProvider, ItemResult};
581
582    /// Stat key for the EquipProvider test game.
583    #[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}