1use std::fmt::Debug;
23use std::hash::Hash;
24
25use super::{Inventory, ItemKind, ItemProvider, ShopProvider};
26use crate::battle::rng::BattleRng;
27use crate::party::{MonsterInstance, MonsterProvider};
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum UsageContext {
35 FieldOnly,
37 BattleOnly,
39 FieldAndBattle,
41 None,
43}
44
45impl UsageContext {
46 fn allows(self, eligibility: UsageContext) -> bool {
53 match eligibility {
54 UsageContext::None => false,
55 UsageContext::FieldAndBattle => !matches!(self, UsageContext::None),
56 UsageContext::FieldOnly => {
57 matches!(self, UsageContext::FieldOnly | UsageContext::FieldAndBattle)
58 }
59 UsageContext::BattleOnly => {
60 matches!(self, UsageContext::BattleOnly | UsageContext::FieldAndBattle)
61 }
62 }
63 }
64}
65
66#[derive(Clone, Debug, PartialEq, Eq)]
73pub enum ItemUseResult<I: Copy + Eq + Hash + Debug> {
74 Applied {
78 consume: bool,
80 message_key: Option<String>,
82 },
83 NoEffect,
86 Caught,
88 Failed,
91 EvolutionTriggered {
95 item: I,
98 message_key: Option<String>,
100 },
101 MoveLearned {
105 consume: bool,
107 message_key: Option<String>,
109 },
110}
111
112impl<I: Copy + Eq + Hash + Debug> ItemUseResult<I> {
113 pub fn consumes(&self) -> bool {
115 match self {
116 ItemUseResult::Applied { consume, .. } => *consume,
117 ItemUseResult::Caught => true,
118 ItemUseResult::MoveLearned { consume, .. } => *consume,
119 ItemUseResult::EvolutionTriggered { .. } => false,
120 ItemUseResult::NoEffect | ItemUseResult::Failed => false,
121 }
122 }
123}
124
125pub fn use_item<const N: usize, I, M>(
136 provider: &I,
137 monster_provider: &M,
138 inv: &mut Inventory<I::Item, N>,
139 item: I::Item,
140 ctx: UsageContext,
141 target: Option<&mut MonsterInstance<M>>,
142 rng: &mut dyn BattleRng,
143) -> ItemUseResult<I::Item>
144where
145 I: ItemProvider,
146 M: MonsterProvider,
147{
148 if !inv.contains(&item, 1) {
150 return ItemUseResult::Failed;
151 }
152 if !ctx.allows(provider.usable_in(&item)) {
154 return ItemUseResult::Failed;
155 }
156
157 let kind = provider.item_kind(&item);
167 let result = match kind {
168 ItemKind::TeachMove => {
169 if let Some(target) = target {
170 provider
171 .on_teach_move(item, target)
172 .unwrap_or(ItemUseResult::NoEffect)
173 } else {
174 ItemUseResult::NoEffect
175 }
176 }
177 ItemKind::KeyItem | ItemKind::Currency => provider
178 .on_use_field(item)
179 .unwrap_or(ItemUseResult::NoEffect),
180 _ => provider.apply_effect(monster_provider, item, ctx, target, rng),
181 };
182
183 if result.consumes() {
185 inv.remove(&item, 1);
186 }
187 result
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
192pub enum ShopError {
193 NotEnoughMoney,
195 InventoryFull,
198 CannotSell,
201 InvalidQuantity,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub struct ShopReceipt {
208 pub total: u32,
210 pub money_after: u32,
212}
213
214pub fn buy<const N: usize, S>(
222 provider: &S,
223 shop_id: &S::ShopId,
224 inv: &mut Inventory<S::Item, N>,
225 money: &mut u32,
226 item: S::Item,
227 quantity: u32,
228) -> Result<ShopReceipt, ShopError>
229where
230 S: ShopProvider,
231{
232 if quantity == 0 {
233 return Err(ShopError::InvalidQuantity);
234 }
235 let unit_price = provider.buy_price(&item);
236 let discount = provider.discount_rate(shop_id);
237 let effective_price = (unit_price as f32 * discount) as u32;
238 let total = effective_price.saturating_mul(quantity);
239 if *money < total {
240 return Err(ShopError::NotEnoughMoney);
241 }
242 if inv.add(item, quantity).is_err() {
245 return Err(ShopError::InventoryFull);
246 }
247 *money -= total;
248 Ok(ShopReceipt {
249 total,
250 money_after: *money,
251 })
252}
253
254pub fn sell<const N: usize, S>(
261 provider: &S,
262 shop_id: &S::ShopId,
263 inv: &mut Inventory<S::Item, N>,
264 money: &mut u32,
265 item: S::Item,
266 quantity: u32,
267) -> Result<ShopReceipt, ShopError>
268where
269 S: ShopProvider,
270{
271 if quantity == 0 {
272 return Err(ShopError::InvalidQuantity);
273 }
274 if !provider.can_sell(&item) || !inv.contains(&item, quantity) {
275 return Err(ShopError::CannotSell);
276 }
277 if !inv.remove(&item, quantity) {
278 return Err(ShopError::CannotSell);
279 }
280 let base_sell = provider.sell_price(&item);
281 let rate = provider.sell_rate(shop_id);
282 let effective_sell = (base_sell as f32 * rate) as u32;
283 let total = effective_sell.saturating_mul(quantity);
284 *money = money.saturating_add(total);
285 Ok(ShopReceipt {
286 total,
287 money_after: *money,
288 })
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use crate::items::{BagCategory, ItemKind, ItemResult};
295 use crate::party::{MonsterInstance, MonsterStatus, MoveSlot, StatSet};
296
297 struct SeqRng {
301 seq: Vec<u8>,
302 idx: usize,
303 }
304 impl SeqRng {
305 fn new(seq: &[u8]) -> Self {
306 Self {
307 seq: seq.to_vec(),
308 idx: 0,
309 }
310 }
311 }
312 impl BattleRng for SeqRng {
313 fn next_u8(&mut self) -> u8 {
314 let v = self.seq[self.idx % self.seq.len()];
315 self.idx += 1;
316 v
317 }
318 }
319
320 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
323 enum Stat {
324 Hp,
325 }
326 #[derive(Debug, Clone, Copy, Default)]
327 struct MockMon;
328 impl MonsterProvider for MockMon {
329 type SpeciesId = u8;
330 type MoveId = u8;
331 type Genetics = ();
332 type Training = ();
333 type Stat = Stat;
334 fn base_stat(&self, _s: u8, _st: Stat) -> u16 {
335 50
336 }
337 fn calc_stat(&self, _s: u8, _st: Stat, _l: u8, _g: &(), _t: &()) -> u16 {
338 50
339 }
340 fn stats(&self) -> &[Stat] {
341 &[Stat::Hp]
342 }
343 fn hp_stat(&self) -> Stat {
344 Stat::Hp
345 }
346 fn max_moves(&self) -> usize {
347 4
348 }
349 }
350
351 fn mon(current_hp: u16, status: MonsterStatus, pp: u8) -> MonsterInstance<MockMon> {
354 let provider = MockMon;
355 let mut stats = StatSet::zeroed(&provider);
356 stats.set(Stat::Hp, 50);
357 MonsterInstance {
358 species: 1,
359 level: 5,
360 exp: 0,
361 genetics: (),
362 training: (),
363 stats,
364 current_hp,
365 status,
366 moves: vec![MoveSlot {
367 move_id: 0,
368 pp,
369 pp_up: 0,
370 }],
371 }
372 }
373
374 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
377 enum Item {
378 Potion,
379 Antidote,
380 Ball,
381 XAttack,
382 Bicycle, KeyStone, FireStone, }
386
387 struct Game;
388
389 impl ItemProvider for Game {
390 type Item = Item;
391 type Effect = ();
392 type Monster = ();
393 type CustomKind = ();
394
395 fn item_name(&self, _item: &Item) -> &str {
396 "X"
397 }
398 fn item_description(&self, _item: &Item) -> &str {
399 "X"
400 }
401 fn item_effect(&self, _item: &Item) {}
402 fn item_price(&self, item: &Item) -> u32 {
403 match item {
404 Item::Potion => 300,
405 Item::Antidote => 100,
406 Item::Ball => 200,
407 Item::XAttack => 500,
408 Item::Bicycle => 0,
409 Item::KeyStone => 0,
410 Item::FireStone => 2100,
411 }
412 }
413 fn can_use_outside_battle(&self, item: &Item) -> bool {
414 !matches!(item, Item::Ball | Item::XAttack | Item::Bicycle)
415 }
416 fn can_use_in_battle(&self, item: &Item) -> bool {
417 !matches!(item, Item::Bicycle | Item::KeyStone)
418 }
419 fn use_on_monster(&self, _item: &Item, _m: &mut ()) -> ItemResult {
420 ItemResult::NoEffect
421 }
422 fn consume(&self, item: &Item) -> bool {
423 !matches!(item, Item::Bicycle | Item::KeyStone)
424 }
425
426 fn item_kind(&self, item: &Item) -> ItemKind<()> {
427 match item {
428 Item::Bicycle | Item::KeyStone => ItemKind::KeyItem,
429 Item::FireStone => ItemKind::Evolution,
430 _ => ItemKind::Consumable,
431 }
432 }
433
434 fn usable_in(&self, item: &Item) -> UsageContext {
437 match item {
438 Item::Potion | Item::Antidote => UsageContext::FieldAndBattle,
439 Item::Ball | Item::XAttack => UsageContext::BattleOnly,
440 Item::Bicycle => UsageContext::None,
441 Item::KeyStone | Item::FireStone => UsageContext::FieldOnly,
442 }
443 }
444
445 fn apply_effect<M: MonsterProvider>(
446 &self,
447 provider: &M,
448 item: Item,
449 _ctx: UsageContext,
450 target: Option<&mut MonsterInstance<M>>,
451 rng: &mut dyn BattleRng,
452 ) -> ItemUseResult<Item> {
453 let _ = provider;
454 match item {
455 Item::Potion => match target {
456 Some(m) if m.current_hp < 50 => {
459 m.current_hp = (m.current_hp + 20).min(50);
460 ItemUseResult::Applied {
461 consume: true,
462 message_key: None,
463 }
464 }
465 _ => ItemUseResult::NoEffect,
466 },
467 Item::Antidote => match target {
468 Some(m) if m.status == MonsterStatus::Poison => {
469 m.status = MonsterStatus::Healthy;
470 ItemUseResult::Applied {
471 consume: true,
472 message_key: Some("cured".to_string()),
473 }
474 }
475 _ => ItemUseResult::NoEffect,
476 },
477 Item::Ball => {
478 if rng.next_u8() % 2 == 0 {
480 ItemUseResult::Caught
481 } else {
482 ItemUseResult::Failed
483 }
484 }
485 Item::XAttack => ItemUseResult::Applied {
486 consume: true,
487 message_key: None,
488 },
489 Item::Bicycle => ItemUseResult::NoEffect,
490 Item::KeyStone => ItemUseResult::Applied {
491 consume: true,
492 message_key: Some("apply_effect_called".to_string()),
493 },
494 Item::FireStone => ItemUseResult::EvolutionTriggered {
497 item,
498 message_key: Some("evolve?".to_string()),
499 },
500 }
501 }
502
503 fn on_use_field(&self, item: Item) -> Option<ItemUseResult<Item>> {
504 match item {
505 Item::KeyStone => Some(ItemUseResult::Applied {
506 consume: false,
507 message_key: Some("field_used".to_string()),
508 }),
509 _ => None,
510 }
511 }
512 }
513
514 impl ShopProvider for Game {
515 type Item = Item;
516 type ShopId = u8;
517 fn shop_inventory(&self, _shop_id: &u8) -> Vec<(Item, u32)> {
518 vec![(Item::Potion, 300)]
519 }
520 fn shop_name(&self, _shop_id: &u8) -> &str {
521 "Mart"
522 }
523 fn buy_price(&self, item: &Item) -> u32 {
524 self.item_price(item)
525 }
526 fn can_sell(&self, item: &Item) -> bool {
528 !matches!(item, Item::Bicycle | Item::KeyStone)
529 }
530 }
531
532 fn stock(item: Item, qty: u32) -> Inventory<Item, 64> {
533 let mut inv = Inventory::<Item, 64>::new();
534 inv.add(item, qty).unwrap();
535 inv
536 }
537
538 #[test]
541 fn use_item_routes_to_apply_effect_and_consumes_on_applied() {
542 let game = Game;
543 let mut inv = stock(Item::Potion, 3);
544 let mut m = mon(10, MonsterStatus::Healthy, 10);
545 let mut rng = SeqRng::new(&[0]);
546 let r = use_item(
547 &game,
548 &MockMon,
549 &mut inv,
550 Item::Potion,
551 UsageContext::FieldOnly,
552 Some(&mut m),
553 &mut rng,
554 );
555 assert!(matches!(r, ItemUseResult::Applied { consume: true, .. }));
556 assert_eq!(m.current_hp, 30); assert!(inv.contains(&Item::Potion, 2)); assert!(!inv.contains(&Item::Potion, 3));
559 }
560
561 #[test]
562 fn use_item_no_effect_does_not_consume() {
563 let game = Game;
564 let mut inv = stock(Item::Potion, 3);
565 let mut m = mon(50, MonsterStatus::Healthy, 10); let mut rng = SeqRng::new(&[0]);
567 let r = use_item(
568 &game,
569 &MockMon,
570 &mut inv,
571 Item::Potion,
572 UsageContext::FieldOnly,
573 Some(&mut m),
574 &mut rng,
575 );
576 assert_eq!(r, ItemUseResult::NoEffect);
577 assert!(inv.contains(&Item::Potion, 3)); }
579
580 #[test]
581 fn use_item_rejects_not_owned_without_touching_target() {
582 let game = Game;
583 let mut inv: Inventory<Item, 64> = Inventory::new(); let mut m = mon(10, MonsterStatus::Poison, 10);
585 let mut rng = SeqRng::new(&[0]);
586 let r = use_item(
587 &game,
588 &MockMon,
589 &mut inv,
590 Item::Antidote,
591 UsageContext::FieldOnly,
592 Some(&mut m),
593 &mut rng,
594 );
595 assert_eq!(r, ItemUseResult::Failed);
596 assert_eq!(m.status, MonsterStatus::Poison); }
598
599 #[test]
600 fn use_item_rejects_wrong_context() {
601 let game = Game;
602 let mut inv = stock(Item::XAttack, 5); let mut rng = SeqRng::new(&[0]);
604 let r = use_item(
605 &game,
606 &MockMon,
607 &mut inv,
608 Item::XAttack,
609 UsageContext::FieldOnly, None,
611 &mut rng,
612 );
613 assert_eq!(r, ItemUseResult::Failed);
614 assert!(inv.contains(&Item::XAttack, 5)); }
616
617 #[test]
618 fn use_item_caught_consumes_ball() {
619 let game = Game;
620 let mut inv = stock(Item::Ball, 5);
621 let mut rng = SeqRng::new(&[0]); let r = use_item(
623 &game,
624 &MockMon,
625 &mut inv,
626 Item::Ball,
627 UsageContext::BattleOnly,
628 None,
629 &mut rng,
630 );
631 assert_eq!(r, ItemUseResult::Caught);
632 assert!(inv.contains(&Item::Ball, 4)); }
634
635 #[test]
636 fn use_item_failed_ball_not_consumed() {
637 let game = Game;
638 let mut inv = stock(Item::Ball, 5);
639 let mut rng = SeqRng::new(&[1]); let r = use_item(
641 &game,
642 &MockMon,
643 &mut inv,
644 Item::Ball,
645 UsageContext::BattleOnly,
646 None,
647 &mut rng,
648 );
649 assert_eq!(r, ItemUseResult::Failed);
650 assert!(inv.contains(&Item::Ball, 5)); }
652
653 #[test]
654 fn use_item_status_cure_consumes() {
655 let game = Game;
656 let mut inv = stock(Item::Antidote, 1);
657 let mut m = mon(20, MonsterStatus::Poison, 10);
658 let mut rng = SeqRng::new(&[0]);
659 let r = use_item(
660 &game,
661 &MockMon,
662 &mut inv,
663 Item::Antidote,
664 UsageContext::FieldOnly,
665 Some(&mut m),
666 &mut rng,
667 );
668 assert!(matches!(r, ItemUseResult::Applied { consume: true, .. }));
669 assert_eq!(m.status, MonsterStatus::Healthy);
670 assert!(!inv.contains(&Item::Antidote, 1));
671 }
672
673 #[test]
674 fn use_item_default_apply_effect_is_no_effect() {
675 struct Plain;
678 impl ItemProvider for Plain {
679 type Item = u8;
680 type Effect = ();
681 type Monster = ();
682 type CustomKind = ();
683 fn item_name(&self, _i: &u8) -> &str {
684 "X"
685 }
686 fn item_description(&self, _i: &u8) -> &str {
687 "X"
688 }
689 fn item_effect(&self, _i: &u8) {}
690 fn item_price(&self, _i: &u8) -> u32 {
691 0
692 }
693 fn can_use_outside_battle(&self, _i: &u8) -> bool {
694 true
695 }
696 fn can_use_in_battle(&self, _i: &u8) -> bool {
697 true
698 }
699 fn use_on_monster(&self, _i: &u8, _m: &mut ()) -> ItemResult {
700 ItemResult::NoEffect
701 }
702 fn consume(&self, _i: &u8) -> bool {
703 true
704 }
705 fn item_kind(&self, _item: &u8) -> ItemKind<()> {
706 ItemKind::Consumable
707 }
708 }
709 let game = Plain;
710 let mut inv = Inventory::<u8, 64>::new();
711 inv.add(7u8, 2).unwrap();
712 let mut m = mon(10, MonsterStatus::Healthy, 10);
713 let mut rng = SeqRng::new(&[0]);
714 let r = use_item(
715 &game,
716 &MockMon,
717 &mut inv,
718 7u8,
719 UsageContext::FieldOnly,
720 Some(&mut m),
721 &mut rng,
722 );
723 assert_eq!(r, ItemUseResult::NoEffect);
724 assert!(inv.contains(&7u8, 2)); assert_eq!(m.current_hp, 10); }
727
728 #[test]
731 fn buy_deducts_money_and_adds_item() {
732 let game = Game;
733 let mut inv = Inventory::<Item, 64>::new();
734 let mut money = 1000u32;
735 let r = buy(&game, &0u8, &mut inv, &mut money, Item::Potion, 2).unwrap();
736 assert_eq!(r.total, 600); assert_eq!(money, 400);
738 assert!(inv.contains(&Item::Potion, 2));
739 }
740
741 #[test]
742 fn buy_fails_when_broke_and_changes_nothing() {
743 let game = Game;
744 let mut inv = Inventory::<Item, 64>::new();
745 let mut money = 100u32;
746 let err = buy(&game, &0u8, &mut inv, &mut money, Item::Potion, 1).unwrap_err();
747 assert_eq!(err, ShopError::NotEnoughMoney);
748 assert_eq!(money, 100); assert!(!inv.contains(&Item::Potion, 1)); }
751
752 #[test]
753 fn buy_fails_when_inventory_full_and_keeps_money() {
754 let game = Game;
755 let mut inv = Inventory::<Item, 1>::with_capacity(99);
757 inv.add(Item::Antidote, 1).unwrap();
758 let mut money = 1000u32;
759 let err = buy(&game, &0u8, &mut inv, &mut money, Item::Potion, 1).unwrap_err();
760 assert_eq!(err, ShopError::InventoryFull);
761 assert_eq!(money, 1000); assert!(!inv.contains(&Item::Potion, 1)); }
764
765 #[test]
766 fn buy_fails_at_per_slot_cap_and_keeps_money() {
767 let game = Game;
768 let mut inv = Inventory::<Item, 20>::with_capacity(99);
769 inv.add(Item::Potion, 99).unwrap(); let mut money = 1000u32;
771 let err = buy(&game, &0u8, &mut inv, &mut money, Item::Potion, 1).unwrap_err();
772 assert_eq!(err, ShopError::InventoryFull);
773 assert_eq!(money, 1000); assert_eq!(inv.quantity(&Item::Potion), 99); }
776
777 #[test]
778 fn sell_adds_money_and_removes_item() {
779 let game = Game;
780 let mut inv = stock(Item::Potion, 3);
781 let mut money = 0u32;
782 let r = sell(&game, &0u8, &mut inv, &mut money, Item::Potion, 2).unwrap();
783 assert_eq!(r.total, 300);
786 assert_eq!(money, 300);
787 assert!(inv.contains(&Item::Potion, 1));
788 }
789
790 #[test]
791 fn sell_rate_override_scales_sell_price() {
792 struct Pawnshop;
794 impl ShopProvider for Pawnshop {
795 type Item = Item;
796 type ShopId = u8;
797 fn shop_inventory(&self, _shop_id: &u8) -> Vec<(Item, u32)> {
798 vec![]
799 }
800 fn shop_name(&self, _shop_id: &u8) -> &str {
801 "Pawnshop"
802 }
803 fn buy_price(&self, _item: &Item) -> u32 {
804 300
805 }
806 fn sell_rate(&self, _shop_id: &u8) -> f32 {
807 0.8
808 }
809 }
810 let mut inv = stock(Item::Potion, 1);
811 let mut money = 0u32;
812 let r = sell(&Pawnshop, &0u8, &mut inv, &mut money, Item::Potion, 1).unwrap();
813 assert_eq!(r.total, 120);
815 }
816
817 #[test]
818 fn sell_rejects_key_item_and_changes_nothing() {
819 let game = Game;
820 let mut inv = stock(Item::Bicycle, 1);
821 let mut money = 0u32;
822 let err = sell(&game, &0u8, &mut inv, &mut money, Item::Bicycle, 1).unwrap_err();
823 assert_eq!(err, ShopError::CannotSell);
824 assert_eq!(money, 0);
825 assert!(inv.contains(&Item::Bicycle, 1)); }
827
828 #[test]
829 fn sell_rejects_when_not_enough_owned() {
830 let game = Game;
831 let mut inv = stock(Item::Potion, 1);
832 let mut money = 0u32;
833 let err = sell(&game, &0u8, &mut inv, &mut money, Item::Potion, 5).unwrap_err();
834 assert_eq!(err, ShopError::CannotSell);
835 assert!(inv.contains(&Item::Potion, 1));
836 assert_eq!(money, 0);
837 }
838
839 #[test]
842 fn use_item_key_item_routes_to_on_use_field_not_apply_effect() {
843 let game = Game;
844 let mut inv = stock(Item::KeyStone, 1);
845 let mut m = mon(50, MonsterStatus::Healthy, 10);
846 let mut rng = SeqRng::new(&[0]);
847 let r = use_item(
848 &game,
849 &MockMon,
850 &mut inv,
851 Item::KeyStone,
852 UsageContext::FieldOnly,
853 Some(&mut m),
854 &mut rng,
855 );
856 assert_eq!(
858 r,
859 ItemUseResult::Applied {
860 consume: false,
861 message_key: Some("field_used".to_string()),
862 }
863 );
864 assert!(inv.contains(&Item::KeyStone, 1));
866 }
867
868 #[test]
869 fn evolution_item_dispatches_via_apply_effect_and_is_not_consumed() {
870 let game = Game;
871 let mut inv = stock(Item::FireStone, 1);
872 let mut m = mon(50, MonsterStatus::Healthy, 10);
873 let mut rng = SeqRng::new(&[0]);
874 let r = use_item(
875 &game,
876 &MockMon,
877 &mut inv,
878 Item::FireStone,
879 UsageContext::FieldOnly,
880 Some(&mut m),
881 &mut rng,
882 );
883 assert_eq!(
887 r,
888 ItemUseResult::EvolutionTriggered {
889 item: Item::FireStone,
890 message_key: Some("evolve?".to_string()),
891 }
892 );
893 assert!(inv.contains(&Item::FireStone, 1)); }
895
896 #[test]
899 fn bag_category_variants_exist() {
900 let _ = BagCategory::Items;
901 let _ = BagCategory::Medicine;
902 }
903}