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 core::cmp::Ordering;
19use core::fmt::Debug;
20use core::hash::Hash;
21
22pub mod equip;
23pub mod kind;
24pub mod mart;
25pub mod use_driver;
26pub use equip::{EquipProvider, EquipSlot};
27pub use kind::ItemKind;
28pub use mart::{MartBackend, MartDriver, MartState, MartStock};
29pub use use_driver::{buy, sell, use_item, ItemUseResult, ShopError, ShopReceipt, UsageContext};
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| cmp(a.as_ref().unwrap(), b.as_ref().unwrap()));
247    }
248
249    /// Sort slots by item name, using the provided `name_fn` to extract a
250    /// string name from each item.
251    pub fn sort_by_name<F>(&mut self, name_fn: F)
252    where
253        F: Fn(&I) -> &str,
254    {
255        self.items[..self.len]
256            .sort_by(|a, b| name_fn(&a.as_ref().unwrap().0).cmp(name_fn(&b.as_ref().unwrap().0)));
257    }
258
259    /// Consume the inventory and return its occupied slots as a `Vec`.
260    pub fn into_inner(self) -> Vec<(I, u32)> {
261        self.items.iter().flatten().copied().collect()
262    }
263
264    /// Iterate over all `(item, quantity)` entries.
265    pub fn iter(&self) -> impl Iterator<Item = &(I, u32)> {
266        self.items[..self.len].iter().filter_map(Option::as_ref)
267    }
268
269    /// Mutably iterate over all `(item, quantity)` entries.
270    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut (I, u32)> {
271        self.items[..self.len].iter_mut().filter_map(Option::as_mut)
272    }
273
274    /// The `(item, quantity)` entry at `index`, if occupied.
275    pub fn get(&self, index: usize) -> Option<&(I, u32)> {
276        self.items.get(index).and_then(Option::as_ref)
277    }
278
279    /// Mutable access to the `(item, quantity)` entry at `index`.
280    pub fn get_mut(&mut self, index: usize) -> Option<&mut (I, u32)> {
281        self.items.get_mut(index).and_then(Option::as_mut)
282    }
283
284    /// Append a new slot at the end (caller must ensure `!is_full()`).
285    /// Per-slot quantities are not merged or capped here.
286    pub fn push_slot(&mut self, item: I, quantity: u32) -> Result<(), AddError> {
287        if self.is_full() {
288            return Err(AddError::InventoryFull);
289        }
290        self.items[self.len] = Some((item, quantity));
291        self.len += 1;
292        Ok(())
293    }
294
295    /// Remove the slot at `index`, shifting subsequent slots left.
296    ///
297    /// # Panics
298    ///
299    /// Panics if `index` is out of bounds (not an occupied slot).
300    pub fn remove_at(&mut self, index: usize) {
301        assert!(index < self.len, "inventory slot index out of bounds");
302        self.items.copy_within(index + 1..self.len, index);
303        self.items[self.len - 1] = None;
304        self.len -= 1;
305    }
306
307    /// Swap the two slots at `a` and `b` (both must be occupied).
308    ///
309    /// # Panics
310    ///
311    /// Panics if either index is out of bounds.
312    pub fn swap(&mut self, a: usize, b: usize) {
313        self.items.swap(a, b);
314    }
315
316    /// Remove all items.
317    pub fn clear(&mut self) {
318        self.items.fill(None);
319        self.len = 0;
320    }
321}
322
323impl<I: Copy + Eq + Hash + Debug, const N: usize> Default for Inventory<I, N> {
324    fn default() -> Self {
325        Self::new()
326    }
327}
328
329// ── Traits ────────────────────────────────────────────────────────────────
330
331/// Provider trait for item metadata, usage rules, and effects.
332///
333/// Game-specific crates implement this trait to supply all item data the
334/// engine needs: names, descriptions, prices, usage eligibility, and the
335/// logic for applying an item to a monster.
336///
337/// # Associated types
338///
339/// * `Item` — The item identifier (typically an enum).
340/// * `Effect` — The effect descriptor (heal amount, status cure, etc.).
341/// * `Monster` — The monster / character type that items can target.
342pub trait ItemProvider {
343    /// Concrete item identifier type.
344    type Item: Copy + Eq + Hash + Debug;
345    /// Describes what the item does when used.
346    type Effect;
347    /// The monster / party-member type that items may be applied to.
348    type Monster;
349
350    /// Game-specific item kind discriminant (e.g. an enum of custom sub-categories).
351    type CustomKind: Copy + Eq + Hash + Debug;
352
353    /// Human-readable name of the item (e.g., `"Potion"`).
354    fn item_name(&self, item: &Self::Item) -> &str;
355
356    /// In-game flavour / description text.
357    fn item_description(&self, item: &Self::Item) -> &str;
358
359    /// The effect this item produces.
360    fn item_effect(&self, item: &Self::Item) -> Self::Effect;
361
362    /// Base purchase / sale price in the in-game currency.
363    fn item_price(&self, item: &Self::Item) -> u32;
364
365    /// Whether the item may be used outside of battle (e.g. from the bag
366    /// menu on the overworld).
367    fn can_use_outside_battle(&self, item: &Self::Item) -> bool;
368
369    /// Whether the item may be used during battle.
370    fn can_use_in_battle(&self, item: &Self::Item) -> bool;
371
372    /// Attempt to apply the item's effect to `monster`.
373    ///
374    /// Returns [`ItemResult::Used`] on success, or an appropriate error
375    /// variant if the item could not be applied.
376    fn use_on_monster(&self, item: &Self::Item, monster: &mut Self::Monster) -> ItemResult;
377
378    /// Returns `true` if the item is consumed (removed from inventory) after
379    /// a successful use.  Permanent items (key items, reusable tools, etc.)
380    /// return `false`.
381    fn consume(&self, item: &Self::Item) -> bool;
382
383    /// Classify the item into a gameplay category (`Consumable`, `Equipment`,
384    /// `KeyItem`, `Custom(...)`, etc.). Used by the engine for default
385    /// shop/bag behaviours.
386    ///
387    /// Equipment metadata (slots, stat bonuses) lives on the optional
388    /// [`EquipProvider`](crate::items::equip::EquipProvider) trait so that
389    /// games without an equipment system never declare slot or stat types.
390    fn item_kind(&self, item: &Self::Item) -> ItemKind<Self::CustomKind>;
391
392    /// Called when this item is used to teach a move to `target`. Returns
393    /// `None` (the default) if this item is not a move-teaching item.
394    fn on_teach_move<M: crate::party::MonsterProvider>(
395        &self,
396        item: Self::Item,
397        target: &mut crate::party::MonsterInstance<M>,
398    ) -> Option<ItemUseResult<Self::Item>> {
399        let _ = (item, target);
400        None
401    }
402
403    /// Called when this item is used in the field (overworld). Returns
404    /// `None` (the default) if the item has no field effect.
405    fn on_use_field(&self, item: Self::Item) -> Option<ItemUseResult<Self::Item>> {
406        let _ = item;
407        None
408    }
409
410    // ── P0e: opaque item-effect dispatch (additive, defaulted) ──────────────
411
412    /// Where / whether this item may be used (field, battle, both, or none).
413    ///
414    /// Defaults to [`UsageContext::FieldAndBattle`]. The
415    /// [`use_item`](crate::items::use_item) driver uses this to gate usage by
416    /// the active context before dispatching the effect.
417    fn usable_in(&self, item: &Self::Item) -> UsageContext {
418        let _ = item;
419        UsageContext::FieldAndBattle
420    }
421
422    /// Apply the item's effect to an optional target monster, in a context.
423    ///
424    /// This is an **opaque** dispatch: the engine routes the call and the game
425    /// owns ALL numbers — heal amounts, status cures, vitamins, level-up candy,
426    /// repel steps, capture rolls, and any game-specific item bugs. The engine
427    /// only reports the [`ItemUseResult`] back to
428    /// [`use_item`](crate::items::use_item), which consumes from the bag on
429    /// success.
430    ///
431    /// `provider` is the game's [`MonsterProvider`](crate::party::MonsterProvider)
432    /// instance, passed through so the effect implementation can query species
433    /// data, stat formulas, etc.
434    ///
435    /// Generic over the [`MonsterProvider`](crate::party::MonsterProvider) so
436    /// the engine never couples to a concrete monster model. Defaults to
437    /// [`ItemUseResult::NoEffect`] so games (and tests) that only need bag/shop
438    /// bookkeeping compile unchanged.
439    fn apply_effect<M: crate::party::MonsterProvider>(
440        &self,
441        provider: &M,
442        item: Self::Item,
443        ctx: UsageContext,
444        target: Option<&mut crate::party::MonsterInstance<M>>,
445        rng: &mut dyn crate::battle::rng::BattleRng,
446    ) -> ItemUseResult<Self::Item> {
447        let _ = (provider, item, ctx, target, rng);
448        ItemUseResult::NoEffect
449    }
450}
451
452/// Provider trait for shop / mart data.
453///
454/// Shops in a JRPG may sell items at prices that differ from the item's
455/// base price.  This trait lets the engine query a shop's inventory and
456/// its display name.
457///
458/// # Associated types
459///
460/// * `Item` — The item identifier (must match the [`ItemProvider::Item`] type).
461/// * `ShopId` — The shop identifier (typically an enum of shop locations).
462pub trait ShopProvider {
463    /// Concrete item identifier type.
464    type Item: Copy + Eq + Hash + Debug;
465    /// Shop location / identity type.
466    type ShopId: Copy + Eq + Hash + Debug;
467
468    /// Returns the shop's inventory as `(item, price)` pairs.
469    ///
470    /// The price in each pair is the price **this specific shop** charges,
471    /// which may differ from the base [`ItemProvider::item_price`].
472    fn shop_inventory(&self, shop_id: &Self::ShopId) -> Vec<(Self::Item, u32)>;
473
474    /// Human-readable name of the shop (e.g., `"City Mart"`).
475    fn shop_name(&self, shop_id: &Self::ShopId) -> &str;
476
477    // ── P0e: buy/sell pricing (additive, defaulted) ─────────────────────────
478
479    /// Price the player pays to buy one unit of `item`.
480    ///
481    /// Defaults to `0`; games override with their list price. Used by the
482    /// [`buy`](crate::items::buy) driver.
483    fn buy_price(&self, item: &Self::Item) -> u32 {
484        let _ = item;
485        0
486    }
487
488    /// Price the player receives for selling one unit of `item`.
489    ///
490    /// Defaults to half the [`buy_price`](ShopProvider::buy_price), matching
491    /// the Gen-1 mart sell rate. Used by the [`sell`](crate::items::sell)
492    /// driver.
493    fn sell_price(&self, item: &Self::Item) -> u32 {
494        self.buy_price(item) / 2
495    }
496
497    /// Whether the shop will buy `item` from the player.
498    ///
499    /// Defaults to `true`; games return `false` for key items and anything
500    /// else that cannot be sold. Used by the [`sell`](crate::items::sell)
501    /// driver.
502    fn can_sell(&self, item: &Self::Item) -> bool {
503        let _ = item;
504        true
505    }
506
507    // ── P0e: discount & stock limit features (additive, defaulted) ────────
508
509    /// Discount multiplier applied when buying from this shop.
510    ///
511    /// `1.0` = full price, `0.8` = 20 % off, `1.2` = premium surcharge.
512    /// Used by the [`buy`](crate::items::buy) driver.
513    fn discount_rate(&self, _shop_id: &Self::ShopId) -> f32 {
514        1.0
515    }
516
517    /// Per-shop sell-back multiplier applied **on top of**
518    /// [`sell_price`](ShopProvider::sell_price).
519    ///
520    /// Defaults to `1.0` (pass-through). The Gen-1 half-price rule is already
521    /// encoded in the `sell_price` default (`buy_price / 2`), so a non-unit
522    /// default here would compose to quarter price. Override per shop for
523    /// special vendors that pay more or less than the game-wide sell price.
524    fn sell_rate(&self, _shop_id: &Self::ShopId) -> f32 {
525        1.0
526    }
527
528    /// Whether `item` has limited stock in this shop.
529    ///
530    /// Defaults to `false` — unlimited stock by default.
531    fn has_limited_stock(&self, _item: &Self::Item) -> bool {
532        false
533    }
534
535    /// Maximum stock count for `item` when [`has_limited_stock`] is `true`.
536    ///
537    /// Defaults to `0`; meaningful only when `has_limited_stock` returns
538    /// `true`.
539    fn max_stock(&self, _item: &Self::Item) -> u32 {
540        0
541    }
542
543    /// Whether this shop periodically restocks sold‑out items.
544    ///
545    /// Defaults to `false`.
546    fn restocks(&self, _shop_id: &Self::ShopId) -> bool {
547        false
548    }
549
550    /// How many game‑ticks / steps / frames between restock cycles.
551    ///
552    /// Defaults to `0`; meaningful only when [`restocks`] returns `true`.
553    fn restock_interval(&self, _shop_id: &Self::ShopId) -> u32 {
554        0
555    }
556}
557
558// ── Mock tests ────────────────────────────────────────────────────────────
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563
564    // -- Mock types ---------------------------------------------------------
565
566    /// Minimal item type for testing the provider traits.
567    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
568    struct MockItem {
569        name: &'static str,
570        price: u32,
571        heal_amount: u32,
572    }
573
574    /// Effect descriptor for mock items.
575    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
576    enum MockEffect {
577        Heal(u32),
578        None,
579    }
580
581    /// Minimal monster for testing item application.
582    #[derive(Debug, Clone)]
583    #[allow(dead_code)]
584    struct MockMonster {
585        name: &'static str,
586        max_hp: u32,
587        current_hp: u32,
588    }
589
590    // -- Mock providers -----------------------------------------------------
591
592    struct MockItemProvider;
593
594    impl ItemProvider for MockItemProvider {
595        type Item = MockItem;
596        type Effect = MockEffect;
597        type Monster = MockMonster;
598        type CustomKind = ();
599
600        fn item_name(&self, item: &Self::Item) -> &str {
601            item.name
602        }
603
604        fn item_description(&self, item: &Self::Item) -> &str {
605            if item.heal_amount > 0 {
606                "Restores HP."
607            } else {
608                "Has no effect in battle."
609            }
610        }
611
612        fn item_effect(&self, item: &Self::Item) -> Self::Effect {
613            if item.heal_amount > 0 {
614                MockEffect::Heal(item.heal_amount)
615            } else {
616                MockEffect::None
617            }
618        }
619
620        fn item_price(&self, item: &Self::Item) -> u32 {
621            item.price
622        }
623
624        fn can_use_outside_battle(&self, _item: &Self::Item) -> bool {
625            true
626        }
627
628        fn can_use_in_battle(&self, _item: &Self::Item) -> bool {
629            true
630        }
631
632        fn use_on_monster(&self, item: &Self::Item, monster: &mut Self::Monster) -> ItemResult {
633            match self.item_effect(item) {
634                MockEffect::Heal(amount) => {
635                    if monster.current_hp >= monster.max_hp {
636                        return ItemResult::NoEffect;
637                    }
638                    monster.current_hp = (monster.current_hp + amount).min(monster.max_hp);
639                    ItemResult::Used
640                }
641                MockEffect::None => ItemResult::NoEffect,
642            }
643        }
644
645        fn consume(&self, _item: &Self::Item) -> bool {
646            true
647        }
648
649        fn item_kind(&self, item: &Self::Item) -> ItemKind<()> {
650            if item.heal_amount > 0 {
651                ItemKind::Consumable
652            } else {
653                ItemKind::Consumable
654            }
655        }
656    }
657
658    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
659    enum MockShopId {
660        CityMart,
661    }
662
663    struct MockShopProvider;
664
665    impl ShopProvider for MockShopProvider {
666        type Item = MockItem;
667        type ShopId = MockShopId;
668
669        fn shop_inventory(&self, shop_id: &Self::ShopId) -> Vec<(Self::Item, u32)> {
670            match shop_id {
671                MockShopId::CityMart => vec![
672                    (
673                        MockItem {
674                            name: "Potion",
675                            price: 300,
676                            heal_amount: 20,
677                        },
678                        300,
679                    ),
680                    (
681                        MockItem {
682                            name: "Elixir",
683                            price: 500,
684                            heal_amount: 0,
685                        },
686                        500,
687                    ),
688                ],
689            }
690        }
691
692        fn shop_name(&self, shop_id: &Self::ShopId) -> &str {
693            match shop_id {
694                MockShopId::CityMart => "City Mart",
695            }
696        }
697    }
698
699    // -- Tests: ItemProvider ------------------------------------------------
700
701    #[test]
702    fn potion_heals_monster() {
703        let provider = MockItemProvider;
704        let potion = MockItem {
705            name: "Potion",
706            price: 300,
707            heal_amount: 20,
708        };
709        let mut monster = MockMonster {
710            name: "Sprout",
711            max_hp: 100,
712            current_hp: 50,
713        };
714
715        let result = provider.use_on_monster(&potion, &mut monster);
716        assert_eq!(result, ItemResult::Used);
717        assert_eq!(monster.current_hp, 70);
718        assert!(provider.consume(&potion));
719    }
720
721    #[test]
722    fn potion_no_effect_on_full_hp() {
723        let provider = MockItemProvider;
724        let potion = MockItem {
725            name: "Potion",
726            price: 300,
727            heal_amount: 20,
728        };
729        let mut monster = MockMonster {
730            name: "Sprout",
731            max_hp: 100,
732            current_hp: 100,
733        };
734
735        let result = provider.use_on_monster(&potion, &mut monster);
736        assert_eq!(result, ItemResult::NoEffect);
737        assert_eq!(monster.current_hp, 100);
738    }
739
740    #[test]
741    fn elixir_has_no_heal_effect() {
742        let provider = MockItemProvider;
743        let elixir = MockItem {
744            name: "Elixir",
745            price: 500,
746            heal_amount: 0,
747        };
748        let mut monster = MockMonster {
749            name: "Sprout",
750            max_hp: 100,
751            current_hp: 50,
752        };
753
754        let result = provider.use_on_monster(&elixir, &mut monster);
755        assert_eq!(result, ItemResult::NoEffect);
756        assert_eq!(monster.current_hp, 50); // unchanged
757    }
758
759    // -- Tests: ShopProvider ------------------------------------------------
760
761    #[test]
762    fn shop_inventory_has_two_items() {
763        let provider = MockShopProvider;
764        let inventory = provider.shop_inventory(&MockShopId::CityMart);
765
766        assert_eq!(inventory.len(), 2);
767        assert_eq!(inventory[0].0.name, "Potion");
768        assert_eq!(inventory[0].1, 300);
769        assert_eq!(inventory[1].0.name, "Elixir");
770        assert_eq!(inventory[1].1, 500);
771    }
772
773    #[test]
774    fn shop_name_is_correct() {
775        let provider = MockShopProvider;
776        assert_eq!(provider.shop_name(&MockShopId::CityMart), "City Mart");
777    }
778
779    // -- Tests: Inventory ---------------------------------------------------
780
781    #[test]
782    fn inventory_add_and_remove() {
783        let mut inv: Inventory<MockItem, 8> = Inventory::new();
784        let potion = MockItem {
785            name: "Potion",
786            price: 300,
787            heal_amount: 20,
788        };
789
790        inv.add(potion, 3).unwrap();
791        assert_eq!(inv.count(), 1);
792        assert!(inv.contains(&potion, 2));
793        assert!(!inv.contains(&potion, 4));
794
795        assert!(inv.remove(&potion, 2));
796        assert_eq!(inv.count(), 1);
797        assert!(inv.contains(&potion, 1));
798
799        assert!(inv.remove(&potion, 1));
800        assert_eq!(inv.count(), 0);
801        assert!(!inv.contains(&potion, 1));
802    }
803
804    #[test]
805    fn inventory_stacks_same_item() {
806        let mut inv: Inventory<MockItem, 8> = Inventory::new();
807        let potion = MockItem {
808            name: "Potion",
809            price: 300,
810            heal_amount: 20,
811        };
812
813        inv.add(potion, 3).unwrap();
814        inv.add(potion, 5).unwrap();
815        assert_eq!(inv.count(), 1); // merged, not a new slot
816        assert!(inv.contains(&potion, 8));
817    }
818
819    #[test]
820    fn inventory_remove_insufficient_quantity() {
821        let mut inv: Inventory<MockItem, 8> = Inventory::new();
822        let potion = MockItem {
823            name: "Potion",
824            price: 300,
825            heal_amount: 20,
826        };
827
828        inv.add(potion, 2).unwrap();
829        assert!(!inv.remove(&potion, 5));
830        assert_eq!(inv.count(), 1);
831        assert!(inv.contains(&potion, 2)); // unchanged
832    }
833
834    #[test]
835    fn inventory_remove_nonexistent_item() {
836        let mut inv: Inventory<MockItem, 8> = Inventory::new();
837        let potion = MockItem {
838            name: "Potion",
839            price: 300,
840            heal_amount: 20,
841        };
842
843        assert!(!inv.remove(&potion, 1));
844    }
845
846    // ── New inventory tests ────────────────────────────────────────────────
847
848    #[test]
849    fn inventory_new_is_unlimited() {
850        let inv: Inventory<MockItem, 8> = Inventory::new();
851        assert!(!inv.is_full());
852        let potion = MockItem {
853            name: "Potion",
854            price: 300,
855            heal_amount: 20,
856        };
857        assert!(!inv.would_exceed_per_slot_cap(&potion, u32::MAX));
858    }
859
860    #[test]
861    fn inventory_with_capacity_rejects_overfill() {
862        let mut inv = Inventory::<MockItem, 2>::with_capacity(10);
863        let potion = MockItem {
864            name: "Potion",
865            price: 300,
866            heal_amount: 20,
867        };
868        let elixir = MockItem {
869            name: "Elixir",
870            price: 500,
871            heal_amount: 0,
872        };
873        let antidote = MockItem {
874            name: "Antidote",
875            price: 200,
876            heal_amount: 0,
877        };
878
879        assert!(inv.add(potion, 1).is_ok());
880        assert!(inv.add(elixir, 1).is_ok());
881        assert_eq!(inv.add(antidote, 1), Err(AddError::InventoryFull));
882    }
883
884    #[test]
885    fn inventory_with_capacity_rejects_per_slot_overflow() {
886        let mut inv = Inventory::<MockItem, 10>::with_capacity(5);
887        let potion = MockItem {
888            name: "Potion",
889            price: 300,
890            heal_amount: 20,
891        };
892
893        assert!(inv.add(potion, 3).is_ok());
894        assert!(inv.add(potion, 2).is_ok()); // total = 5, at cap
895        assert_eq!(inv.add(potion, 1), Err(AddError::PerSlotCapReached(5)));
896    }
897
898    #[test]
899    fn inventory_quantity() {
900        let mut inv: Inventory<MockItem, 8> = Inventory::new();
901        let potion = MockItem {
902            name: "Potion",
903            price: 300,
904            heal_amount: 20,
905        };
906
907        assert_eq!(inv.quantity(&potion), 0);
908        inv.add(potion, 3).unwrap();
909        assert_eq!(inv.quantity(&potion), 3);
910    }
911
912    #[test]
913    fn inventory_add_zero_is_ok() {
914        let mut inv: Inventory<MockItem, 8> = Inventory::new();
915        let potion = MockItem {
916            name: "Potion",
917            price: 300,
918            heal_amount: 20,
919        };
920        assert!(inv.add(potion, 0).is_ok());
921        assert_eq!(inv.count(), 0);
922    }
923
924    #[test]
925    fn inventory_filter() {
926        let mut inv: Inventory<MockItem, 8> = Inventory::new();
927        inv.add(
928            MockItem {
929                name: "Potion",
930                price: 300,
931                heal_amount: 20,
932            },
933            1,
934        )
935        .unwrap();
936        inv.add(
937            MockItem {
938                name: "Elixir",
939                price: 500,
940                heal_amount: 0,
941            },
942            1,
943        )
944        .unwrap();
945        inv.add(
946            MockItem {
947                name: "Antidote",
948                price: 200,
949                heal_amount: 0,
950            },
951            1,
952        )
953        .unwrap();
954
955        let cheap = inv.filter(|i| i.price < 350);
956        assert_eq!(cheap.len(), 2); // Potion + Antidote
957    }
958
959    #[test]
960    fn inventory_sort_by_name() {
961        let mut inv: Inventory<MockItem, 8> = Inventory::new();
962        let antidote = MockItem {
963            name: "Antidote",
964            price: 200,
965            heal_amount: 0,
966        };
967        let elixir = MockItem {
968            name: "Elixir",
969            price: 500,
970            heal_amount: 0,
971        };
972        let potion = MockItem {
973            name: "Potion",
974            price: 300,
975            heal_amount: 20,
976        };
977
978        inv.add(elixir, 1).unwrap();
979        inv.add(antidote, 1).unwrap();
980        inv.add(potion, 1).unwrap();
981
982        inv.sort_by_name(|i| i.name);
983        assert_eq!(inv.get(0).unwrap().0.name, "Antidote");
984        assert_eq!(inv.get(1).unwrap().0.name, "Elixir");
985        assert_eq!(inv.get(2).unwrap().0.name, "Potion");
986    }
987
988    #[test]
989    fn inventory_sort_by_price() {
990        let mut inv: Inventory<MockItem, 8> = Inventory::new();
991        inv.add(
992            MockItem {
993                name: "Potion",
994                price: 300,
995                heal_amount: 20,
996            },
997            1,
998        )
999        .unwrap();
1000        inv.add(
1001            MockItem {
1002                name: "Antidote",
1003                price: 200,
1004                heal_amount: 0,
1005            },
1006            1,
1007        )
1008        .unwrap();
1009        inv.add(
1010            MockItem {
1011                name: "Elixir",
1012                price: 500,
1013                heal_amount: 0,
1014            },
1015            1,
1016        )
1017        .unwrap();
1018
1019        inv.sort_by(|a, b| a.0.price.cmp(&b.0.price));
1020        assert_eq!(inv.get(0).unwrap().0.name, "Antidote");
1021        assert_eq!(inv.get(1).unwrap().0.name, "Potion");
1022        assert_eq!(inv.get(2).unwrap().0.name, "Elixir");
1023    }
1024
1025    #[test]
1026    fn inventory_into_inner() {
1027        let mut inv: Inventory<MockItem, 8> = Inventory::new();
1028        let potion = MockItem {
1029            name: "Potion",
1030            price: 300,
1031            heal_amount: 20,
1032        };
1033        inv.add(potion, 3).unwrap();
1034
1035        let inner = inv.into_inner();
1036        assert_eq!(inner.len(), 1);
1037        assert_eq!(inner[0].0.name, "Potion");
1038        assert_eq!(inner[0].1, 3);
1039    }
1040
1041    #[test]
1042    fn inventory_iter() {
1043        let mut inv: Inventory<MockItem, 8> = Inventory::new();
1044        inv.add(
1045            MockItem {
1046                name: "Potion",
1047                price: 300,
1048                heal_amount: 20,
1049            },
1050            1,
1051        )
1052        .unwrap();
1053        inv.add(
1054            MockItem {
1055                name: "Elixir",
1056                price: 500,
1057                heal_amount: 0,
1058            },
1059            1,
1060        )
1061        .unwrap();
1062
1063        let names: Vec<&str> = inv.iter().map(|(i, _)| i.name).collect();
1064        assert_eq!(names, vec!["Potion", "Elixir"]);
1065    }
1066
1067    #[test]
1068    fn inventory_simple_inventory_alias() {
1069        let mut inv: SimpleInventory<MockItem> = SimpleInventory::new();
1070        let potion = MockItem {
1071            name: "Potion",
1072            price: 300,
1073            heal_amount: 20,
1074        };
1075        inv.add(potion, 1).unwrap();
1076        assert_eq!(inv.count(), 1);
1077    }
1078
1079    #[test]
1080    fn inventory_is_full_false_when_under_cap() {
1081        let mut inv = Inventory::<MockItem, 3>::with_capacity(99);
1082        let potion = MockItem {
1083            name: "Potion",
1084            price: 300,
1085            heal_amount: 20,
1086        };
1087        assert!(!inv.is_full());
1088        inv.add(potion, 1).unwrap();
1089        assert!(!inv.is_full());
1090    }
1091
1092    #[test]
1093    fn inventory_is_full_true_at_cap() {
1094        let mut inv = Inventory::<MockItem, 2>::with_capacity(99);
1095        let potion = MockItem {
1096            name: "Potion",
1097            price: 300,
1098            heal_amount: 20,
1099        };
1100        let elixir = MockItem {
1101            name: "Elixir",
1102            price: 500,
1103            heal_amount: 0,
1104        };
1105        inv.add(potion, 1).unwrap();
1106        inv.add(elixir, 1).unwrap();
1107        assert!(inv.is_full());
1108    }
1109
1110    #[test]
1111    fn inventory_would_exceed_per_slot_cap() {
1112        let mut inv = Inventory::<MockItem, 10>::with_capacity(5);
1113        let potion = MockItem {
1114            name: "Potion",
1115            price: 300,
1116            heal_amount: 20,
1117        };
1118        assert!(!inv.would_exceed_per_slot_cap(&potion, 5)); // OK, at cap
1119        assert!(inv.would_exceed_per_slot_cap(&potion, 6)); // exceeds
1120        inv.add(potion, 3).unwrap();
1121        assert!(!inv.would_exceed_per_slot_cap(&potion, 2)); // 3+2=5, at cap
1122        assert!(inv.would_exceed_per_slot_cap(&potion, 3)); // 3+3=6, exceeds
1123    }
1124}