use std::cmp::Ordering;
use std::fmt::Debug;
use std::hash::Hash;
pub mod equip;
pub mod kind;
pub mod use_driver;
pub use use_driver::{
buy, sell, use_item, ItemUseResult, ShopError, ShopReceipt, UsageContext,
};
pub use kind::ItemKind;
pub use equip::{EquipProvider, EquipSlot};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ItemResult {
Used,
NotUsable,
NotOwned,
NoEffect,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BagCategory {
Items,
Medicine,
Balls,
Battle,
Key,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AddError {
InventoryFull,
PerSlotCapReached(u32),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Inventory<I: Copy + Eq + Hash + Debug, const N: usize> {
items: [Option<(I, u32)>; N],
len: usize,
max_per_slot: Option<u32>,
}
pub type SimpleInventory<I> = Inventory<I, 256>;
impl<I: Copy + Eq + Hash + Debug, const N: usize> Inventory<I, N> {
pub fn new() -> Self {
Self {
items: [None; N],
len: 0,
max_per_slot: None,
}
}
pub fn with_capacity(max_per_slot: u32) -> Self {
Self {
items: [None; N],
len: 0,
max_per_slot: Some(max_per_slot),
}
}
pub fn count(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn capacity(&self) -> usize {
N
}
pub fn contains(&self, item: &I, quantity: u32) -> bool {
self.iter().any(|(i, q)| i == item && *q >= quantity)
}
pub fn add(&mut self, item: I, quantity: u32) -> Result<(), AddError> {
if quantity == 0 {
return Ok(());
}
if self.would_exceed_per_slot_cap(&item, quantity) {
return Err(AddError::PerSlotCapReached(self.max_per_slot.unwrap()));
}
let exists = self.iter().any(|(i, _)| *i == item);
if !exists && self.is_full() {
return Err(AddError::InventoryFull);
}
for slot in &mut self.items[..self.len] {
if let Some((existing, qty)) = slot {
if *existing == item {
*qty = qty.saturating_add(quantity);
return Ok(());
}
}
}
self.items[self.len] = Some((item, quantity));
self.len += 1;
Ok(())
}
pub fn remove(&mut self, item: &I, quantity: u32) -> bool {
for i in 0..self.len {
if let Some((existing, qty)) = &mut self.items[i] {
if existing == item {
if *qty < quantity {
return false;
}
if *qty == quantity {
self.remove_at(i);
} else {
*qty -= quantity;
}
return true;
}
}
}
false
}
pub fn quantity(&self, item: &I) -> u32 {
self.iter()
.find(|(i, _)| i == item)
.map(|(_, q)| *q)
.unwrap_or(0)
}
pub fn is_full(&self) -> bool {
self.len >= N
}
pub fn would_exceed_per_slot_cap(&self, item: &I, add_quantity: u32) -> bool {
let Some(cap) = self.max_per_slot else {
return false;
};
let current = self.quantity(item);
current.saturating_add(add_quantity) > cap
}
pub fn filter<F>(&self, pred: F) -> Vec<&(I, u32)>
where
F: Fn(&I) -> bool,
{
self.iter().filter(|(i, _)| pred(i)).collect()
}
pub fn sort_by<F>(&mut self, mut cmp: F)
where
F: FnMut(&(I, u32), &(I, u32)) -> Ordering,
{
self.items[..self.len].sort_by(|a, b| {
cmp(a.as_ref().unwrap(), b.as_ref().unwrap())
});
}
pub fn sort_by_name<F>(&mut self, name_fn: F)
where
F: Fn(&I) -> &str,
{
self.items[..self.len].sort_by(|a, b| {
name_fn(&a.as_ref().unwrap().0).cmp(name_fn(&b.as_ref().unwrap().0))
});
}
pub fn into_inner(self) -> Vec<(I, u32)> {
self.items.iter().flatten().copied().collect()
}
pub fn iter(&self) -> impl Iterator<Item = &(I, u32)> {
self.items[..self.len].iter().filter_map(Option::as_ref)
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut (I, u32)> {
self.items[..self.len].iter_mut().filter_map(Option::as_mut)
}
pub fn get(&self, index: usize) -> Option<&(I, u32)> {
self.items.get(index).and_then(Option::as_ref)
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut (I, u32)> {
self.items.get_mut(index).and_then(Option::as_mut)
}
pub fn push_slot(&mut self, item: I, quantity: u32) -> Result<(), AddError> {
if self.is_full() {
return Err(AddError::InventoryFull);
}
self.items[self.len] = Some((item, quantity));
self.len += 1;
Ok(())
}
pub fn remove_at(&mut self, index: usize) {
assert!(index < self.len, "inventory slot index out of bounds");
self.items.copy_within(index + 1..self.len, index);
self.items[self.len - 1] = None;
self.len -= 1;
}
pub fn swap(&mut self, a: usize, b: usize) {
self.items.swap(a, b);
}
pub fn clear(&mut self) {
self.items.fill(None);
self.len = 0;
}
}
impl<I: Copy + Eq + Hash + Debug, const N: usize> Default for Inventory<I, N> {
fn default() -> Self {
Self::new()
}
}
pub trait ItemProvider {
type Item: Copy + Eq + Hash + Debug;
type Effect;
type Monster;
type CustomKind: Copy + Eq + Hash + Debug;
fn item_name(&self, item: &Self::Item) -> &str;
fn item_description(&self, item: &Self::Item) -> &str;
fn item_effect(&self, item: &Self::Item) -> Self::Effect;
fn item_price(&self, item: &Self::Item) -> u32;
fn can_use_outside_battle(&self, item: &Self::Item) -> bool;
fn can_use_in_battle(&self, item: &Self::Item) -> bool;
fn use_on_monster(&self, item: &Self::Item, monster: &mut Self::Monster) -> ItemResult;
fn consume(&self, item: &Self::Item) -> bool;
fn item_kind(&self, item: &Self::Item) -> ItemKind<Self::CustomKind>;
fn on_teach_move<M: crate::party::MonsterProvider>(
&self,
item: Self::Item,
target: &mut crate::party::MonsterInstance<M>,
) -> Option<ItemUseResult<Self::Item>> {
let _ = (item, target);
None
}
fn on_use_field(&self, item: Self::Item) -> Option<ItemUseResult<Self::Item>> {
let _ = item;
None
}
fn usable_in(&self, item: &Self::Item) -> UsageContext {
let _ = item;
UsageContext::FieldAndBattle
}
fn apply_effect<M: crate::party::MonsterProvider>(
&self,
provider: &M,
item: Self::Item,
ctx: UsageContext,
target: Option<&mut crate::party::MonsterInstance<M>>,
rng: &mut dyn crate::battle::rng::BattleRng,
) -> ItemUseResult<Self::Item> {
let _ = (provider, item, ctx, target, rng);
ItemUseResult::NoEffect
}
}
pub trait ShopProvider {
type Item: Copy + Eq + Hash + Debug;
type ShopId: Copy + Eq + Hash + Debug;
fn shop_inventory(&self, shop_id: &Self::ShopId) -> Vec<(Self::Item, u32)>;
fn shop_name(&self, shop_id: &Self::ShopId) -> &str;
fn buy_price(&self, item: &Self::Item) -> u32 {
let _ = item;
0
}
fn sell_price(&self, item: &Self::Item) -> u32 {
self.buy_price(item) / 2
}
fn can_sell(&self, item: &Self::Item) -> bool {
let _ = item;
true
}
fn discount_rate(&self, _shop_id: &Self::ShopId) -> f32 {
1.0
}
fn sell_rate(&self, _shop_id: &Self::ShopId) -> f32 {
1.0
}
fn has_limited_stock(&self, _item: &Self::Item) -> bool {
false
}
fn max_stock(&self, _item: &Self::Item) -> u32 {
0
}
fn restocks(&self, _shop_id: &Self::ShopId) -> bool {
false
}
fn restock_interval(&self, _shop_id: &Self::ShopId) -> u32 {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct MockItem {
name: &'static str,
price: u32,
heal_amount: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MockEffect {
Heal(u32),
None,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct MockMonster {
name: &'static str,
max_hp: u32,
current_hp: u32,
}
struct MockItemProvider;
impl ItemProvider for MockItemProvider {
type Item = MockItem;
type Effect = MockEffect;
type Monster = MockMonster;
type CustomKind = ();
fn item_name(&self, item: &Self::Item) -> &str {
item.name
}
fn item_description(&self, item: &Self::Item) -> &str {
if item.heal_amount > 0 {
"Restores HP."
} else {
"Has no effect in battle."
}
}
fn item_effect(&self, item: &Self::Item) -> Self::Effect {
if item.heal_amount > 0 {
MockEffect::Heal(item.heal_amount)
} else {
MockEffect::None
}
}
fn item_price(&self, item: &Self::Item) -> u32 {
item.price
}
fn can_use_outside_battle(&self, _item: &Self::Item) -> bool {
true
}
fn can_use_in_battle(&self, _item: &Self::Item) -> bool {
true
}
fn use_on_monster(&self, item: &Self::Item, monster: &mut Self::Monster) -> ItemResult {
match self.item_effect(item) {
MockEffect::Heal(amount) => {
if monster.current_hp >= monster.max_hp {
return ItemResult::NoEffect;
}
monster.current_hp = (monster.current_hp + amount).min(monster.max_hp);
ItemResult::Used
}
MockEffect::None => ItemResult::NoEffect,
}
}
fn consume(&self, _item: &Self::Item) -> bool {
true
}
fn item_kind(&self, item: &Self::Item) -> ItemKind<()> {
if item.heal_amount > 0 {
ItemKind::Consumable
} else {
ItemKind::Consumable
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum MockShopId {
CityMart,
}
struct MockShopProvider;
impl ShopProvider for MockShopProvider {
type Item = MockItem;
type ShopId = MockShopId;
fn shop_inventory(&self, shop_id: &Self::ShopId) -> Vec<(Self::Item, u32)> {
match shop_id {
MockShopId::CityMart => vec![
(
MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
},
300,
),
(
MockItem {
name: "Elixir",
price: 500,
heal_amount: 0,
},
500,
),
],
}
}
fn shop_name(&self, shop_id: &Self::ShopId) -> &str {
match shop_id {
MockShopId::CityMart => "City Mart",
}
}
}
#[test]
fn potion_heals_monster() {
let provider = MockItemProvider;
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
let mut monster = MockMonster {
name: "Sprout",
max_hp: 100,
current_hp: 50,
};
let result = provider.use_on_monster(&potion, &mut monster);
assert_eq!(result, ItemResult::Used);
assert_eq!(monster.current_hp, 70);
assert!(provider.consume(&potion));
}
#[test]
fn potion_no_effect_on_full_hp() {
let provider = MockItemProvider;
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
let mut monster = MockMonster {
name: "Sprout",
max_hp: 100,
current_hp: 100,
};
let result = provider.use_on_monster(&potion, &mut monster);
assert_eq!(result, ItemResult::NoEffect);
assert_eq!(monster.current_hp, 100);
}
#[test]
fn elixir_has_no_heal_effect() {
let provider = MockItemProvider;
let elixir = MockItem {
name: "Elixir",
price: 500,
heal_amount: 0,
};
let mut monster = MockMonster {
name: "Sprout",
max_hp: 100,
current_hp: 50,
};
let result = provider.use_on_monster(&elixir, &mut monster);
assert_eq!(result, ItemResult::NoEffect);
assert_eq!(monster.current_hp, 50); }
#[test]
fn shop_inventory_has_two_items() {
let provider = MockShopProvider;
let inventory = provider.shop_inventory(&MockShopId::CityMart);
assert_eq!(inventory.len(), 2);
assert_eq!(inventory[0].0.name, "Potion");
assert_eq!(inventory[0].1, 300);
assert_eq!(inventory[1].0.name, "Elixir");
assert_eq!(inventory[1].1, 500);
}
#[test]
fn shop_name_is_correct() {
let provider = MockShopProvider;
assert_eq!(
provider.shop_name(&MockShopId::CityMart),
"City Mart"
);
}
#[test]
fn inventory_add_and_remove() {
let mut inv: Inventory<MockItem, 8> = Inventory::new();
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
inv.add(potion, 3).unwrap();
assert_eq!(inv.count(), 1);
assert!(inv.contains(&potion, 2));
assert!(!inv.contains(&potion, 4));
assert!(inv.remove(&potion, 2));
assert_eq!(inv.count(), 1);
assert!(inv.contains(&potion, 1));
assert!(inv.remove(&potion, 1));
assert_eq!(inv.count(), 0);
assert!(!inv.contains(&potion, 1));
}
#[test]
fn inventory_stacks_same_item() {
let mut inv: Inventory<MockItem, 8> = Inventory::new();
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
inv.add(potion, 3).unwrap();
inv.add(potion, 5).unwrap();
assert_eq!(inv.count(), 1); assert!(inv.contains(&potion, 8));
}
#[test]
fn inventory_remove_insufficient_quantity() {
let mut inv: Inventory<MockItem, 8> = Inventory::new();
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
inv.add(potion, 2).unwrap();
assert!(!inv.remove(&potion, 5));
assert_eq!(inv.count(), 1);
assert!(inv.contains(&potion, 2)); }
#[test]
fn inventory_remove_nonexistent_item() {
let mut inv: Inventory<MockItem, 8> = Inventory::new();
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
assert!(!inv.remove(&potion, 1));
}
#[test]
fn inventory_new_is_unlimited() {
let inv: Inventory<MockItem, 8> = Inventory::new();
assert!(!inv.is_full());
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
assert!(!inv.would_exceed_per_slot_cap(&potion, u32::MAX));
}
#[test]
fn inventory_with_capacity_rejects_overfill() {
let mut inv = Inventory::<MockItem, 2>::with_capacity(10);
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
let elixir = MockItem {
name: "Elixir",
price: 500,
heal_amount: 0,
};
let antidote = MockItem {
name: "Antidote",
price: 200,
heal_amount: 0,
};
assert!(inv.add(potion, 1).is_ok());
assert!(inv.add(elixir, 1).is_ok());
assert_eq!(inv.add(antidote, 1), Err(AddError::InventoryFull));
}
#[test]
fn inventory_with_capacity_rejects_per_slot_overflow() {
let mut inv = Inventory::<MockItem, 10>::with_capacity(5);
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
assert!(inv.add(potion, 3).is_ok());
assert!(inv.add(potion, 2).is_ok()); assert_eq!(inv.add(potion, 1), Err(AddError::PerSlotCapReached(5)));
}
#[test]
fn inventory_quantity() {
let mut inv: Inventory<MockItem, 8> = Inventory::new();
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
assert_eq!(inv.quantity(&potion), 0);
inv.add(potion, 3).unwrap();
assert_eq!(inv.quantity(&potion), 3);
}
#[test]
fn inventory_add_zero_is_ok() {
let mut inv: Inventory<MockItem, 8> = Inventory::new();
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
assert!(inv.add(potion, 0).is_ok());
assert_eq!(inv.count(), 0);
}
#[test]
fn inventory_filter() {
let mut inv: Inventory<MockItem, 8> = Inventory::new();
inv.add(
MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
},
1,
)
.unwrap();
inv.add(
MockItem {
name: "Elixir",
price: 500,
heal_amount: 0,
},
1,
)
.unwrap();
inv.add(
MockItem {
name: "Antidote",
price: 200,
heal_amount: 0,
},
1,
)
.unwrap();
let cheap = inv.filter(|i| i.price < 350);
assert_eq!(cheap.len(), 2); }
#[test]
fn inventory_sort_by_name() {
let mut inv: Inventory<MockItem, 8> = Inventory::new();
let antidote = MockItem {
name: "Antidote",
price: 200,
heal_amount: 0,
};
let elixir = MockItem {
name: "Elixir",
price: 500,
heal_amount: 0,
};
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
inv.add(elixir, 1).unwrap();
inv.add(antidote, 1).unwrap();
inv.add(potion, 1).unwrap();
inv.sort_by_name(|i| i.name);
assert_eq!(inv.get(0).unwrap().0.name, "Antidote");
assert_eq!(inv.get(1).unwrap().0.name, "Elixir");
assert_eq!(inv.get(2).unwrap().0.name, "Potion");
}
#[test]
fn inventory_sort_by_price() {
let mut inv: Inventory<MockItem, 8> = Inventory::new();
inv.add(
MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
},
1,
)
.unwrap();
inv.add(
MockItem {
name: "Antidote",
price: 200,
heal_amount: 0,
},
1,
)
.unwrap();
inv.add(
MockItem {
name: "Elixir",
price: 500,
heal_amount: 0,
},
1,
)
.unwrap();
inv.sort_by(|a, b| a.0.price.cmp(&b.0.price));
assert_eq!(inv.get(0).unwrap().0.name, "Antidote");
assert_eq!(inv.get(1).unwrap().0.name, "Potion");
assert_eq!(inv.get(2).unwrap().0.name, "Elixir");
}
#[test]
fn inventory_into_inner() {
let mut inv: Inventory<MockItem, 8> = Inventory::new();
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
inv.add(potion, 3).unwrap();
let inner = inv.into_inner();
assert_eq!(inner.len(), 1);
assert_eq!(inner[0].0.name, "Potion");
assert_eq!(inner[0].1, 3);
}
#[test]
fn inventory_iter() {
let mut inv: Inventory<MockItem, 8> = Inventory::new();
inv.add(
MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
},
1,
)
.unwrap();
inv.add(
MockItem {
name: "Elixir",
price: 500,
heal_amount: 0,
},
1,
)
.unwrap();
let names: Vec<&str> = inv.iter().map(|(i, _)| i.name).collect();
assert_eq!(names, vec!["Potion", "Elixir"]);
}
#[test]
fn inventory_simple_inventory_alias() {
let mut inv: SimpleInventory<MockItem> = SimpleInventory::new();
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
inv.add(potion, 1).unwrap();
assert_eq!(inv.count(), 1);
}
#[test]
fn inventory_is_full_false_when_under_cap() {
let mut inv = Inventory::<MockItem, 3>::with_capacity(99);
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
assert!(!inv.is_full());
inv.add(potion, 1).unwrap();
assert!(!inv.is_full());
}
#[test]
fn inventory_is_full_true_at_cap() {
let mut inv = Inventory::<MockItem, 2>::with_capacity(99);
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
let elixir = MockItem {
name: "Elixir",
price: 500,
heal_amount: 0,
};
inv.add(potion, 1).unwrap();
inv.add(elixir, 1).unwrap();
assert!(inv.is_full());
}
#[test]
fn inventory_would_exceed_per_slot_cap() {
let mut inv = Inventory::<MockItem, 10>::with_capacity(5);
let potion = MockItem {
name: "Potion",
price: 300,
heal_amount: 20,
};
assert!(!inv.would_exceed_per_slot_cap(&potion, 5)); assert!(inv.would_exceed_per_slot_cap(&potion, 6)); inv.add(potion, 3).unwrap();
assert!(!inv.would_exceed_per_slot_cap(&potion, 2)); assert!(inv.would_exceed_per_slot_cap(&potion, 3)); }
}