use std::fmt::Debug;
use std::hash::Hash;
use super::{Inventory, ItemKind, ItemProvider, ShopProvider};
use crate::battle::rng::BattleRng;
use crate::party::{MonsterInstance, MonsterProvider};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UsageContext {
FieldOnly,
BattleOnly,
FieldAndBattle,
None,
}
impl UsageContext {
fn allows(self, eligibility: UsageContext) -> bool {
match eligibility {
UsageContext::None => false,
UsageContext::FieldAndBattle => !matches!(self, UsageContext::None),
UsageContext::FieldOnly => {
matches!(self, UsageContext::FieldOnly | UsageContext::FieldAndBattle)
}
UsageContext::BattleOnly => {
matches!(self, UsageContext::BattleOnly | UsageContext::FieldAndBattle)
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ItemUseResult<I: Copy + Eq + Hash + Debug> {
Applied {
consume: bool,
message_key: Option<String>,
},
NoEffect,
Caught,
Failed,
EvolutionTriggered {
item: I,
message_key: Option<String>,
},
MoveLearned {
consume: bool,
message_key: Option<String>,
},
}
impl<I: Copy + Eq + Hash + Debug> ItemUseResult<I> {
pub fn consumes(&self) -> bool {
match self {
ItemUseResult::Applied { consume, .. } => *consume,
ItemUseResult::Caught => true,
ItemUseResult::MoveLearned { consume, .. } => *consume,
ItemUseResult::EvolutionTriggered { .. } => false,
ItemUseResult::NoEffect | ItemUseResult::Failed => false,
}
}
}
pub fn use_item<const N: usize, I, M>(
provider: &I,
monster_provider: &M,
inv: &mut Inventory<I::Item, N>,
item: I::Item,
ctx: UsageContext,
target: Option<&mut MonsterInstance<M>>,
rng: &mut dyn BattleRng,
) -> ItemUseResult<I::Item>
where
I: ItemProvider,
M: MonsterProvider,
{
if !inv.contains(&item, 1) {
return ItemUseResult::Failed;
}
if !ctx.allows(provider.usable_in(&item)) {
return ItemUseResult::Failed;
}
let kind = provider.item_kind(&item);
let result = match kind {
ItemKind::TeachMove => {
if let Some(target) = target {
provider
.on_teach_move(item, target)
.unwrap_or(ItemUseResult::NoEffect)
} else {
ItemUseResult::NoEffect
}
}
ItemKind::KeyItem | ItemKind::Currency => provider
.on_use_field(item)
.unwrap_or(ItemUseResult::NoEffect),
_ => provider.apply_effect(monster_provider, item, ctx, target, rng),
};
if result.consumes() {
inv.remove(&item, 1);
}
result
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShopError {
NotEnoughMoney,
InventoryFull,
CannotSell,
InvalidQuantity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShopReceipt {
pub total: u32,
pub money_after: u32,
}
pub fn buy<const N: usize, S>(
provider: &S,
shop_id: &S::ShopId,
inv: &mut Inventory<S::Item, N>,
money: &mut u32,
item: S::Item,
quantity: u32,
) -> Result<ShopReceipt, ShopError>
where
S: ShopProvider,
{
if quantity == 0 {
return Err(ShopError::InvalidQuantity);
}
let unit_price = provider.buy_price(&item);
let discount = provider.discount_rate(shop_id);
let effective_price = (unit_price as f32 * discount) as u32;
let total = effective_price.saturating_mul(quantity);
if *money < total {
return Err(ShopError::NotEnoughMoney);
}
if inv.add(item, quantity).is_err() {
return Err(ShopError::InventoryFull);
}
*money -= total;
Ok(ShopReceipt {
total,
money_after: *money,
})
}
pub fn sell<const N: usize, S>(
provider: &S,
shop_id: &S::ShopId,
inv: &mut Inventory<S::Item, N>,
money: &mut u32,
item: S::Item,
quantity: u32,
) -> Result<ShopReceipt, ShopError>
where
S: ShopProvider,
{
if quantity == 0 {
return Err(ShopError::InvalidQuantity);
}
if !provider.can_sell(&item) || !inv.contains(&item, quantity) {
return Err(ShopError::CannotSell);
}
if !inv.remove(&item, quantity) {
return Err(ShopError::CannotSell);
}
let base_sell = provider.sell_price(&item);
let rate = provider.sell_rate(shop_id);
let effective_sell = (base_sell as f32 * rate) as u32;
let total = effective_sell.saturating_mul(quantity);
*money = money.saturating_add(total);
Ok(ShopReceipt {
total,
money_after: *money,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::items::{BagCategory, ItemKind, ItemResult};
use crate::party::{MonsterInstance, MonsterStatus, MoveSlot, StatSet};
struct SeqRng {
seq: Vec<u8>,
idx: usize,
}
impl SeqRng {
fn new(seq: &[u8]) -> Self {
Self {
seq: seq.to_vec(),
idx: 0,
}
}
}
impl BattleRng for SeqRng {
fn next_u8(&mut self) -> u8 {
let v = self.seq[self.idx % self.seq.len()];
self.idx += 1;
v
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Stat {
Hp,
}
#[derive(Debug, Clone, Copy, Default)]
struct MockMon;
impl MonsterProvider for MockMon {
type SpeciesId = u8;
type MoveId = u8;
type Genetics = ();
type Training = ();
type Stat = Stat;
fn base_stat(&self, _s: u8, _st: Stat) -> u16 {
50
}
fn calc_stat(&self, _s: u8, _st: Stat, _l: u8, _g: &(), _t: &()) -> u16 {
50
}
fn stats(&self) -> &[Stat] {
&[Stat::Hp]
}
fn hp_stat(&self) -> Stat {
Stat::Hp
}
fn max_moves(&self) -> usize {
4
}
}
fn mon(current_hp: u16, status: MonsterStatus, pp: u8) -> MonsterInstance<MockMon> {
let provider = MockMon;
let mut stats = StatSet::zeroed(&provider);
stats.set(Stat::Hp, 50);
MonsterInstance {
species: 1,
level: 5,
exp: 0,
genetics: (),
training: (),
stats,
current_hp,
status,
moves: vec![MoveSlot {
move_id: 0,
pp,
pp_up: 0,
}],
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum Item {
Potion,
Antidote,
Ball,
XAttack,
Bicycle, KeyStone, FireStone, }
struct Game;
impl ItemProvider for Game {
type Item = Item;
type Effect = ();
type Monster = ();
type CustomKind = ();
fn item_name(&self, _item: &Item) -> &str {
"X"
}
fn item_description(&self, _item: &Item) -> &str {
"X"
}
fn item_effect(&self, _item: &Item) {}
fn item_price(&self, item: &Item) -> u32 {
match item {
Item::Potion => 300,
Item::Antidote => 100,
Item::Ball => 200,
Item::XAttack => 500,
Item::Bicycle => 0,
Item::KeyStone => 0,
Item::FireStone => 2100,
}
}
fn can_use_outside_battle(&self, item: &Item) -> bool {
!matches!(item, Item::Ball | Item::XAttack | Item::Bicycle)
}
fn can_use_in_battle(&self, item: &Item) -> bool {
!matches!(item, Item::Bicycle | Item::KeyStone)
}
fn use_on_monster(&self, _item: &Item, _m: &mut ()) -> ItemResult {
ItemResult::NoEffect
}
fn consume(&self, item: &Item) -> bool {
!matches!(item, Item::Bicycle | Item::KeyStone)
}
fn item_kind(&self, item: &Item) -> ItemKind<()> {
match item {
Item::Bicycle | Item::KeyStone => ItemKind::KeyItem,
Item::FireStone => ItemKind::Evolution,
_ => ItemKind::Consumable,
}
}
fn usable_in(&self, item: &Item) -> UsageContext {
match item {
Item::Potion | Item::Antidote => UsageContext::FieldAndBattle,
Item::Ball | Item::XAttack => UsageContext::BattleOnly,
Item::Bicycle => UsageContext::None,
Item::KeyStone | Item::FireStone => UsageContext::FieldOnly,
}
}
fn apply_effect<M: MonsterProvider>(
&self,
provider: &M,
item: Item,
_ctx: UsageContext,
target: Option<&mut MonsterInstance<M>>,
rng: &mut dyn BattleRng,
) -> ItemUseResult<Item> {
let _ = provider;
match item {
Item::Potion => match target {
Some(m) if m.current_hp < 50 => {
m.current_hp = (m.current_hp + 20).min(50);
ItemUseResult::Applied {
consume: true,
message_key: None,
}
}
_ => ItemUseResult::NoEffect,
},
Item::Antidote => match target {
Some(m) if m.status == MonsterStatus::Poison => {
m.status = MonsterStatus::Healthy;
ItemUseResult::Applied {
consume: true,
message_key: Some("cured".to_string()),
}
}
_ => ItemUseResult::NoEffect,
},
Item::Ball => {
if rng.next_u8() % 2 == 0 {
ItemUseResult::Caught
} else {
ItemUseResult::Failed
}
}
Item::XAttack => ItemUseResult::Applied {
consume: true,
message_key: None,
},
Item::Bicycle => ItemUseResult::NoEffect,
Item::KeyStone => ItemUseResult::Applied {
consume: true,
message_key: Some("apply_effect_called".to_string()),
},
Item::FireStone => ItemUseResult::EvolutionTriggered {
item,
message_key: Some("evolve?".to_string()),
},
}
}
fn on_use_field(&self, item: Item) -> Option<ItemUseResult<Item>> {
match item {
Item::KeyStone => Some(ItemUseResult::Applied {
consume: false,
message_key: Some("field_used".to_string()),
}),
_ => None,
}
}
}
impl ShopProvider for Game {
type Item = Item;
type ShopId = u8;
fn shop_inventory(&self, _shop_id: &u8) -> Vec<(Item, u32)> {
vec![(Item::Potion, 300)]
}
fn shop_name(&self, _shop_id: &u8) -> &str {
"Mart"
}
fn buy_price(&self, item: &Item) -> u32 {
self.item_price(item)
}
fn can_sell(&self, item: &Item) -> bool {
!matches!(item, Item::Bicycle | Item::KeyStone)
}
}
fn stock(item: Item, qty: u32) -> Inventory<Item, 64> {
let mut inv = Inventory::<Item, 64>::new();
inv.add(item, qty).unwrap();
inv
}
#[test]
fn use_item_routes_to_apply_effect_and_consumes_on_applied() {
let game = Game;
let mut inv = stock(Item::Potion, 3);
let mut m = mon(10, MonsterStatus::Healthy, 10);
let mut rng = SeqRng::new(&[0]);
let r = use_item(
&game,
&MockMon,
&mut inv,
Item::Potion,
UsageContext::FieldOnly,
Some(&mut m),
&mut rng,
);
assert!(matches!(r, ItemUseResult::Applied { consume: true, .. }));
assert_eq!(m.current_hp, 30); assert!(inv.contains(&Item::Potion, 2)); assert!(!inv.contains(&Item::Potion, 3));
}
#[test]
fn use_item_no_effect_does_not_consume() {
let game = Game;
let mut inv = stock(Item::Potion, 3);
let mut m = mon(50, MonsterStatus::Healthy, 10); let mut rng = SeqRng::new(&[0]);
let r = use_item(
&game,
&MockMon,
&mut inv,
Item::Potion,
UsageContext::FieldOnly,
Some(&mut m),
&mut rng,
);
assert_eq!(r, ItemUseResult::NoEffect);
assert!(inv.contains(&Item::Potion, 3)); }
#[test]
fn use_item_rejects_not_owned_without_touching_target() {
let game = Game;
let mut inv: Inventory<Item, 64> = Inventory::new(); let mut m = mon(10, MonsterStatus::Poison, 10);
let mut rng = SeqRng::new(&[0]);
let r = use_item(
&game,
&MockMon,
&mut inv,
Item::Antidote,
UsageContext::FieldOnly,
Some(&mut m),
&mut rng,
);
assert_eq!(r, ItemUseResult::Failed);
assert_eq!(m.status, MonsterStatus::Poison); }
#[test]
fn use_item_rejects_wrong_context() {
let game = Game;
let mut inv = stock(Item::XAttack, 5); let mut rng = SeqRng::new(&[0]);
let r = use_item(
&game,
&MockMon,
&mut inv,
Item::XAttack,
UsageContext::FieldOnly, None,
&mut rng,
);
assert_eq!(r, ItemUseResult::Failed);
assert!(inv.contains(&Item::XAttack, 5)); }
#[test]
fn use_item_caught_consumes_ball() {
let game = Game;
let mut inv = stock(Item::Ball, 5);
let mut rng = SeqRng::new(&[0]); let r = use_item(
&game,
&MockMon,
&mut inv,
Item::Ball,
UsageContext::BattleOnly,
None,
&mut rng,
);
assert_eq!(r, ItemUseResult::Caught);
assert!(inv.contains(&Item::Ball, 4)); }
#[test]
fn use_item_failed_ball_not_consumed() {
let game = Game;
let mut inv = stock(Item::Ball, 5);
let mut rng = SeqRng::new(&[1]); let r = use_item(
&game,
&MockMon,
&mut inv,
Item::Ball,
UsageContext::BattleOnly,
None,
&mut rng,
);
assert_eq!(r, ItemUseResult::Failed);
assert!(inv.contains(&Item::Ball, 5)); }
#[test]
fn use_item_status_cure_consumes() {
let game = Game;
let mut inv = stock(Item::Antidote, 1);
let mut m = mon(20, MonsterStatus::Poison, 10);
let mut rng = SeqRng::new(&[0]);
let r = use_item(
&game,
&MockMon,
&mut inv,
Item::Antidote,
UsageContext::FieldOnly,
Some(&mut m),
&mut rng,
);
assert!(matches!(r, ItemUseResult::Applied { consume: true, .. }));
assert_eq!(m.status, MonsterStatus::Healthy);
assert!(!inv.contains(&Item::Antidote, 1));
}
#[test]
fn use_item_default_apply_effect_is_no_effect() {
struct Plain;
impl ItemProvider for Plain {
type Item = u8;
type Effect = ();
type Monster = ();
type CustomKind = ();
fn item_name(&self, _i: &u8) -> &str {
"X"
}
fn item_description(&self, _i: &u8) -> &str {
"X"
}
fn item_effect(&self, _i: &u8) {}
fn item_price(&self, _i: &u8) -> u32 {
0
}
fn can_use_outside_battle(&self, _i: &u8) -> bool {
true
}
fn can_use_in_battle(&self, _i: &u8) -> bool {
true
}
fn use_on_monster(&self, _i: &u8, _m: &mut ()) -> ItemResult {
ItemResult::NoEffect
}
fn consume(&self, _i: &u8) -> bool {
true
}
fn item_kind(&self, _item: &u8) -> ItemKind<()> {
ItemKind::Consumable
}
}
let game = Plain;
let mut inv = Inventory::<u8, 64>::new();
inv.add(7u8, 2).unwrap();
let mut m = mon(10, MonsterStatus::Healthy, 10);
let mut rng = SeqRng::new(&[0]);
let r = use_item(
&game,
&MockMon,
&mut inv,
7u8,
UsageContext::FieldOnly,
Some(&mut m),
&mut rng,
);
assert_eq!(r, ItemUseResult::NoEffect);
assert!(inv.contains(&7u8, 2)); assert_eq!(m.current_hp, 10); }
#[test]
fn buy_deducts_money_and_adds_item() {
let game = Game;
let mut inv = Inventory::<Item, 64>::new();
let mut money = 1000u32;
let r = buy(&game, &0u8, &mut inv, &mut money, Item::Potion, 2).unwrap();
assert_eq!(r.total, 600); assert_eq!(money, 400);
assert!(inv.contains(&Item::Potion, 2));
}
#[test]
fn buy_fails_when_broke_and_changes_nothing() {
let game = Game;
let mut inv = Inventory::<Item, 64>::new();
let mut money = 100u32;
let err = buy(&game, &0u8, &mut inv, &mut money, Item::Potion, 1).unwrap_err();
assert_eq!(err, ShopError::NotEnoughMoney);
assert_eq!(money, 100); assert!(!inv.contains(&Item::Potion, 1)); }
#[test]
fn buy_fails_when_inventory_full_and_keeps_money() {
let game = Game;
let mut inv = Inventory::<Item, 1>::with_capacity(99);
inv.add(Item::Antidote, 1).unwrap();
let mut money = 1000u32;
let err = buy(&game, &0u8, &mut inv, &mut money, Item::Potion, 1).unwrap_err();
assert_eq!(err, ShopError::InventoryFull);
assert_eq!(money, 1000); assert!(!inv.contains(&Item::Potion, 1)); }
#[test]
fn buy_fails_at_per_slot_cap_and_keeps_money() {
let game = Game;
let mut inv = Inventory::<Item, 20>::with_capacity(99);
inv.add(Item::Potion, 99).unwrap(); let mut money = 1000u32;
let err = buy(&game, &0u8, &mut inv, &mut money, Item::Potion, 1).unwrap_err();
assert_eq!(err, ShopError::InventoryFull);
assert_eq!(money, 1000); assert_eq!(inv.quantity(&Item::Potion), 99); }
#[test]
fn sell_adds_money_and_removes_item() {
let game = Game;
let mut inv = stock(Item::Potion, 3);
let mut money = 0u32;
let r = sell(&game, &0u8, &mut inv, &mut money, Item::Potion, 2).unwrap();
assert_eq!(r.total, 300);
assert_eq!(money, 300);
assert!(inv.contains(&Item::Potion, 1));
}
#[test]
fn sell_rate_override_scales_sell_price() {
struct Pawnshop;
impl ShopProvider for Pawnshop {
type Item = Item;
type ShopId = u8;
fn shop_inventory(&self, _shop_id: &u8) -> Vec<(Item, u32)> {
vec![]
}
fn shop_name(&self, _shop_id: &u8) -> &str {
"Pawnshop"
}
fn buy_price(&self, _item: &Item) -> u32 {
300
}
fn sell_rate(&self, _shop_id: &u8) -> f32 {
0.8
}
}
let mut inv = stock(Item::Potion, 1);
let mut money = 0u32;
let r = sell(&Pawnshop, &0u8, &mut inv, &mut money, Item::Potion, 1).unwrap();
assert_eq!(r.total, 120);
}
#[test]
fn sell_rejects_key_item_and_changes_nothing() {
let game = Game;
let mut inv = stock(Item::Bicycle, 1);
let mut money = 0u32;
let err = sell(&game, &0u8, &mut inv, &mut money, Item::Bicycle, 1).unwrap_err();
assert_eq!(err, ShopError::CannotSell);
assert_eq!(money, 0);
assert!(inv.contains(&Item::Bicycle, 1)); }
#[test]
fn sell_rejects_when_not_enough_owned() {
let game = Game;
let mut inv = stock(Item::Potion, 1);
let mut money = 0u32;
let err = sell(&game, &0u8, &mut inv, &mut money, Item::Potion, 5).unwrap_err();
assert_eq!(err, ShopError::CannotSell);
assert!(inv.contains(&Item::Potion, 1));
assert_eq!(money, 0);
}
#[test]
fn use_item_key_item_routes_to_on_use_field_not_apply_effect() {
let game = Game;
let mut inv = stock(Item::KeyStone, 1);
let mut m = mon(50, MonsterStatus::Healthy, 10);
let mut rng = SeqRng::new(&[0]);
let r = use_item(
&game,
&MockMon,
&mut inv,
Item::KeyStone,
UsageContext::FieldOnly,
Some(&mut m),
&mut rng,
);
assert_eq!(
r,
ItemUseResult::Applied {
consume: false,
message_key: Some("field_used".to_string()),
}
);
assert!(inv.contains(&Item::KeyStone, 1));
}
#[test]
fn evolution_item_dispatches_via_apply_effect_and_is_not_consumed() {
let game = Game;
let mut inv = stock(Item::FireStone, 1);
let mut m = mon(50, MonsterStatus::Healthy, 10);
let mut rng = SeqRng::new(&[0]);
let r = use_item(
&game,
&MockMon,
&mut inv,
Item::FireStone,
UsageContext::FieldOnly,
Some(&mut m),
&mut rng,
);
assert_eq!(
r,
ItemUseResult::EvolutionTriggered {
item: Item::FireStone,
message_key: Some("evolve?".to_string()),
}
);
assert!(inv.contains(&Item::FireStone, 1)); }
#[test]
fn bag_category_variants_exist() {
let _ = BagCategory::Items;
let _ = BagCategory::Medicine;
}
}