Skip to main content

dotzuki_engine/items/
mod.rs

1//! # Items module
2//!
3//! Core traits and types for item management in a JRPG engine.
4//!
5//! Provides [`ItemProvider`] for querying item metadata and applying effects,
6//! and [`ShopProvider`] for shop inventories.  Both traits use associated types
7//! so that implementing crates supply their own concrete item, effect, monster,
8//! and shop-identifier types — no game-specific data lives in this module.
9//!
10//! ## Supporting types
11//!
12//! | Type | Purpose |
13//! |------|---------|
14//! | [`Inventory<I>`] | Generic item inventory with `add` / `remove` / `contains` |
15//! | [`ItemResult`] | Outcome of attempting to use an item |
16//! | [`BagCategory`] | Broad classification of item types for UI organisation |
17
18use std::cmp::Ordering;
19use std::fmt::Debug;
20use std::hash::Hash;
21
22pub mod equip;
23pub mod kind;
24pub mod use_driver;
25pub use use_driver::{
26    buy, sell, use_item, ItemUseResult, ShopError, ShopReceipt, UsageContext,
27};
28pub use kind::ItemKind;
29pub use equip::{EquipProvider, EquipSlot};
30
31// ── Supporting types ──────────────────────────────────────────────────────
32
33/// Outcome of attempting to apply an item to a monster.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ItemResult {
36    /// Item was consumed and its effect applied successfully.
37    Used,
38    /// The item cannot be used in the current context (e.g., battle-only item
39    /// used outside battle).
40    NotUsable,
41    /// The player's bag does not contain this item.
42    NotOwned,
43    /// Item was applicable but produced no effect (e.g., healing a fully-healed
44    /// monster).
45    NoEffect,
46}
47
48/// Broad classification of item types, used by UI code to organise bag menus.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub enum BagCategory {
51    /// General consumables and utility items.
52    Items,
53    /// HP / PP / status recovery items.
54    Medicine,
55    /// Capture devices (balls / traps / etc.).
56    Balls,
57    /// Items usable only in battle (X items, Guard Spec., etc.).
58    Battle,
59    /// Key / plot items that cannot be sold or discarded.
60    Key,
61    /// Catch-all for categories not covered above.
62    Other,
63}
64
65/// Error returned when adding an item to an inventory fails due to capacity
66/// limits.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum AddError {
69    /// The inventory has reached its maximum number of distinct item slots.
70    InventoryFull,
71    /// A single slot has reached its per-slot quantity cap.
72    PerSlotCapReached(u32),
73}
74
75/// Generic item inventory that stores `(item, quantity)` pairs.
76///
77/// The type parameter `I` is the item identifier type, which must satisfy
78/// `Copy + Eq + Hash + Debug`.  Items with the same identity are stacked
79/// into a single slot — no duplicate entries.
80///
81/// The const parameter `N` is the **fixed slot capacity**: the inventory is
82/// stored inline in an array of `N` slots (zero heap allocation) and can hold
83/// at most `N` distinct items.  `max_per_slot` is an optional quantity cap;
84/// when `None` (the default, via [`new`](Inventory::new)) quantities are
85/// effectively unlimited.
86///
87/// Occupied slots are kept contiguous at the front of the backing array, so
88/// removal shifts subsequent slots left — identical ordering semantics to the
89/// former `Vec`-backed implementation.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct Inventory<I: Copy + Eq + Hash + Debug, const N: usize> {
92    /// Fixed-capacity slot storage; occupied slots live at `items[..len]`.
93    items: [Option<(I, u32)>; N],
94    /// Number of occupied slots (contiguous prefix of `items`).
95    len: usize,
96    /// Maximum quantity per slot (`None` = unlimited).
97    max_per_slot: Option<u32>,
98}
99
100/// Convenience alias for a large-capacity inventory.
101///
102/// `N = 256` is effectively unbounded for any real DOTZUKI item table.  This
103/// alias exists for forward-compatibility: if a second generic parameter is
104/// ever added to `Inventory` for tag/kind filtering, `SimpleInventory` will
105/// expand to `Inventory<I, 256, ()>` so that existing usage keeps compiling.
106pub type SimpleInventory<I> = Inventory<I, 256>;
107
108impl<I: Copy + Eq + Hash + Debug, const N: usize> Inventory<I, N> {
109    /// Create an empty inventory with `N` slots and no per-slot quantity cap.
110    pub fn new() -> Self {
111        Self {
112            items: [None; N],
113            len: 0,
114            max_per_slot: None,
115        }
116    }
117
118    /// Create an empty inventory with `N` slots and the given per-slot
119    /// quantity cap.
120    pub fn with_capacity(max_per_slot: u32) -> Self {
121        Self {
122            items: [None; N],
123            len: 0,
124            max_per_slot: Some(max_per_slot),
125        }
126    }
127
128    /// Number of distinct item slots (not total item count).
129    pub fn count(&self) -> usize {
130        self.len
131    }
132
133    /// Returns `true` if the inventory holds no items.
134    pub fn is_empty(&self) -> bool {
135        self.len == 0
136    }
137
138    /// Maximum number of distinct item slots (`N`).
139    pub fn capacity(&self) -> usize {
140        N
141    }
142
143    /// Returns `true` if the inventory holds at least `quantity` of `item`.
144    pub fn contains(&self, item: &I, quantity: u32) -> bool {
145        self.iter().any(|(i, q)| i == item && *q >= quantity)
146    }
147
148    /// Add `quantity` copies of `item`.
149    ///
150    /// If the item already exists in a slot the quantities are merged;
151    /// otherwise a new slot is appended.
152    ///
153    /// # Errors
154    ///
155    /// Returns [`AddError::InventoryFull`] if the inventory has reached its
156    /// slot limit and the item would require a new slot.
157    ///
158    /// Returns [`AddError::PerSlotCapReached`] if the combined quantity would
159    /// exceed the per-slot cap.
160    pub fn add(&mut self, item: I, quantity: u32) -> Result<(), AddError> {
161        if quantity == 0 {
162            return Ok(());
163        }
164        // Reject if adding to an existing slot would exceed per-slot cap.
165        if self.would_exceed_per_slot_cap(&item, quantity) {
166            return Err(AddError::PerSlotCapReached(self.max_per_slot.unwrap()));
167        }
168        // If the item does not already exist, check the slot cap.
169        let exists = self.iter().any(|(i, _)| *i == item);
170        if !exists && self.is_full() {
171            return Err(AddError::InventoryFull);
172        }
173        for slot in &mut self.items[..self.len] {
174            if let Some((existing, qty)) = slot {
175                if *existing == item {
176                    *qty = qty.saturating_add(quantity);
177                    return Ok(());
178                }
179            }
180        }
181        self.items[self.len] = Some((item, quantity));
182        self.len += 1;
183        Ok(())
184    }
185
186    /// Remove up to `quantity` copies of `item` from the first matching slot.
187    /// Returns `true` if the removal succeeded (item found and quantity
188    /// sufficient).
189    pub fn remove(&mut self, item: &I, quantity: u32) -> bool {
190        for i in 0..self.len {
191            if let Some((existing, qty)) = &mut self.items[i] {
192                if existing == item {
193                    if *qty < quantity {
194                        return false;
195                    }
196                    if *qty == quantity {
197                        self.remove_at(i);
198                    } else {
199                        *qty -= quantity;
200                    }
201                    return true;
202                }
203            }
204        }
205        false
206    }
207
208    // ── New methods ───────────────────────────────────────────────────────
209
210    /// Quantity of `item` in the inventory (0 if not owned).
211    pub fn quantity(&self, item: &I) -> u32 {
212        self.iter()
213            .find(|(i, _)| i == item)
214            .map(|(_, q)| *q)
215            .unwrap_or(0)
216    }
217
218    /// Returns `true` if the inventory has reached its slot limit.
219    pub fn is_full(&self) -> bool {
220        self.len >= N
221    }
222
223    /// Returns `true` if adding `add_quantity` of `item` would exceed the
224    /// per-slot quantity cap.
225    pub fn would_exceed_per_slot_cap(&self, item: &I, add_quantity: u32) -> bool {
226        let Some(cap) = self.max_per_slot else {
227            return false;
228        };
229        let current = self.quantity(item);
230        current.saturating_add(add_quantity) > cap
231    }
232
233    /// Return all slot entries matching a predicate.
234    pub fn filter<F>(&self, pred: F) -> Vec<&(I, u32)>
235    where
236        F: Fn(&I) -> bool,
237    {
238        self.iter().filter(|(i, _)| pred(i)).collect()
239    }
240
241    /// Sort slots by a custom comparator.
242    pub fn sort_by<F>(&mut self, mut cmp: F)
243    where
244        F: FnMut(&(I, u32), &(I, u32)) -> Ordering,
245    {
246        self.items[..self.len].sort_by(|a, b| {
247            cmp(a.as_ref().unwrap(), b.as_ref().unwrap())
248        });
249    }
250
251    /// Sort slots by item name, using the provided `name_fn` to extract a
252    /// string name from each item.
253    pub fn sort_by_name<F>(&mut self, name_fn: F)
254    where
255        F: Fn(&I) -> &str,
256    {
257        self.items[..self.len].sort_by(|a, b| {
258            name_fn(&a.as_ref().unwrap().0).cmp(name_fn(&b.as_ref().unwrap().0))
259        });
260    }
261
262    /// Consume the inventory and return its occupied slots as a `Vec`.
263    pub fn into_inner(self) -> Vec<(I, u32)> {
264        self.items.iter().flatten().copied().collect()
265    }
266
267    /// Iterate over all `(item, quantity)` entries.
268    pub fn iter(&self) -> impl Iterator<Item = &(I, u32)> {
269        self.items[..self.len].iter().filter_map(Option::as_ref)
270    }
271
272    /// Mutably iterate over all `(item, quantity)` entries.
273    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut (I, u32)> {
274        self.items[..self.len].iter_mut().filter_map(Option::as_mut)
275    }
276
277    /// The `(item, quantity)` entry at `index`, if occupied.
278    pub fn get(&self, index: usize) -> Option<&(I, u32)> {
279        self.items.get(index).and_then(Option::as_ref)
280    }
281
282    /// Mutable access to the `(item, quantity)` entry at `index`.
283    pub fn get_mut(&mut self, index: usize) -> Option<&mut (I, u32)> {
284        self.items.get_mut(index).and_then(Option::as_mut)
285    }
286
287    /// Append a new slot at the end (caller must ensure `!is_full()`).
288    /// Per-slot quantities are not merged or capped here.
289    pub fn push_slot(&mut self, item: I, quantity: u32) -> Result<(), AddError> {
290        if self.is_full() {
291            return Err(AddError::InventoryFull);
292        }
293        self.items[self.len] = Some((item, quantity));
294        self.len += 1;
295        Ok(())
296    }
297
298    /// Remove the slot at `index`, shifting subsequent slots left.
299    ///
300    /// # Panics
301    ///
302    /// Panics if `index` is out of bounds (not an occupied slot).
303    pub fn remove_at(&mut self, index: usize) {
304        assert!(index < self.len, "inventory slot index out of bounds");
305        self.items.copy_within(index + 1..self.len, index);
306        self.items[self.len - 1] = None;
307        self.len -= 1;
308    }
309
310    /// Swap the two slots at `a` and `b` (both must be occupied).
311    ///
312    /// # Panics
313    ///
314    /// Panics if either index is out of bounds.
315    pub fn swap(&mut self, a: usize, b: usize) {
316        self.items.swap(a, b);
317    }
318
319    /// Remove all items.
320    pub fn clear(&mut self) {
321        self.items.fill(None);
322        self.len = 0;
323    }
324}
325
326impl<I: Copy + Eq + Hash + Debug, const N: usize> Default for Inventory<I, N> {
327    fn default() -> Self {
328        Self::new()
329    }
330}
331
332// ── Traits ────────────────────────────────────────────────────────────────
333
334/// Provider trait for item metadata, usage rules, and effects.
335///
336/// Game-specific crates implement this trait to supply all item data the
337/// engine needs: names, descriptions, prices, usage eligibility, and the
338/// logic for applying an item to a monster.
339///
340/// # Associated types
341///
342/// * `Item` — The item identifier (typically an enum).
343/// * `Effect` — The effect descriptor (heal amount, status cure, etc.).
344/// * `Monster` — The monster / character type that items can target.
345pub trait ItemProvider {
346    /// Concrete item identifier type.
347    type Item: Copy + Eq + Hash + Debug;
348    /// Describes what the item does when used.
349    type Effect;
350    /// The monster / party-member type that items may be applied to.
351    type Monster;
352
353    /// Game-specific item kind discriminant (e.g. an enum of custom sub-categories).
354    type CustomKind: Copy + Eq + Hash + Debug;
355
356    /// Human-readable name of the item (e.g., `"Potion"`).
357    fn item_name(&self, item: &Self::Item) -> &str;
358
359    /// In-game flavour / description text.
360    fn item_description(&self, item: &Self::Item) -> &str;
361
362    /// The effect this item produces.
363    fn item_effect(&self, item: &Self::Item) -> Self::Effect;
364
365    /// Base purchase / sale price in the in-game currency.
366    fn item_price(&self, item: &Self::Item) -> u32;
367
368    /// Whether the item may be used outside of battle (e.g. from the bag
369    /// menu on the overworld).
370    fn can_use_outside_battle(&self, item: &Self::Item) -> bool;
371
372    /// Whether the item may be used during battle.
373    fn can_use_in_battle(&self, item: &Self::Item) -> bool;
374
375    /// Attempt to apply the item's effect to `monster`.
376    ///
377    /// Returns [`ItemResult::Used`] on success, or an appropriate error
378    /// variant if the item could not be applied.
379    fn use_on_monster(&self, item: &Self::Item, monster: &mut Self::Monster) -> ItemResult;
380
381    /// Returns `true` if the item is consumed (removed from inventory) after
382    /// a successful use.  Permanent items (key items, reusable tools, etc.)
383    /// return `false`.
384    fn consume(&self, item: &Self::Item) -> bool;
385
386    /// Classify the item into a gameplay category (`Consumable`, `Equipment`,
387    /// `KeyItem`, `Custom(...)`, etc.). Used by the engine for default
388    /// shop/bag behaviours.
389    ///
390    /// Equipment metadata (slots, stat bonuses) lives on the optional
391    /// [`EquipProvider`](crate::items::equip::EquipProvider) trait so that
392    /// games without an equipment system never declare slot or stat types.
393    fn item_kind(&self, item: &Self::Item) -> ItemKind<Self::CustomKind>;
394
395    /// Called when this item is used to teach a move to `target`. Returns
396    /// `None` (the default) if this item is not a move-teaching item.
397    fn on_teach_move<M: crate::party::MonsterProvider>(
398        &self,
399        item: Self::Item,
400        target: &mut crate::party::MonsterInstance<M>,
401    ) -> Option<ItemUseResult<Self::Item>> {
402        let _ = (item, target);
403        None
404    }
405
406    /// Called when this item is used in the field (overworld). Returns
407    /// `None` (the default) if the item has no field effect.
408    fn on_use_field(&self, item: Self::Item) -> Option<ItemUseResult<Self::Item>> {
409        let _ = item;
410        None
411    }
412
413    // ── P0e: opaque item-effect dispatch (additive, defaulted) ──────────────
414
415    /// Where / whether this item may be used (field, battle, both, or none).
416    ///
417    /// Defaults to [`UsageContext::FieldAndBattle`]. The
418    /// [`use_item`](crate::items::use_item) driver uses this to gate usage by
419    /// the active context before dispatching the effect.
420    fn usable_in(&self, item: &Self::Item) -> UsageContext {
421        let _ = item;
422        UsageContext::FieldAndBattle
423    }
424
425    /// Apply the item's effect to an optional target monster, in a context.
426    ///
427    /// This is an **opaque** dispatch: the engine routes the call and the game
428    /// owns ALL numbers — heal amounts, status cures, vitamins, level-up candy,
429    /// repel steps, capture rolls, and any game-specific item bugs. The engine
430    /// only reports the [`ItemUseResult`] back to
431    /// [`use_item`](crate::items::use_item), which consumes from the bag on
432    /// success.
433    ///
434    /// `provider` is the game's [`MonsterProvider`](crate::party::MonsterProvider)
435    /// instance, passed through so the effect implementation can query species
436    /// data, stat formulas, etc.
437    ///
438    /// Generic over the [`MonsterProvider`](crate::party::MonsterProvider) so
439    /// the engine never couples to a concrete monster model. Defaults to
440    /// [`ItemUseResult::NoEffect`] so games (and tests) that only need bag/shop
441    /// bookkeeping compile unchanged.
442    fn apply_effect<M: crate::party::MonsterProvider>(
443        &self,
444        provider: &M,
445        item: Self::Item,
446        ctx: UsageContext,
447        target: Option<&mut crate::party::MonsterInstance<M>>,
448        rng: &mut dyn crate::battle::rng::BattleRng,
449    ) -> ItemUseResult<Self::Item> {
450        let _ = (provider, item, ctx, target, rng);
451        ItemUseResult::NoEffect
452    }
453}
454
455/// Provider trait for shop / mart data.
456///
457/// Shops in a JRPG may sell items at prices that differ from the item's
458/// base price.  This trait lets the engine query a shop's inventory and
459/// its display name.
460///
461/// # Associated types
462///
463/// * `Item` — The item identifier (must match the [`ItemProvider::Item`] type).
464/// * `ShopId` — The shop identifier (typically an enum of shop locations).
465pub trait ShopProvider {
466    /// Concrete item identifier type.
467    type Item: Copy + Eq + Hash + Debug;
468    /// Shop location / identity type.
469    type ShopId: Copy + Eq + Hash + Debug;
470
471    /// Returns the shop's inventory as `(item, price)` pairs.
472    ///
473    /// The price in each pair is the price **this specific shop** charges,
474    /// which may differ from the base [`ItemProvider::item_price`].
475    fn shop_inventory(&self, shop_id: &Self::ShopId) -> Vec<(Self::Item, u32)>;
476
477    /// Human-readable name of the shop (e.g., `"City Mart"`).
478    fn shop_name(&self, shop_id: &Self::ShopId) -> &str;
479
480    // ── P0e: buy/sell pricing (additive, defaulted) ─────────────────────────
481
482    /// Price the player pays to buy one unit of `item`.
483    ///
484    /// Defaults to `0`; games override with their list price. Used by the
485    /// [`buy`](crate::items::buy) driver.
486    fn buy_price(&self, item: &Self::Item) -> u32 {
487        let _ = item;
488        0
489    }
490
491    /// Price the player receives for selling one unit of `item`.
492    ///
493    /// Defaults to half the [`buy_price`](ShopProvider::buy_price), matching
494    /// the Gen-1 mart sell rate. Used by the [`sell`](crate::items::sell)
495    /// driver.
496    fn sell_price(&self, item: &Self::Item) -> u32 {
497        self.buy_price(item) / 2
498    }
499
500    /// Whether the shop will buy `item` from the player.
501    ///
502    /// Defaults to `true`; games return `false` for key items and anything
503    /// else that cannot be sold. Used by the [`sell`](crate::items::sell)
504    /// driver.
505    fn can_sell(&self, item: &Self::Item) -> bool {
506        let _ = item;
507        true
508    }
509
510    // ── P0e: discount & stock limit features (additive, defaulted) ────────
511
512    /// Discount multiplier applied when buying from this shop.
513    ///
514    /// `1.0` = full price, `0.8` = 20 % off, `1.2` = premium surcharge.
515    /// Used by the [`buy`](crate::items::buy) driver.
516    fn discount_rate(&self, _shop_id: &Self::ShopId) -> f32 {
517        1.0
518    }
519
520    /// Per-shop sell-back multiplier applied **on top of**
521    /// [`sell_price`](ShopProvider::sell_price).
522    ///
523    /// Defaults to `1.0` (pass-through). The Gen-1 half-price rule is already
524    /// encoded in the `sell_price` default (`buy_price / 2`), so a non-unit
525    /// default here would compose to quarter price. Override per shop for
526    /// special vendors that pay more or less than the game-wide sell price.
527    fn sell_rate(&self, _shop_id: &Self::ShopId) -> f32 {
528        1.0
529    }
530
531    /// Whether `item` has limited stock in this shop.
532    ///
533    /// Defaults to `false` — unlimited stock by default.
534    fn has_limited_stock(&self, _item: &Self::Item) -> bool {
535        false
536    }
537
538    /// Maximum stock count for `item` when [`has_limited_stock`] is `true`.
539    ///
540    /// Defaults to `0`; meaningful only when `has_limited_stock` returns
541    /// `true`.
542    fn max_stock(&self, _item: &Self::Item) -> u32 {
543        0
544    }
545
546    /// Whether this shop periodically restocks sold‑out items.
547    ///
548    /// Defaults to `false`.
549    fn restocks(&self, _shop_id: &Self::ShopId) -> bool {
550        false
551    }
552
553    /// How many game‑ticks / steps / frames between restock cycles.
554    ///
555    /// Defaults to `0`; meaningful only when [`restocks`] returns `true`.
556    fn restock_interval(&self, _shop_id: &Self::ShopId) -> u32 {
557        0
558    }
559}
560
561// ── Mock tests ────────────────────────────────────────────────────────────
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566
567    // -- Mock types ---------------------------------------------------------
568
569    /// Minimal item type for testing the provider traits.
570    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
571    struct MockItem {
572        name: &'static str,
573        price: u32,
574        heal_amount: u32,
575    }
576
577    /// Effect descriptor for mock items.
578    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
579    enum MockEffect {
580        Heal(u32),
581        None,
582    }
583
584    /// Minimal monster for testing item application.
585    #[derive(Debug, Clone)]
586    #[allow(dead_code)]
587    struct MockMonster {
588        name: &'static str,
589        max_hp: u32,
590        current_hp: u32,
591    }
592
593    // -- Mock providers -----------------------------------------------------
594
595    struct MockItemProvider;
596
597    impl ItemProvider for MockItemProvider {
598        type Item = MockItem;
599        type Effect = MockEffect;
600        type Monster = MockMonster;
601        type CustomKind = ();
602
603        fn item_name(&self, item: &Self::Item) -> &str {
604            item.name
605        }
606
607        fn item_description(&self, item: &Self::Item) -> &str {
608            if item.heal_amount > 0 {
609                "Restores HP."
610            } else {
611                "Has no effect in battle."
612            }
613        }
614
615        fn item_effect(&self, item: &Self::Item) -> Self::Effect {
616            if item.heal_amount > 0 {
617                MockEffect::Heal(item.heal_amount)
618            } else {
619                MockEffect::None
620            }
621        }
622
623        fn item_price(&self, item: &Self::Item) -> u32 {
624            item.price
625        }
626
627        fn can_use_outside_battle(&self, _item: &Self::Item) -> bool {
628            true
629        }
630
631        fn can_use_in_battle(&self, _item: &Self::Item) -> bool {
632            true
633        }
634
635        fn use_on_monster(&self, item: &Self::Item, monster: &mut Self::Monster) -> ItemResult {
636            match self.item_effect(item) {
637                MockEffect::Heal(amount) => {
638                    if monster.current_hp >= monster.max_hp {
639                        return ItemResult::NoEffect;
640                    }
641                    monster.current_hp = (monster.current_hp + amount).min(monster.max_hp);
642                    ItemResult::Used
643                }
644                MockEffect::None => ItemResult::NoEffect,
645            }
646        }
647
648        fn consume(&self, _item: &Self::Item) -> bool {
649            true
650        }
651
652        fn item_kind(&self, item: &Self::Item) -> ItemKind<()> {
653            if item.heal_amount > 0 {
654                ItemKind::Consumable
655            } else {
656                ItemKind::Consumable
657            }
658        }
659    }
660
661    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
662    enum MockShopId {
663        CityMart,
664    }
665
666    struct MockShopProvider;
667
668    impl ShopProvider for MockShopProvider {
669        type Item = MockItem;
670        type ShopId = MockShopId;
671
672        fn shop_inventory(&self, shop_id: &Self::ShopId) -> Vec<(Self::Item, u32)> {
673            match shop_id {
674                MockShopId::CityMart => vec![
675                    (
676                        MockItem {
677                            name: "Potion",
678                            price: 300,
679                            heal_amount: 20,
680                        },
681                        300,
682                    ),
683                    (
684                        MockItem {
685                            name: "Elixir",
686                            price: 500,
687                            heal_amount: 0,
688                        },
689                        500,
690                    ),
691                ],
692            }
693        }
694
695        fn shop_name(&self, shop_id: &Self::ShopId) -> &str {
696            match shop_id {
697                MockShopId::CityMart => "City Mart",
698            }
699        }
700    }
701
702    // -- Tests: ItemProvider ------------------------------------------------
703
704    #[test]
705    fn potion_heals_monster() {
706        let provider = MockItemProvider;
707        let potion = MockItem {
708            name: "Potion",
709            price: 300,
710            heal_amount: 20,
711        };
712        let mut monster = MockMonster {
713            name: "Sprout",
714            max_hp: 100,
715            current_hp: 50,
716        };
717
718        let result = provider.use_on_monster(&potion, &mut monster);
719        assert_eq!(result, ItemResult::Used);
720        assert_eq!(monster.current_hp, 70);
721        assert!(provider.consume(&potion));
722    }
723
724    #[test]
725    fn potion_no_effect_on_full_hp() {
726        let provider = MockItemProvider;
727        let potion = MockItem {
728            name: "Potion",
729            price: 300,
730            heal_amount: 20,
731        };
732        let mut monster = MockMonster {
733            name: "Sprout",
734            max_hp: 100,
735            current_hp: 100,
736        };
737
738        let result = provider.use_on_monster(&potion, &mut monster);
739        assert_eq!(result, ItemResult::NoEffect);
740        assert_eq!(monster.current_hp, 100);
741    }
742
743    #[test]
744    fn elixir_has_no_heal_effect() {
745        let provider = MockItemProvider;
746        let elixir = MockItem {
747            name: "Elixir",
748            price: 500,
749            heal_amount: 0,
750        };
751        let mut monster = MockMonster {
752            name: "Sprout",
753            max_hp: 100,
754            current_hp: 50,
755        };
756
757        let result = provider.use_on_monster(&elixir, &mut monster);
758        assert_eq!(result, ItemResult::NoEffect);
759        assert_eq!(monster.current_hp, 50); // unchanged
760    }
761
762    // -- Tests: ShopProvider ------------------------------------------------
763
764    #[test]
765    fn shop_inventory_has_two_items() {
766        let provider = MockShopProvider;
767        let inventory = provider.shop_inventory(&MockShopId::CityMart);
768
769        assert_eq!(inventory.len(), 2);
770        assert_eq!(inventory[0].0.name, "Potion");
771        assert_eq!(inventory[0].1, 300);
772        assert_eq!(inventory[1].0.name, "Elixir");
773        assert_eq!(inventory[1].1, 500);
774    }
775
776    #[test]
777    fn shop_name_is_correct() {
778        let provider = MockShopProvider;
779        assert_eq!(
780            provider.shop_name(&MockShopId::CityMart),
781            "City Mart"
782        );
783    }
784
785    // -- Tests: Inventory ---------------------------------------------------
786
787    #[test]
788    fn inventory_add_and_remove() {
789        let mut inv: Inventory<MockItem, 8> = Inventory::new();
790        let potion = MockItem {
791            name: "Potion",
792            price: 300,
793            heal_amount: 20,
794        };
795
796        inv.add(potion, 3).unwrap();
797        assert_eq!(inv.count(), 1);
798        assert!(inv.contains(&potion, 2));
799        assert!(!inv.contains(&potion, 4));
800
801        assert!(inv.remove(&potion, 2));
802        assert_eq!(inv.count(), 1);
803        assert!(inv.contains(&potion, 1));
804
805        assert!(inv.remove(&potion, 1));
806        assert_eq!(inv.count(), 0);
807        assert!(!inv.contains(&potion, 1));
808    }
809
810    #[test]
811    fn inventory_stacks_same_item() {
812        let mut inv: Inventory<MockItem, 8> = Inventory::new();
813        let potion = MockItem {
814            name: "Potion",
815            price: 300,
816            heal_amount: 20,
817        };
818
819        inv.add(potion, 3).unwrap();
820        inv.add(potion, 5).unwrap();
821        assert_eq!(inv.count(), 1); // merged, not a new slot
822        assert!(inv.contains(&potion, 8));
823    }
824
825    #[test]
826    fn inventory_remove_insufficient_quantity() {
827        let mut inv: Inventory<MockItem, 8> = Inventory::new();
828        let potion = MockItem {
829            name: "Potion",
830            price: 300,
831            heal_amount: 20,
832        };
833
834        inv.add(potion, 2).unwrap();
835        assert!(!inv.remove(&potion, 5));
836        assert_eq!(inv.count(), 1);
837        assert!(inv.contains(&potion, 2)); // unchanged
838    }
839
840    #[test]
841    fn inventory_remove_nonexistent_item() {
842        let mut inv: Inventory<MockItem, 8> = Inventory::new();
843        let potion = MockItem {
844            name: "Potion",
845            price: 300,
846            heal_amount: 20,
847        };
848
849        assert!(!inv.remove(&potion, 1));
850    }
851
852    // ── New inventory tests ────────────────────────────────────────────────
853
854    #[test]
855    fn inventory_new_is_unlimited() {
856        let inv: Inventory<MockItem, 8> = Inventory::new();
857        assert!(!inv.is_full());
858        let potion = MockItem {
859            name: "Potion",
860            price: 300,
861            heal_amount: 20,
862        };
863        assert!(!inv.would_exceed_per_slot_cap(&potion, u32::MAX));
864    }
865
866    #[test]
867    fn inventory_with_capacity_rejects_overfill() {
868        let mut inv = Inventory::<MockItem, 2>::with_capacity(10);
869        let potion = MockItem {
870            name: "Potion",
871            price: 300,
872            heal_amount: 20,
873        };
874        let elixir = MockItem {
875            name: "Elixir",
876            price: 500,
877            heal_amount: 0,
878        };
879        let antidote = MockItem {
880            name: "Antidote",
881            price: 200,
882            heal_amount: 0,
883        };
884
885        assert!(inv.add(potion, 1).is_ok());
886        assert!(inv.add(elixir, 1).is_ok());
887        assert_eq!(inv.add(antidote, 1), Err(AddError::InventoryFull));
888    }
889
890    #[test]
891    fn inventory_with_capacity_rejects_per_slot_overflow() {
892        let mut inv = Inventory::<MockItem, 10>::with_capacity(5);
893        let potion = MockItem {
894            name: "Potion",
895            price: 300,
896            heal_amount: 20,
897        };
898
899        assert!(inv.add(potion, 3).is_ok());
900        assert!(inv.add(potion, 2).is_ok()); // total = 5, at cap
901        assert_eq!(inv.add(potion, 1), Err(AddError::PerSlotCapReached(5)));
902    }
903
904    #[test]
905    fn inventory_quantity() {
906        let mut inv: Inventory<MockItem, 8> = Inventory::new();
907        let potion = MockItem {
908            name: "Potion",
909            price: 300,
910            heal_amount: 20,
911        };
912
913        assert_eq!(inv.quantity(&potion), 0);
914        inv.add(potion, 3).unwrap();
915        assert_eq!(inv.quantity(&potion), 3);
916    }
917
918    #[test]
919    fn inventory_add_zero_is_ok() {
920        let mut inv: Inventory<MockItem, 8> = Inventory::new();
921        let potion = MockItem {
922            name: "Potion",
923            price: 300,
924            heal_amount: 20,
925        };
926        assert!(inv.add(potion, 0).is_ok());
927        assert_eq!(inv.count(), 0);
928    }
929
930    #[test]
931    fn inventory_filter() {
932        let mut inv: Inventory<MockItem, 8> = Inventory::new();
933        inv.add(
934            MockItem {
935                name: "Potion",
936                price: 300,
937                heal_amount: 20,
938            },
939            1,
940        )
941        .unwrap();
942        inv.add(
943            MockItem {
944                name: "Elixir",
945                price: 500,
946                heal_amount: 0,
947            },
948            1,
949        )
950        .unwrap();
951        inv.add(
952            MockItem {
953                name: "Antidote",
954                price: 200,
955                heal_amount: 0,
956            },
957            1,
958        )
959        .unwrap();
960
961        let cheap = inv.filter(|i| i.price < 350);
962        assert_eq!(cheap.len(), 2); // Potion + Antidote
963    }
964
965    #[test]
966    fn inventory_sort_by_name() {
967        let mut inv: Inventory<MockItem, 8> = Inventory::new();
968        let antidote = MockItem {
969            name: "Antidote",
970            price: 200,
971            heal_amount: 0,
972        };
973        let elixir = MockItem {
974            name: "Elixir",
975            price: 500,
976            heal_amount: 0,
977        };
978        let potion = MockItem {
979            name: "Potion",
980            price: 300,
981            heal_amount: 20,
982        };
983
984        inv.add(elixir, 1).unwrap();
985        inv.add(antidote, 1).unwrap();
986        inv.add(potion, 1).unwrap();
987
988        inv.sort_by_name(|i| i.name);
989        assert_eq!(inv.get(0).unwrap().0.name, "Antidote");
990        assert_eq!(inv.get(1).unwrap().0.name, "Elixir");
991        assert_eq!(inv.get(2).unwrap().0.name, "Potion");
992    }
993
994    #[test]
995    fn inventory_sort_by_price() {
996        let mut inv: Inventory<MockItem, 8> = Inventory::new();
997        inv.add(
998            MockItem {
999                name: "Potion",
1000                price: 300,
1001                heal_amount: 20,
1002            },
1003            1,
1004        )
1005        .unwrap();
1006        inv.add(
1007            MockItem {
1008                name: "Antidote",
1009                price: 200,
1010                heal_amount: 0,
1011            },
1012            1,
1013        )
1014        .unwrap();
1015        inv.add(
1016            MockItem {
1017                name: "Elixir",
1018                price: 500,
1019                heal_amount: 0,
1020            },
1021            1,
1022        )
1023        .unwrap();
1024
1025        inv.sort_by(|a, b| a.0.price.cmp(&b.0.price));
1026        assert_eq!(inv.get(0).unwrap().0.name, "Antidote");
1027        assert_eq!(inv.get(1).unwrap().0.name, "Potion");
1028        assert_eq!(inv.get(2).unwrap().0.name, "Elixir");
1029    }
1030
1031    #[test]
1032    fn inventory_into_inner() {
1033        let mut inv: Inventory<MockItem, 8> = Inventory::new();
1034        let potion = MockItem {
1035            name: "Potion",
1036            price: 300,
1037            heal_amount: 20,
1038        };
1039        inv.add(potion, 3).unwrap();
1040
1041        let inner = inv.into_inner();
1042        assert_eq!(inner.len(), 1);
1043        assert_eq!(inner[0].0.name, "Potion");
1044        assert_eq!(inner[0].1, 3);
1045    }
1046
1047    #[test]
1048    fn inventory_iter() {
1049        let mut inv: Inventory<MockItem, 8> = Inventory::new();
1050        inv.add(
1051            MockItem {
1052                name: "Potion",
1053                price: 300,
1054                heal_amount: 20,
1055            },
1056            1,
1057        )
1058        .unwrap();
1059        inv.add(
1060            MockItem {
1061                name: "Elixir",
1062                price: 500,
1063                heal_amount: 0,
1064            },
1065            1,
1066        )
1067        .unwrap();
1068
1069        let names: Vec<&str> = inv.iter().map(|(i, _)| i.name).collect();
1070        assert_eq!(names, vec!["Potion", "Elixir"]);
1071    }
1072
1073    #[test]
1074    fn inventory_simple_inventory_alias() {
1075        let mut inv: SimpleInventory<MockItem> = SimpleInventory::new();
1076        let potion = MockItem {
1077            name: "Potion",
1078            price: 300,
1079            heal_amount: 20,
1080        };
1081        inv.add(potion, 1).unwrap();
1082        assert_eq!(inv.count(), 1);
1083    }
1084
1085    #[test]
1086    fn inventory_is_full_false_when_under_cap() {
1087        let mut inv = Inventory::<MockItem, 3>::with_capacity(99);
1088        let potion = MockItem {
1089            name: "Potion",
1090            price: 300,
1091            heal_amount: 20,
1092        };
1093        assert!(!inv.is_full());
1094        inv.add(potion, 1).unwrap();
1095        assert!(!inv.is_full());
1096    }
1097
1098    #[test]
1099    fn inventory_is_full_true_at_cap() {
1100        let mut inv = Inventory::<MockItem, 2>::with_capacity(99);
1101        let potion = MockItem {
1102            name: "Potion",
1103            price: 300,
1104            heal_amount: 20,
1105        };
1106        let elixir = MockItem {
1107            name: "Elixir",
1108            price: 500,
1109            heal_amount: 0,
1110        };
1111        inv.add(potion, 1).unwrap();
1112        inv.add(elixir, 1).unwrap();
1113        assert!(inv.is_full());
1114    }
1115
1116    #[test]
1117    fn inventory_would_exceed_per_slot_cap() {
1118        let mut inv = Inventory::<MockItem, 10>::with_capacity(5);
1119        let potion = MockItem {
1120            name: "Potion",
1121            price: 300,
1122            heal_amount: 20,
1123        };
1124        assert!(!inv.would_exceed_per_slot_cap(&potion, 5)); // OK, at cap
1125        assert!(inv.would_exceed_per_slot_cap(&potion, 6)); // exceeds
1126        inv.add(potion, 3).unwrap();
1127        assert!(!inv.would_exceed_per_slot_cap(&potion, 2)); // 3+2=5, at cap
1128        assert!(inv.would_exceed_per_slot_cap(&potion, 3)); // 3+3=6, exceeds
1129    }
1130}