1use core::fmt::Debug;
23use core::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!(
61 self,
62 UsageContext::BattleOnly | UsageContext::FieldAndBattle
63 )
64 }
65 }
66 }
67}
68
69#[derive(Clone, Debug, PartialEq, Eq)]
76pub enum ItemUseResult<I: Copy + Eq + Hash + Debug> {
77 Applied {
81 consume: bool,
83 message_key: Option<String>,
85 },
86 NoEffect,
89 Caught,
91 Failed,
94 EvolutionTriggered {
98 item: I,
101 message_key: Option<String>,
103 },
104 MoveLearned {
108 consume: bool,
110 message_key: Option<String>,
112 },
113}
114
115impl<I: Copy + Eq + Hash + Debug> ItemUseResult<I> {
116 pub fn consumes(&self) -> bool {
118 match self {
119 ItemUseResult::Applied { consume, .. } => *consume,
120 ItemUseResult::Caught => true,
121 ItemUseResult::MoveLearned { consume, .. } => *consume,
122 ItemUseResult::EvolutionTriggered { .. } => false,
123 ItemUseResult::NoEffect | ItemUseResult::Failed => false,
124 }
125 }
126}
127
128pub fn use_item<const N: usize, I, M>(
139 provider: &I,
140 monster_provider: &M,
141 inv: &mut Inventory<I::Item, N>,
142 item: I::Item,
143 ctx: UsageContext,
144 target: Option<&mut MonsterInstance<M>>,
145 rng: &mut dyn BattleRng,
146) -> ItemUseResult<I::Item>
147where
148 I: ItemProvider,
149 M: MonsterProvider,
150{
151 if !inv.contains(&item, 1) {
153 return ItemUseResult::Failed;
154 }
155 if !ctx.allows(provider.usable_in(&item)) {
157 return ItemUseResult::Failed;
158 }
159
160 let kind = provider.item_kind(&item);
170 let result = match kind {
171 ItemKind::TeachMove => {
172 if let Some(target) = target {
173 provider
174 .on_teach_move(item, target)
175 .unwrap_or(ItemUseResult::NoEffect)
176 } else {
177 ItemUseResult::NoEffect
178 }
179 }
180 ItemKind::KeyItem | ItemKind::Currency => provider
181 .on_use_field(item)
182 .unwrap_or(ItemUseResult::NoEffect),
183 _ => provider.apply_effect(monster_provider, item, ctx, target, rng),
184 };
185
186 if result.consumes() {
188 inv.remove(&item, 1);
189 }
190 result
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
195pub enum ShopError {
196 NotEnoughMoney,
198 InventoryFull,
201 CannotSell,
204 InvalidQuantity,
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210pub struct ShopReceipt {
211 pub total: u32,
213 pub money_after: u32,
215}
216
217pub fn buy<const N: usize, S>(
225 provider: &S,
226 shop_id: &S::ShopId,
227 inv: &mut Inventory<S::Item, N>,
228 money: &mut u32,
229 item: S::Item,
230 quantity: u32,
231) -> Result<ShopReceipt, ShopError>
232where
233 S: ShopProvider,
234{
235 if quantity == 0 {
236 return Err(ShopError::InvalidQuantity);
237 }
238 let unit_price = provider.buy_price(&item);
239 let discount = provider.discount_rate(shop_id);
240 let effective_price = (unit_price as f32 * discount) as u32;
241 let total = effective_price.saturating_mul(quantity);
242 if *money < total {
243 return Err(ShopError::NotEnoughMoney);
244 }
245 if inv.add(item, quantity).is_err() {
248 return Err(ShopError::InventoryFull);
249 }
250 *money -= total;
251 Ok(ShopReceipt {
252 total,
253 money_after: *money,
254 })
255}
256
257pub fn sell<const N: usize, S>(
264 provider: &S,
265 shop_id: &S::ShopId,
266 inv: &mut Inventory<S::Item, N>,
267 money: &mut u32,
268 item: S::Item,
269 quantity: u32,
270) -> Result<ShopReceipt, ShopError>
271where
272 S: ShopProvider,
273{
274 if quantity == 0 {
275 return Err(ShopError::InvalidQuantity);
276 }
277 if !provider.can_sell(&item) || !inv.contains(&item, quantity) {
278 return Err(ShopError::CannotSell);
279 }
280 if !inv.remove(&item, quantity) {
281 return Err(ShopError::CannotSell);
282 }
283 let base_sell = provider.sell_price(&item);
284 let rate = provider.sell_rate(shop_id);
285 let effective_sell = (base_sell as f32 * rate) as u32;
286 let total = effective_sell.saturating_mul(quantity);
287 *money = money.saturating_add(total);
288 Ok(ShopReceipt {
289 total,
290 money_after: *money,
291 })
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use crate::items::{BagCategory, ItemKind, ItemResult};
298 use crate::party::{MonsterInstance, MonsterStatus, MoveSlot, StatSet};
299
300 struct SeqRng {
304 seq: Vec<u8>,
305 idx: usize,
306 }
307 impl SeqRng {
308 fn new(seq: &[u8]) -> Self {
309 Self {
310 seq: seq.to_vec(),
311 idx: 0,
312 }
313 }
314 }
315 impl BattleRng for SeqRng {
316 fn next_u8(&mut self) -> u8 {
317 let v = self.seq[self.idx % self.seq.len()];
318 self.idx += 1;
319 v
320 }
321 }
322
323 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
326 enum Stat {
327 Hp,
328 }
329 #[derive(Debug, Clone, Copy, Default)]
330 struct MockMon;
331 impl MonsterProvider for MockMon {
332 type SpeciesId = u8;
333 type MoveId = u8;
334 type Genetics = ();
335 type Training = ();
336 type Stat = Stat;
337 fn base_stat(&self, _s: u8, _st: Stat) -> u16 {
338 50
339 }
340 fn calc_stat(&self, _s: u8, _st: Stat, _l: u8, _g: &(), _t: &()) -> u16 {
341 50
342 }
343 fn stats(&self) -> &[Stat] {
344 &[Stat::Hp]
345 }
346 fn hp_stat(&self) -> Stat {
347 Stat::Hp
348 }
349 fn max_moves(&self) -> usize {
350 4
351 }
352 }
353
354 fn mon(current_hp: u16, status: MonsterStatus, pp: u8) -> MonsterInstance<MockMon> {
357 let provider = MockMon;
358 let mut stats = StatSet::zeroed(&provider);
359 stats.set(Stat::Hp, 50);
360 MonsterInstance {
361 species: 1,
362 level: 5,
363 exp: 0,
364 genetics: (),
365 training: (),
366 stats,
367 current_hp,
368 status,
369 moves: vec![MoveSlot {
370 move_id: 0,
371 pp,
372 pp_up: 0,
373 }],
374 }
375 }
376
377 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
380 enum Item {
381 Potion,
382 Antidote,
383 Ball,
384 XAttack,
385 Bicycle, KeyStone, FireStone, }
389
390 struct Game;
391
392 impl ItemProvider for Game {
393 type Item = Item;
394 type Effect = ();
395 type Monster = ();
396 type CustomKind = ();
397
398 fn item_name(&self, _item: &Item) -> &str {
399 "X"
400 }
401 fn item_description(&self, _item: &Item) -> &str {
402 "X"
403 }
404 fn item_effect(&self, _item: &Item) {}
405 fn item_price(&self, item: &Item) -> u32 {
406 match item {
407 Item::Potion => 300,
408 Item::Antidote => 100,
409 Item::Ball => 200,
410 Item::XAttack => 500,
411 Item::Bicycle => 0,
412 Item::KeyStone => 0,
413 Item::FireStone => 2100,
414 }
415 }
416 fn can_use_outside_battle(&self, item: &Item) -> bool {
417 !matches!(item, Item::Ball | Item::XAttack | Item::Bicycle)
418 }
419 fn can_use_in_battle(&self, item: &Item) -> bool {
420 !matches!(item, Item::Bicycle | Item::KeyStone)
421 }
422 fn use_on_monster(&self, _item: &Item, _m: &mut ()) -> ItemResult {
423 ItemResult::NoEffect
424 }
425 fn consume(&self, item: &Item) -> bool {
426 !matches!(item, Item::Bicycle | Item::KeyStone)
427 }
428
429 fn item_kind(&self, item: &Item) -> ItemKind<()> {
430 match item {
431 Item::Bicycle | Item::KeyStone => ItemKind::KeyItem,
432 Item::FireStone => ItemKind::Evolution,
433 _ => ItemKind::Consumable,
434 }
435 }
436
437 fn usable_in(&self, item: &Item) -> UsageContext {
440 match item {
441 Item::Potion | Item::Antidote => UsageContext::FieldAndBattle,
442 Item::Ball | Item::XAttack => UsageContext::BattleOnly,
443 Item::Bicycle => UsageContext::None,
444 Item::KeyStone | Item::FireStone => UsageContext::FieldOnly,
445 }
446 }
447
448 fn apply_effect<M: MonsterProvider>(
449 &self,
450 provider: &M,
451 item: Item,
452 _ctx: UsageContext,
453 target: Option<&mut MonsterInstance<M>>,
454 rng: &mut dyn BattleRng,
455 ) -> ItemUseResult<Item> {
456 let _ = provider;
457 match item {
458 Item::Potion => match target {
459 Some(m) if m.current_hp < 50 => {
462 m.current_hp = (m.current_hp + 20).min(50);
463 ItemUseResult::Applied {
464 consume: true,
465 message_key: None,
466 }
467 }
468 _ => ItemUseResult::NoEffect,
469 },
470 Item::Antidote => match target {
471 Some(m) if m.status == MonsterStatus::Poison => {
472 m.status = MonsterStatus::Healthy;
473 ItemUseResult::Applied {
474 consume: true,
475 message_key: Some("cured".to_string()),
476 }
477 }
478 _ => ItemUseResult::NoEffect,
479 },
480 Item::Ball => {
481 if rng.next_u8() % 2 == 0 {
483 ItemUseResult::Caught
484 } else {
485 ItemUseResult::Failed
486 }
487 }
488 Item::XAttack => ItemUseResult::Applied {
489 consume: true,
490 message_key: None,
491 },
492 Item::Bicycle => ItemUseResult::NoEffect,
493 Item::KeyStone => ItemUseResult::Applied {
494 consume: true,
495 message_key: Some("apply_effect_called".to_string()),
496 },
497 Item::FireStone => ItemUseResult::EvolutionTriggered {
500 item,
501 message_key: Some("evolve?".to_string()),
502 },
503 }
504 }
505
506 fn on_use_field(&self, item: Item) -> Option<ItemUseResult<Item>> {
507 match item {
508 Item::KeyStone => Some(ItemUseResult::Applied {
509 consume: false,
510 message_key: Some("field_used".to_string()),
511 }),
512 _ => None,
513 }
514 }
515 }
516
517 impl ShopProvider for Game {
518 type Item = Item;
519 type ShopId = u8;
520 fn shop_inventory(&self, _shop_id: &u8) -> Vec<(Item, u32)> {
521 vec![(Item::Potion, 300)]
522 }
523 fn shop_name(&self, _shop_id: &u8) -> &str {
524 "Mart"
525 }
526 fn buy_price(&self, item: &Item) -> u32 {
527 self.item_price(item)
528 }
529 fn can_sell(&self, item: &Item) -> bool {
531 !matches!(item, Item::Bicycle | Item::KeyStone)
532 }
533 }
534
535 fn stock(item: Item, qty: u32) -> Inventory<Item, 64> {
536 let mut inv = Inventory::<Item, 64>::new();
537 inv.add(item, qty).unwrap();
538 inv
539 }
540
541 #[test]
544 fn use_item_routes_to_apply_effect_and_consumes_on_applied() {
545 let game = Game;
546 let mut inv = stock(Item::Potion, 3);
547 let mut m = mon(10, MonsterStatus::Healthy, 10);
548 let mut rng = SeqRng::new(&[0]);
549 let r = use_item(
550 &game,
551 &MockMon,
552 &mut inv,
553 Item::Potion,
554 UsageContext::FieldOnly,
555 Some(&mut m),
556 &mut rng,
557 );
558 assert!(matches!(r, ItemUseResult::Applied { consume: true, .. }));
559 assert_eq!(m.current_hp, 30); assert!(inv.contains(&Item::Potion, 2)); assert!(!inv.contains(&Item::Potion, 3));
562 }
563
564 #[test]
565 fn use_item_no_effect_does_not_consume() {
566 let game = Game;
567 let mut inv = stock(Item::Potion, 3);
568 let mut m = mon(50, MonsterStatus::Healthy, 10); let mut rng = SeqRng::new(&[0]);
570 let r = use_item(
571 &game,
572 &MockMon,
573 &mut inv,
574 Item::Potion,
575 UsageContext::FieldOnly,
576 Some(&mut m),
577 &mut rng,
578 );
579 assert_eq!(r, ItemUseResult::NoEffect);
580 assert!(inv.contains(&Item::Potion, 3)); }
582
583 #[test]
584 fn use_item_rejects_not_owned_without_touching_target() {
585 let game = Game;
586 let mut inv: Inventory<Item, 64> = Inventory::new(); let mut m = mon(10, MonsterStatus::Poison, 10);
588 let mut rng = SeqRng::new(&[0]);
589 let r = use_item(
590 &game,
591 &MockMon,
592 &mut inv,
593 Item::Antidote,
594 UsageContext::FieldOnly,
595 Some(&mut m),
596 &mut rng,
597 );
598 assert_eq!(r, ItemUseResult::Failed);
599 assert_eq!(m.status, MonsterStatus::Poison); }
601
602 #[test]
603 fn use_item_rejects_wrong_context() {
604 let game = Game;
605 let mut inv = stock(Item::XAttack, 5); let mut rng = SeqRng::new(&[0]);
607 let r = use_item(
608 &game,
609 &MockMon,
610 &mut inv,
611 Item::XAttack,
612 UsageContext::FieldOnly, None,
614 &mut rng,
615 );
616 assert_eq!(r, ItemUseResult::Failed);
617 assert!(inv.contains(&Item::XAttack, 5)); }
619
620 #[test]
621 fn use_item_caught_consumes_ball() {
622 let game = Game;
623 let mut inv = stock(Item::Ball, 5);
624 let mut rng = SeqRng::new(&[0]); let r = use_item(
626 &game,
627 &MockMon,
628 &mut inv,
629 Item::Ball,
630 UsageContext::BattleOnly,
631 None,
632 &mut rng,
633 );
634 assert_eq!(r, ItemUseResult::Caught);
635 assert!(inv.contains(&Item::Ball, 4)); }
637
638 #[test]
639 fn use_item_failed_ball_not_consumed() {
640 let game = Game;
641 let mut inv = stock(Item::Ball, 5);
642 let mut rng = SeqRng::new(&[1]); let r = use_item(
644 &game,
645 &MockMon,
646 &mut inv,
647 Item::Ball,
648 UsageContext::BattleOnly,
649 None,
650 &mut rng,
651 );
652 assert_eq!(r, ItemUseResult::Failed);
653 assert!(inv.contains(&Item::Ball, 5)); }
655
656 #[test]
657 fn use_item_status_cure_consumes() {
658 let game = Game;
659 let mut inv = stock(Item::Antidote, 1);
660 let mut m = mon(20, MonsterStatus::Poison, 10);
661 let mut rng = SeqRng::new(&[0]);
662 let r = use_item(
663 &game,
664 &MockMon,
665 &mut inv,
666 Item::Antidote,
667 UsageContext::FieldOnly,
668 Some(&mut m),
669 &mut rng,
670 );
671 assert!(matches!(r, ItemUseResult::Applied { consume: true, .. }));
672 assert_eq!(m.status, MonsterStatus::Healthy);
673 assert!(!inv.contains(&Item::Antidote, 1));
674 }
675
676 #[test]
677 fn use_item_default_apply_effect_is_no_effect() {
678 struct Plain;
681 impl ItemProvider for Plain {
682 type Item = u8;
683 type Effect = ();
684 type Monster = ();
685 type CustomKind = ();
686 fn item_name(&self, _i: &u8) -> &str {
687 "X"
688 }
689 fn item_description(&self, _i: &u8) -> &str {
690 "X"
691 }
692 fn item_effect(&self, _i: &u8) {}
693 fn item_price(&self, _i: &u8) -> u32 {
694 0
695 }
696 fn can_use_outside_battle(&self, _i: &u8) -> bool {
697 true
698 }
699 fn can_use_in_battle(&self, _i: &u8) -> bool {
700 true
701 }
702 fn use_on_monster(&self, _i: &u8, _m: &mut ()) -> ItemResult {
703 ItemResult::NoEffect
704 }
705 fn consume(&self, _i: &u8) -> bool {
706 true
707 }
708 fn item_kind(&self, _item: &u8) -> ItemKind<()> {
709 ItemKind::Consumable
710 }
711 }
712 let game = Plain;
713 let mut inv = Inventory::<u8, 64>::new();
714 inv.add(7u8, 2).unwrap();
715 let mut m = mon(10, MonsterStatus::Healthy, 10);
716 let mut rng = SeqRng::new(&[0]);
717 let r = use_item(
718 &game,
719 &MockMon,
720 &mut inv,
721 7u8,
722 UsageContext::FieldOnly,
723 Some(&mut m),
724 &mut rng,
725 );
726 assert_eq!(r, ItemUseResult::NoEffect);
727 assert!(inv.contains(&7u8, 2)); assert_eq!(m.current_hp, 10); }
730
731 #[test]
734 fn buy_deducts_money_and_adds_item() {
735 let game = Game;
736 let mut inv = Inventory::<Item, 64>::new();
737 let mut money = 1000u32;
738 let r = buy(&game, &0u8, &mut inv, &mut money, Item::Potion, 2).unwrap();
739 assert_eq!(r.total, 600); assert_eq!(money, 400);
741 assert!(inv.contains(&Item::Potion, 2));
742 }
743
744 #[test]
745 fn buy_fails_when_broke_and_changes_nothing() {
746 let game = Game;
747 let mut inv = Inventory::<Item, 64>::new();
748 let mut money = 100u32;
749 let err = buy(&game, &0u8, &mut inv, &mut money, Item::Potion, 1).unwrap_err();
750 assert_eq!(err, ShopError::NotEnoughMoney);
751 assert_eq!(money, 100); assert!(!inv.contains(&Item::Potion, 1)); }
754
755 #[test]
756 fn buy_fails_when_inventory_full_and_keeps_money() {
757 let game = Game;
758 let mut inv = Inventory::<Item, 1>::with_capacity(99);
760 inv.add(Item::Antidote, 1).unwrap();
761 let mut money = 1000u32;
762 let err = buy(&game, &0u8, &mut inv, &mut money, Item::Potion, 1).unwrap_err();
763 assert_eq!(err, ShopError::InventoryFull);
764 assert_eq!(money, 1000); assert!(!inv.contains(&Item::Potion, 1)); }
767
768 #[test]
769 fn buy_fails_at_per_slot_cap_and_keeps_money() {
770 let game = Game;
771 let mut inv = Inventory::<Item, 20>::with_capacity(99);
772 inv.add(Item::Potion, 99).unwrap(); let mut money = 1000u32;
774 let err = buy(&game, &0u8, &mut inv, &mut money, Item::Potion, 1).unwrap_err();
775 assert_eq!(err, ShopError::InventoryFull);
776 assert_eq!(money, 1000); assert_eq!(inv.quantity(&Item::Potion), 99); }
779
780 #[test]
781 fn sell_adds_money_and_removes_item() {
782 let game = Game;
783 let mut inv = stock(Item::Potion, 3);
784 let mut money = 0u32;
785 let r = sell(&game, &0u8, &mut inv, &mut money, Item::Potion, 2).unwrap();
786 assert_eq!(r.total, 300);
789 assert_eq!(money, 300);
790 assert!(inv.contains(&Item::Potion, 1));
791 }
792
793 #[test]
794 fn sell_rate_override_scales_sell_price() {
795 struct Pawnshop;
797 impl ShopProvider for Pawnshop {
798 type Item = Item;
799 type ShopId = u8;
800 fn shop_inventory(&self, _shop_id: &u8) -> Vec<(Item, u32)> {
801 vec![]
802 }
803 fn shop_name(&self, _shop_id: &u8) -> &str {
804 "Pawnshop"
805 }
806 fn buy_price(&self, _item: &Item) -> u32 {
807 300
808 }
809 fn sell_rate(&self, _shop_id: &u8) -> f32 {
810 0.8
811 }
812 }
813 let mut inv = stock(Item::Potion, 1);
814 let mut money = 0u32;
815 let r = sell(&Pawnshop, &0u8, &mut inv, &mut money, Item::Potion, 1).unwrap();
816 assert_eq!(r.total, 120);
818 }
819
820 #[test]
821 fn sell_rejects_key_item_and_changes_nothing() {
822 let game = Game;
823 let mut inv = stock(Item::Bicycle, 1);
824 let mut money = 0u32;
825 let err = sell(&game, &0u8, &mut inv, &mut money, Item::Bicycle, 1).unwrap_err();
826 assert_eq!(err, ShopError::CannotSell);
827 assert_eq!(money, 0);
828 assert!(inv.contains(&Item::Bicycle, 1)); }
830
831 #[test]
832 fn sell_rejects_when_not_enough_owned() {
833 let game = Game;
834 let mut inv = stock(Item::Potion, 1);
835 let mut money = 0u32;
836 let err = sell(&game, &0u8, &mut inv, &mut money, Item::Potion, 5).unwrap_err();
837 assert_eq!(err, ShopError::CannotSell);
838 assert!(inv.contains(&Item::Potion, 1));
839 assert_eq!(money, 0);
840 }
841
842 #[test]
845 fn use_item_key_item_routes_to_on_use_field_not_apply_effect() {
846 let game = Game;
847 let mut inv = stock(Item::KeyStone, 1);
848 let mut m = mon(50, MonsterStatus::Healthy, 10);
849 let mut rng = SeqRng::new(&[0]);
850 let r = use_item(
851 &game,
852 &MockMon,
853 &mut inv,
854 Item::KeyStone,
855 UsageContext::FieldOnly,
856 Some(&mut m),
857 &mut rng,
858 );
859 assert_eq!(
861 r,
862 ItemUseResult::Applied {
863 consume: false,
864 message_key: Some("field_used".to_string()),
865 }
866 );
867 assert!(inv.contains(&Item::KeyStone, 1));
869 }
870
871 #[test]
872 fn evolution_item_dispatches_via_apply_effect_and_is_not_consumed() {
873 let game = Game;
874 let mut inv = stock(Item::FireStone, 1);
875 let mut m = mon(50, MonsterStatus::Healthy, 10);
876 let mut rng = SeqRng::new(&[0]);
877 let r = use_item(
878 &game,
879 &MockMon,
880 &mut inv,
881 Item::FireStone,
882 UsageContext::FieldOnly,
883 Some(&mut m),
884 &mut rng,
885 );
886 assert_eq!(
890 r,
891 ItemUseResult::EvolutionTriggered {
892 item: Item::FireStone,
893 message_key: Some("evolve?".to_string()),
894 }
895 );
896 assert!(inv.contains(&Item::FireStone, 1)); }
898
899 #[test]
902 fn bag_category_variants_exist() {
903 let _ = BagCategory::Items;
904 let _ = BagCategory::Medicine;
905 }
906}