1use core::fmt::Debug;
21use core::hash::Hash;
22use core::str::FromStr;
23
24use super::use_driver::{buy, sell, ShopError};
25use super::{Inventory, ShopProvider};
26use crate::menu::MenuInput;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum MartSound {
33 Purchase,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum ConfirmChoice {
40 Yes,
41 No,
42}
43
44impl ConfirmChoice {
45 fn toggle(self) -> Self {
46 match self {
47 ConfirmChoice::Yes => ConfirmChoice::No,
48 ConfirmChoice::No => ConfirmChoice::Yes,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum MartTopChoice {
56 Buy,
57 Sell,
58 Quit,
59}
60
61impl MartTopChoice {
62 const ORDER: [MartTopChoice; 3] = [MartTopChoice::Buy, MartTopChoice::Sell, MartTopChoice::Quit];
63
64 pub fn position(self) -> usize {
65 Self::ORDER.iter().position(|&c| c == self).expect("valid choice")
66 }
67
68 fn next(self) -> Self {
69 let pos = self.position();
70 Self::ORDER[(pos + 1) % 3]
71 }
72
73 fn prev(self) -> Self {
74 let pos = self.position();
75 Self::ORDER[(pos + 2) % 3] }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum MartUpdate {
82 Continue,
84 PlaySound(MartSound),
86 Exit,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum BuyResult {
95 Success { total_cost: u32 },
96 NotEnoughMoney,
97 BagFull,
98 InvalidItem,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum SellResult {
104 Success { total_value: u32 },
105 Unsellable,
106 NotInBag,
107 InvalidItem,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum BuyMenuState {
115 SelectItem { cursor: usize },
117 Quantity { item_index: usize, quantity: u8 },
119 Confirm {
121 item_index: usize,
122 quantity: u8,
123 selected: ConfirmChoice,
124 },
125 Result {
127 dialogue: BuyResult,
128 return_to_list: bool,
130 },
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
135pub enum SellMenuState {
136 SelectItem { cursor: usize },
138 Quantity {
140 item_index: usize,
141 quantity: u8,
142 max_quantity: u8,
143 },
144 Confirm {
146 item_index: usize,
147 quantity: u8,
148 max_quantity: u8,
149 selected: ConfirmChoice,
150 },
151 Result {
153 dialogue: SellResult,
154 return_to_list: bool,
156 },
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
161pub enum MartPhase {
162 MainMenu { cursor: MartTopChoice },
163 Buy(BuyMenuState),
164 Sell(SellMenuState),
165 Exiting,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct MartStock<I: Copy + Eq + Hash + Debug> {
173 items: Vec<I>,
174}
175
176impl<I: Copy + Eq + Hash + Debug> MartStock<I> {
177 pub fn new(items: Vec<I>) -> Self {
178 Self { items }
179 }
180
181 pub fn items(&self) -> &[I] {
182 &self.items
183 }
184
185 pub fn get(&self, index: usize) -> Option<I> {
186 self.items.get(index).copied()
187 }
188
189 pub fn len(&self) -> usize {
190 self.items.len()
191 }
192
193 pub fn is_empty(&self) -> bool {
194 self.items.is_empty()
195 }
196}
197
198impl<I: Copy + Eq + Hash + Debug + FromStr> MartStock<I> {
199 pub fn from_strings<S: AsRef<str>>(items: &[S]) -> Result<Self, String> {
204 let mut parsed = Vec::with_capacity(items.len());
205 for s in items {
206 match I::from_str(s.as_ref()) {
207 Ok(id) => parsed.push(id),
208 Err(_) => return Err(s.as_ref().to_string()),
209 }
210 }
211 Ok(Self::new(parsed))
212 }
213}
214
215pub trait MartBackend {
223 type Item: Copy + Eq + Hash + Debug;
225
226 fn bag_len(&self) -> usize;
228
229 fn bag_entry(&self, index: usize) -> Option<(Self::Item, u8)>;
231
232 fn can_buy(&self, item: &Self::Item) -> bool {
235 let _ = item;
236 true
237 }
238
239 fn commit_buy(&mut self, item: Self::Item, quantity: u8) -> BuyResult;
242
243 fn commit_sell(&mut self, bag_index: usize, quantity: u8) -> SellResult;
247}
248
249pub struct MartDriver<'a, S: ShopProvider, const N: usize> {
256 pub provider: &'a S,
257 pub shop_id: S::ShopId,
258 pub money: &'a mut u32,
259 pub bag: &'a mut Inventory<S::Item, N>,
260}
261
262impl<S: ShopProvider, const N: usize> MartBackend for MartDriver<'_, S, N> {
263 type Item = S::Item;
264
265 fn bag_len(&self) -> usize {
266 self.bag.count()
267 }
268
269 fn bag_entry(&self, index: usize) -> Option<(S::Item, u8)> {
270 self.bag
271 .get(index)
272 .map(|&(item, qty)| (item, qty.min(u8::MAX as u32) as u8))
273 }
274
275 fn commit_buy(&mut self, item: S::Item, quantity: u8) -> BuyResult {
276 match buy(
277 self.provider,
278 &self.shop_id,
279 self.bag,
280 self.money,
281 item,
282 quantity as u32,
283 ) {
284 Ok(receipt) => BuyResult::Success {
285 total_cost: receipt.total,
286 },
287 Err(ShopError::NotEnoughMoney) => BuyResult::NotEnoughMoney,
288 Err(ShopError::InventoryFull) => BuyResult::BagFull,
289 Err(ShopError::CannotSell | ShopError::InvalidQuantity) => BuyResult::InvalidItem,
290 }
291 }
292
293 fn commit_sell(&mut self, bag_index: usize, quantity: u8) -> SellResult {
294 let Some(&(item, _)) = self.bag.get(bag_index) else {
295 return SellResult::NotInBag;
296 };
297 match sell(
298 self.provider,
299 &self.shop_id,
300 self.bag,
301 self.money,
302 item,
303 quantity as u32,
304 ) {
305 Ok(receipt) => SellResult::Success {
306 total_value: receipt.total,
307 },
308 Err(ShopError::CannotSell) => SellResult::Unsellable,
311 Err(_) => SellResult::InvalidItem,
312 }
313 }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct MartState<I: Copy + Eq + Hash + Debug> {
325 pub inventory: MartStock<I>,
327 pub phase: MartPhase,
328}
329
330impl<I: Copy + Eq + Hash + Debug> MartState<I> {
331 pub fn new(inventory: MartStock<I>) -> Self {
335 Self {
336 inventory,
337 phase: MartPhase::MainMenu {
338 cursor: MartTopChoice::Buy,
339 },
340 }
341 }
342
343 pub fn update_frame<B: MartBackend<Item = I>>(
350 &mut self,
351 input: MenuInput,
352 backend: &mut B,
353 ) -> MartUpdate {
354 match &self.phase {
355 MartPhase::MainMenu { cursor } => self.update_main_menu(input, backend, *cursor),
356 MartPhase::Buy(bs) => self.update_buy(input, backend, bs.clone()),
357 MartPhase::Sell(ss) => self.update_sell(input, backend, ss.clone()),
358 MartPhase::Exiting => MartUpdate::Exit,
359 }
360 }
361
362 fn update_main_menu<B: MartBackend<Item = I>>(
365 &mut self,
366 input: MenuInput,
367 backend: &B,
368 cursor: MartTopChoice,
369 ) -> MartUpdate {
370 if input.cancel {
371 self.phase = MartPhase::Exiting;
372 return MartUpdate::Exit;
373 }
374 let new_cursor = if input.up {
375 cursor.prev()
376 } else if input.down {
377 cursor.next()
378 } else {
379 cursor
380 };
381 if new_cursor != cursor {
382 self.phase = MartPhase::MainMenu { cursor: new_cursor };
383 }
384 if input.confirm {
385 match new_cursor {
386 MartTopChoice::Buy => {
387 self.phase = MartPhase::Buy(BuyMenuState::SelectItem { cursor: 0 });
388 }
389 MartTopChoice::Sell => {
390 if backend.bag_len() == 0 {
392 return MartUpdate::Continue;
393 }
394 self.phase = MartPhase::Sell(SellMenuState::SelectItem { cursor: 0 });
395 }
396 MartTopChoice::Quit => {
397 self.phase = MartPhase::Exiting;
398 return MartUpdate::Exit;
399 }
400 }
401 }
402 MartUpdate::Continue
403 }
404
405 fn update_buy<B: MartBackend<Item = I>>(
408 &mut self,
409 input: MenuInput,
410 backend: &mut B,
411 bs: BuyMenuState,
412 ) -> MartUpdate {
413 match bs {
414 BuyMenuState::SelectItem { cursor } => self.update_buy_select(input, backend, cursor),
415 BuyMenuState::Quantity {
416 item_index,
417 quantity,
418 } => self.update_buy_quantity(input, item_index, quantity),
419 BuyMenuState::Confirm {
420 item_index,
421 quantity,
422 selected,
423 } => self.update_buy_confirm(input, backend, item_index, quantity, selected),
424 BuyMenuState::Result {
425 return_to_list, ..
426 } => {
427 if return_to_list {
429 self.phase = MartPhase::Buy(BuyMenuState::SelectItem { cursor: 0 });
430 } else {
431 self.phase = MartPhase::MainMenu {
432 cursor: MartTopChoice::Buy,
433 };
434 }
435 MartUpdate::Continue
436 }
437 }
438 }
439
440 fn update_buy_select<B: MartBackend<Item = I>>(
441 &mut self,
442 input: MenuInput,
443 backend: &B,
444 cursor: usize,
445 ) -> MartUpdate {
446 if input.cancel {
447 self.phase = MartPhase::MainMenu {
448 cursor: MartTopChoice::Buy,
449 };
450 return MartUpdate::Continue;
451 }
452 let len = self.inventory.items().len();
453 let new_cursor = if len == 0 {
454 0
455 } else if input.up {
456 if cursor == 0 {
457 len.saturating_sub(1)
458 } else {
459 cursor - 1
460 }
461 } else if input.down {
462 if cursor + 1 >= len {
463 0
464 } else {
465 cursor + 1
466 }
467 } else {
468 cursor
469 };
470 if new_cursor != cursor {
471 self.phase = MartPhase::Buy(BuyMenuState::SelectItem { cursor: new_cursor });
472 }
473 if input.confirm {
474 if let Some(item) = self.inventory.get(new_cursor) {
475 if backend.can_buy(&item) {
476 self.phase = MartPhase::Buy(BuyMenuState::Quantity {
477 item_index: new_cursor,
478 quantity: 1,
479 });
480 }
481 }
482 }
483 MartUpdate::Continue
484 }
485
486 fn update_buy_quantity(
487 &mut self,
488 input: MenuInput,
489 item_index: usize,
490 mut quantity: u8,
491 ) -> MartUpdate {
492 if input.cancel {
493 self.phase = MartPhase::Buy(BuyMenuState::SelectItem {
495 cursor: item_index,
496 });
497 return MartUpdate::Continue;
498 }
499 if input.up {
500 quantity = if quantity >= 99 { 1 } else { quantity + 1 };
501 } else if input.down {
502 quantity = if quantity <= 1 { 99 } else { quantity - 1 };
503 }
504 if input.confirm {
505 self.phase = MartPhase::Buy(BuyMenuState::Confirm {
506 item_index,
507 quantity,
508 selected: ConfirmChoice::Yes,
509 });
510 } else {
511 self.phase = MartPhase::Buy(BuyMenuState::Quantity {
512 item_index,
513 quantity,
514 });
515 }
516 MartUpdate::Continue
517 }
518
519 fn update_buy_confirm<B: MartBackend<Item = I>>(
520 &mut self,
521 input: MenuInput,
522 backend: &mut B,
523 item_index: usize,
524 quantity: u8,
525 selected: ConfirmChoice,
526 ) -> MartUpdate {
527 if input.cancel {
528 self.phase = MartPhase::Buy(BuyMenuState::Quantity {
530 item_index,
531 quantity,
532 });
533 return MartUpdate::Continue;
534 }
535 let new_selected = if input.up || input.down {
536 selected.toggle()
537 } else {
538 selected
539 };
540 if new_selected != selected {
541 self.phase = MartPhase::Buy(BuyMenuState::Confirm {
542 item_index,
543 quantity,
544 selected: new_selected,
545 });
546 }
547 if input.confirm {
548 match new_selected {
549 ConfirmChoice::Yes => {
550 let item = match self.inventory.get(item_index) {
551 Some(it) => it,
552 None => {
553 self.phase = MartPhase::Buy(BuyMenuState::Result {
554 dialogue: BuyResult::InvalidItem,
555 return_to_list: false,
556 });
557 return MartUpdate::Continue;
558 }
559 };
560 let result = backend.commit_buy(item, quantity);
561 let play_sfx = matches!(result, BuyResult::Success { .. });
562 self.phase = MartPhase::Buy(BuyMenuState::Result {
563 return_to_list: matches!(result, BuyResult::Success { .. }),
564 dialogue: result,
565 });
566 if play_sfx {
567 return MartUpdate::PlaySound(MartSound::Purchase);
568 }
569 }
570 ConfirmChoice::No => {
571 self.phase = MartPhase::Buy(BuyMenuState::SelectItem {
573 cursor: item_index,
574 });
575 }
576 }
577 }
578 MartUpdate::Continue
579 }
580
581 fn update_sell<B: MartBackend<Item = I>>(
584 &mut self,
585 input: MenuInput,
586 backend: &mut B,
587 ss: SellMenuState,
588 ) -> MartUpdate {
589 match ss {
590 SellMenuState::SelectItem { cursor } => {
591 self.update_sell_select(input, backend, cursor)
592 }
593 SellMenuState::Quantity {
594 item_index,
595 quantity,
596 max_quantity,
597 } => self.update_sell_quantity(input, item_index, quantity, max_quantity),
598 SellMenuState::Confirm {
599 item_index,
600 quantity,
601 max_quantity,
602 selected,
603 } => {
604 self.update_sell_confirm(input, backend, item_index, quantity, max_quantity, selected)
605 }
606 SellMenuState::Result {
607 return_to_list, ..
608 } => {
609 if return_to_list {
611 self.phase = MartPhase::Sell(SellMenuState::SelectItem { cursor: 0 });
612 } else {
613 self.phase = MartPhase::MainMenu {
614 cursor: MartTopChoice::Sell,
615 };
616 }
617 MartUpdate::Continue
618 }
619 }
620 }
621
622 fn update_sell_select<B: MartBackend<Item = I>>(
623 &mut self,
624 input: MenuInput,
625 backend: &B,
626 cursor: usize,
627 ) -> MartUpdate {
628 if input.cancel {
629 self.phase = MartPhase::MainMenu {
630 cursor: MartTopChoice::Sell,
631 };
632 return MartUpdate::Continue;
633 }
634 let len = backend.bag_len();
635 let new_cursor = if len == 0 {
636 0
637 } else if input.up {
638 if cursor == 0 {
639 len.saturating_sub(1)
640 } else {
641 cursor - 1
642 }
643 } else if input.down {
644 if cursor + 1 >= len {
645 0
646 } else {
647 cursor + 1
648 }
649 } else {
650 cursor
651 };
652 if new_cursor != cursor {
653 self.phase = MartPhase::Sell(SellMenuState::SelectItem { cursor: new_cursor });
654 }
655 if input.confirm {
656 if let Some((_item, owned)) = backend.bag_entry(new_cursor) {
657 self.phase = MartPhase::Sell(SellMenuState::Quantity {
658 item_index: new_cursor,
659 quantity: 1,
660 max_quantity: owned,
661 });
662 }
663 }
664 MartUpdate::Continue
665 }
666
667 fn update_sell_quantity(
668 &mut self,
669 input: MenuInput,
670 item_index: usize,
671 mut quantity: u8,
672 max_quantity: u8,
673 ) -> MartUpdate {
674 if input.cancel {
675 self.phase = MartPhase::Sell(SellMenuState::SelectItem {
677 cursor: item_index,
678 });
679 return MartUpdate::Continue;
680 }
681 if input.up {
682 quantity = if quantity >= max_quantity {
683 1
684 } else {
685 quantity + 1
686 };
687 } else if input.down {
688 quantity = if quantity <= 1 {
689 max_quantity
690 } else {
691 quantity - 1
692 };
693 }
694 if input.confirm {
695 self.phase = MartPhase::Sell(SellMenuState::Confirm {
696 item_index,
697 quantity,
698 max_quantity,
699 selected: ConfirmChoice::Yes,
700 });
701 } else {
702 self.phase = MartPhase::Sell(SellMenuState::Quantity {
703 item_index,
704 quantity,
705 max_quantity,
706 });
707 }
708 MartUpdate::Continue
709 }
710
711 fn update_sell_confirm<B: MartBackend<Item = I>>(
712 &mut self,
713 input: MenuInput,
714 backend: &mut B,
715 item_index: usize,
716 quantity: u8,
717 max_quantity: u8,
718 selected: ConfirmChoice,
719 ) -> MartUpdate {
720 if input.cancel {
721 self.phase = MartPhase::Sell(SellMenuState::Quantity {
723 item_index,
724 quantity,
725 max_quantity,
726 });
727 return MartUpdate::Continue;
728 }
729 let new_selected = if input.up || input.down {
730 selected.toggle()
731 } else {
732 selected
733 };
734 if new_selected != selected {
735 self.phase = MartPhase::Sell(SellMenuState::Confirm {
736 item_index,
737 quantity,
738 max_quantity,
739 selected: new_selected,
740 });
741 }
742 if input.confirm {
743 match new_selected {
744 ConfirmChoice::Yes => {
745 let result = backend.commit_sell(item_index, quantity);
746 let return_to_list = matches!(result, SellResult::Success { .. });
747 self.phase = MartPhase::Sell(SellMenuState::Result {
748 dialogue: result,
749 return_to_list,
750 });
751 }
752 ConfirmChoice::No => {
753 self.phase = MartPhase::Sell(SellMenuState::SelectItem {
755 cursor: item_index,
756 });
757 }
758 }
759 }
760 MartUpdate::Continue
761 }
762}
763
764#[cfg(test)]
767mod tests {
768 use super::*;
769 use crate::items::{ItemKind, ItemProvider, ItemResult};
770
771 fn menu_up() -> MenuInput {
772 MenuInput {
773 up: true,
774 ..MenuInput::default()
775 }
776 }
777
778 fn menu_down() -> MenuInput {
779 MenuInput {
780 down: true,
781 ..MenuInput::default()
782 }
783 }
784
785 fn menu_confirm() -> MenuInput {
786 MenuInput {
787 confirm: true,
788 ..MenuInput::default()
789 }
790 }
791
792 fn menu_cancel() -> MenuInput {
793 MenuInput {
794 cancel: true,
795 ..MenuInput::default()
796 }
797 }
798
799 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
802 enum Item {
803 Ball,
804 Potion,
805 Antidote,
806 KeyRelic,
807 }
808
809 struct MockMart {
812 money: u32,
813 bag: Vec<(Item, u8)>,
814 slot_capacity: usize,
815 }
816
817 impl MockMart {
818 fn new(money: u32) -> Self {
819 Self {
820 money,
821 bag: Vec::new(),
822 slot_capacity: 20,
823 }
824 }
825
826 fn price(item: Item) -> Option<u32> {
827 match item {
828 Item::Ball => Some(200),
829 Item::Potion => Some(300),
830 Item::Antidote => Some(100),
831 Item::KeyRelic => None,
832 }
833 }
834
835 fn owned(&self, item: Item) -> u32 {
836 self.bag
837 .iter()
838 .filter(|&&(i, _)| i == item)
839 .map(|&(_, q)| q as u32)
840 .sum()
841 }
842 }
843
844 impl MartBackend for MockMart {
845 type Item = Item;
846
847 fn bag_len(&self) -> usize {
848 self.bag.len()
849 }
850
851 fn bag_entry(&self, index: usize) -> Option<(Item, u8)> {
852 self.bag.get(index).copied()
853 }
854
855 fn can_buy(&self, item: &Item) -> bool {
856 Self::price(*item).is_some()
857 }
858
859 fn commit_buy(&mut self, item: Item, quantity: u8) -> BuyResult {
860 let cost = match Self::price(item) {
861 Some(p) => p * quantity as u32,
862 None => return BuyResult::InvalidItem,
863 };
864 if self.money < cost {
865 return BuyResult::NotEnoughMoney;
866 }
867 if !self.bag.iter().any(|&(i, _)| i == item) && self.bag.len() >= self.slot_capacity {
868 return BuyResult::BagFull;
869 }
870 match self.bag.iter_mut().find(|(i, _)| *i == item) {
871 Some(slot) => slot.1 += quantity,
872 None => self.bag.push((item, quantity)),
873 }
874 self.money -= cost;
875 BuyResult::Success { total_cost: cost }
876 }
877
878 fn commit_sell(&mut self, bag_index: usize, quantity: u8) -> SellResult {
879 let Some(&(item, owned)) = self.bag.get(bag_index) else {
880 return SellResult::NotInBag;
881 };
882 if item == Item::KeyRelic {
883 return SellResult::Unsellable;
884 }
885 if quantity > owned {
886 return SellResult::NotInBag;
887 }
888 let value = Self::price(item).map(|p| p / 2 * quantity as u32).unwrap_or(0);
889 if quantity == owned {
890 self.bag.remove(bag_index);
891 } else {
892 self.bag[bag_index].1 -= quantity;
893 }
894 self.money += value;
895 SellResult::Success { total_value: value }
896 }
897 }
898
899 fn stock(items: &[Item]) -> MartStock<Item> {
900 MartStock::new(items.to_vec())
901 }
902
903 #[test]
906 fn mart_stock_basic() {
907 let shop = stock(&[Item::Ball, Item::Potion, Item::Antidote]);
908 assert_eq!(shop.len(), 3);
909 assert!(!shop.is_empty());
910 assert_eq!(shop.get(0), Some(Item::Ball));
911 assert_eq!(shop.get(1), Some(Item::Potion));
912 assert_eq!(shop.get(3), None);
913 }
914
915 #[test]
916 fn mart_stock_empty() {
917 let shop: MartStock<Item> = stock(&[]);
918 assert!(shop.is_empty());
919 assert_eq!(shop.len(), 0);
920 }
921
922 #[test]
923 fn mart_stock_from_strings() {
924 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
925 enum Named {
926 Potion,
927 Antidote,
928 }
929 impl FromStr for Named {
930 type Err = ();
931 fn from_str(s: &str) -> Result<Self, ()> {
932 match s {
933 "Potion" => Ok(Named::Potion),
934 "Antidote" => Ok(Named::Antidote),
935 _ => Err(()),
936 }
937 }
938 }
939 let items = vec!["Potion".to_string(), "Antidote".to_string()];
940 let shop: MartStock<Named> = MartStock::from_strings(&items).unwrap();
941 assert_eq!(shop.items(), &[Named::Potion, Named::Antidote]);
942
943 let bad = vec!["Potion".to_string(), "NotAnItem".to_string()];
944 let err = MartStock::<Named>::from_strings(&bad).unwrap_err();
945 assert_eq!(err, "NotAnItem");
946
947 let empty: Vec<String> = vec![];
948 assert!(MartStock::<Named>::from_strings(&empty).unwrap().is_empty());
949 }
950
951 #[test]
954 fn mart_buy_happy_path() {
955 let mut mart = MartState::new(stock(&[Item::Ball, Item::Potion, Item::Antidote]));
956 let mut p = MockMart::new(1000);
957
958 assert!(matches!(
960 mart.phase,
961 MartPhase::MainMenu {
962 cursor: MartTopChoice::Buy
963 }
964 ));
965
966 assert_eq!(mart.update_frame(menu_confirm(), &mut p), MartUpdate::Continue);
968 assert!(matches!(
969 mart.phase,
970 MartPhase::Buy(BuyMenuState::SelectItem { cursor: 0 })
971 ));
972
973 mart.update_frame(menu_down(), &mut p);
975 assert_eq!(mart.update_frame(menu_confirm(), &mut p), MartUpdate::Continue);
976 assert!(matches!(
977 mart.phase,
978 MartPhase::Buy(BuyMenuState::Quantity {
979 item_index: 1,
980 quantity: 1,
981 })
982 ));
983
984 mart.update_frame(menu_up(), &mut p);
986 mart.update_frame(menu_up(), &mut p);
987 assert_eq!(mart.update_frame(menu_confirm(), &mut p), MartUpdate::Continue);
988 assert!(matches!(
989 mart.phase,
990 MartPhase::Buy(BuyMenuState::Confirm {
991 item_index: 1,
992 quantity: 3,
993 selected: ConfirmChoice::Yes,
994 })
995 ));
996
997 assert_eq!(
999 mart.update_frame(menu_confirm(), &mut p),
1000 MartUpdate::PlaySound(MartSound::Purchase)
1001 );
1002 assert_eq!(p.money, 100); assert_eq!(p.owned(Item::Potion), 3);
1004 assert!(matches!(
1005 mart.phase,
1006 MartPhase::Buy(BuyMenuState::Result {
1007 dialogue: BuyResult::Success { total_cost: 900 },
1008 return_to_list: true,
1009 })
1010 ));
1011
1012 assert_eq!(
1014 mart.update_frame(MenuInput::default(), &mut p),
1015 MartUpdate::Continue
1016 );
1017 assert!(matches!(
1018 mart.phase,
1019 MartPhase::Buy(BuyMenuState::SelectItem { cursor: 0 })
1020 ));
1021 }
1022
1023 #[test]
1024 fn mart_buy_not_enough_money() {
1025 let mut mart = MartState::new(stock(&[Item::Potion]));
1026 let mut p = MockMart::new(100);
1027
1028 mart.update_frame(menu_confirm(), &mut p);
1030 mart.update_frame(menu_confirm(), &mut p);
1031 mart.update_frame(menu_confirm(), &mut p);
1032 assert_eq!(mart.update_frame(menu_confirm(), &mut p), MartUpdate::Continue);
1033 assert!(matches!(
1034 mart.phase,
1035 MartPhase::Buy(BuyMenuState::Result {
1036 dialogue: BuyResult::NotEnoughMoney,
1037 return_to_list: false,
1038 })
1039 ));
1040 assert_eq!(p.money, 100); assert_eq!(
1044 mart.update_frame(MenuInput::default(), &mut p),
1045 MartUpdate::Continue
1046 );
1047 assert!(matches!(mart.phase, MartPhase::MainMenu { .. }));
1048 }
1049
1050 #[test]
1051 fn mart_buy_bag_full() {
1052 let mut mart = MartState::new(stock(&[Item::Potion]));
1053 let mut p = MockMart::new(999999);
1054 p.slot_capacity = 1;
1055 p.bag.push((Item::Ball, 1));
1056
1057 mart.update_frame(menu_confirm(), &mut p);
1058 mart.update_frame(menu_confirm(), &mut p);
1059 mart.update_frame(menu_confirm(), &mut p);
1060 assert_eq!(mart.update_frame(menu_confirm(), &mut p), MartUpdate::Continue);
1061 assert!(matches!(
1062 mart.phase,
1063 MartPhase::Buy(BuyMenuState::Result {
1064 dialogue: BuyResult::BagFull,
1065 return_to_list: false,
1066 })
1067 ));
1068 assert_eq!(p.money, 999999);
1069
1070 assert_eq!(
1072 mart.update_frame(MenuInput::default(), &mut p),
1073 MartUpdate::Continue
1074 );
1075 assert!(matches!(mart.phase, MartPhase::MainMenu { .. }));
1076 }
1077
1078 #[test]
1079 fn mart_buy_cancel_backout_from_quantity() {
1080 let mut mart = MartState::new(stock(&[Item::Potion]));
1081 let mut p = MockMart::new(1000);
1082
1083 mart.update_frame(menu_confirm(), &mut p); mart.update_frame(menu_confirm(), &mut p); assert!(matches!(
1086 mart.phase,
1087 MartPhase::Buy(BuyMenuState::Quantity {
1088 item_index: 0,
1089 quantity: 1,
1090 })
1091 ));
1092
1093 assert_eq!(mart.update_frame(menu_cancel(), &mut p), MartUpdate::Continue);
1095 assert!(matches!(
1096 mart.phase,
1097 MartPhase::Buy(BuyMenuState::SelectItem { cursor: 0 })
1098 ));
1099 }
1100
1101 #[test]
1102 fn mart_buy_cancel_backout_from_confirm() {
1103 let mut mart = MartState::new(stock(&[Item::Potion]));
1104 let mut p = MockMart::new(1000);
1105
1106 mart.update_frame(menu_confirm(), &mut p); mart.update_frame(menu_confirm(), &mut p); mart.update_frame(menu_confirm(), &mut p); assert!(matches!(
1110 mart.phase,
1111 MartPhase::Buy(BuyMenuState::Confirm {
1112 item_index: 0,
1113 quantity: 1,
1114 selected: ConfirmChoice::Yes,
1115 })
1116 ));
1117
1118 assert_eq!(mart.update_frame(menu_cancel(), &mut p), MartUpdate::Continue);
1120 assert!(matches!(
1121 mart.phase,
1122 MartPhase::Buy(BuyMenuState::Quantity {
1123 item_index: 0,
1124 quantity: 1,
1125 })
1126 ));
1127 }
1128
1129 #[test]
1130 fn mart_buy_quantity_wrap() {
1131 let mut mart = MartState::new(stock(&[Item::Potion]));
1132 let mut p = MockMart::new(1000);
1133
1134 mart.update_frame(menu_confirm(), &mut p); mart.update_frame(menu_confirm(), &mut p); mart.update_frame(menu_down(), &mut p);
1139 assert!(matches!(
1140 mart.phase,
1141 MartPhase::Buy(BuyMenuState::Quantity {
1142 item_index: 0,
1143 quantity: 99,
1144 })
1145 ));
1146
1147 mart.update_frame(menu_up(), &mut p);
1149 assert!(matches!(
1150 mart.phase,
1151 MartPhase::Buy(BuyMenuState::Quantity {
1152 item_index: 0,
1153 quantity: 1,
1154 })
1155 ));
1156
1157 mart.update_frame(menu_up(), &mut p);
1159 mart.update_frame(menu_up(), &mut p);
1160 assert!(matches!(
1161 mart.phase,
1162 MartPhase::Buy(BuyMenuState::Quantity {
1163 item_index: 0,
1164 quantity: 3,
1165 })
1166 ));
1167 }
1168
1169 #[test]
1170 fn mart_buy_unbuyable_item_stays_on_select() {
1171 let mut mart = MartState::new(stock(&[Item::KeyRelic]));
1174 let mut p = MockMart::new(1000);
1175 mart.update_frame(menu_confirm(), &mut p); assert_eq!(mart.update_frame(menu_confirm(), &mut p), MartUpdate::Continue);
1177 assert!(matches!(
1178 mart.phase,
1179 MartPhase::Buy(BuyMenuState::SelectItem { cursor: 0 })
1180 ));
1181 }
1182
1183 #[test]
1186 fn mart_sell_happy_path() {
1187 let mut mart = MartState::new(stock(&[Item::Ball]));
1188 let mut p = MockMart::new(0);
1189 p.bag.push((Item::Potion, 5));
1190
1191 mart.update_frame(menu_down(), &mut p);
1193 assert_eq!(mart.update_frame(menu_confirm(), &mut p), MartUpdate::Continue);
1194 assert!(matches!(
1195 mart.phase,
1196 MartPhase::Sell(SellMenuState::SelectItem { cursor: 0 })
1197 ));
1198
1199 assert_eq!(mart.update_frame(menu_confirm(), &mut p), MartUpdate::Continue);
1201 assert!(matches!(
1202 mart.phase,
1203 MartPhase::Sell(SellMenuState::Quantity {
1204 item_index: 0,
1205 quantity: 1,
1206 max_quantity: 5,
1207 })
1208 ));
1209
1210 mart.update_frame(menu_up(), &mut p);
1212 assert_eq!(mart.update_frame(menu_confirm(), &mut p), MartUpdate::Continue);
1213 assert!(matches!(
1214 mart.phase,
1215 MartPhase::Sell(SellMenuState::Confirm {
1216 item_index: 0,
1217 quantity: 2,
1218 max_quantity: 5,
1219 selected: ConfirmChoice::Yes,
1220 })
1221 ));
1222
1223 assert_eq!(mart.update_frame(menu_confirm(), &mut p), MartUpdate::Continue);
1225 assert_eq!(p.money, 300); assert_eq!(p.owned(Item::Potion), 3);
1227 assert!(matches!(
1228 mart.phase,
1229 MartPhase::Sell(SellMenuState::Result {
1230 dialogue: SellResult::Success { total_value: 300 },
1231 return_to_list: true,
1232 })
1233 ));
1234
1235 assert_eq!(
1237 mart.update_frame(MenuInput::default(), &mut p),
1238 MartUpdate::Continue
1239 );
1240 assert!(matches!(
1241 mart.phase,
1242 MartPhase::Sell(SellMenuState::SelectItem { cursor: 0 })
1243 ));
1244 }
1245
1246 #[test]
1247 fn mart_sell_empty_bag_stays_on_main_menu() {
1248 let mut mart = MartState::new(stock(&[Item::Ball]));
1249 let mut p = MockMart::new(0);
1250 mart.update_frame(menu_down(), &mut p); assert_eq!(mart.update_frame(menu_confirm(), &mut p), MartUpdate::Continue);
1252 assert!(matches!(
1253 mart.phase,
1254 MartPhase::MainMenu {
1255 cursor: MartTopChoice::Sell
1256 }
1257 ));
1258 }
1259
1260 #[test]
1261 fn mart_sell_unsellable_item() {
1262 let mut mart = MartState::new(stock(&[Item::Ball]));
1263 let mut p = MockMart::new(0);
1264 p.bag.push((Item::KeyRelic, 1));
1265
1266 mart.update_frame(menu_down(), &mut p);
1267 mart.update_frame(menu_confirm(), &mut p); mart.update_frame(menu_confirm(), &mut p); mart.update_frame(menu_confirm(), &mut p); assert_eq!(mart.update_frame(menu_confirm(), &mut p), MartUpdate::Continue);
1271 assert!(matches!(
1272 mart.phase,
1273 MartPhase::Sell(SellMenuState::Result {
1274 dialogue: SellResult::Unsellable,
1275 return_to_list: false,
1276 })
1277 ));
1278 assert_eq!(p.money, 0);
1279 assert_eq!(p.owned(Item::KeyRelic), 1);
1280 }
1281
1282 #[test]
1283 fn mart_sell_quantity_wrap() {
1284 let mut mart = MartState::new(stock(&[Item::Ball]));
1285 let mut p = MockMart::new(0);
1286 p.bag.push((Item::Potion, 3));
1287
1288 mart.update_frame(menu_down(), &mut p); mart.update_frame(menu_confirm(), &mut p); mart.update_frame(menu_confirm(), &mut p); mart.update_frame(menu_down(), &mut p);
1294 assert!(matches!(
1295 mart.phase,
1296 MartPhase::Sell(SellMenuState::Quantity {
1297 quantity: 3,
1298 max_quantity: 3,
1299 ..
1300 })
1301 ));
1302
1303 mart.update_frame(menu_up(), &mut p);
1305 assert!(matches!(
1306 mart.phase,
1307 MartPhase::Sell(SellMenuState::Quantity {
1308 quantity: 1,
1309 max_quantity: 3,
1310 ..
1311 })
1312 ));
1313 }
1314
1315 #[test]
1318 fn mart_top_menu_quit_returns_exit() {
1319 let mut mart = MartState::new(stock(&[Item::Potion]));
1320 let mut p = MockMart::new(1000);
1321
1322 mart.update_frame(menu_down(), &mut p);
1324 mart.update_frame(menu_down(), &mut p);
1325 assert!(matches!(
1326 mart.phase,
1327 MartPhase::MainMenu {
1328 cursor: MartTopChoice::Quit,
1329 }
1330 ));
1331
1332 assert_eq!(mart.update_frame(menu_confirm(), &mut p), MartUpdate::Exit);
1334 assert!(matches!(mart.phase, MartPhase::Exiting));
1335 }
1336
1337 #[test]
1338 fn mart_cancel_at_main_menu_exits() {
1339 let mut mart = MartState::new(stock(&[Item::Potion]));
1340 let mut p = MockMart::new(1000);
1341
1342 assert_eq!(mart.update_frame(menu_cancel(), &mut p), MartUpdate::Exit);
1343 assert!(matches!(mart.phase, MartPhase::Exiting));
1344 assert_eq!(mart.update_frame(menu_confirm(), &mut p), MartUpdate::Exit);
1346 }
1347
1348 #[test]
1349 fn mart_main_menu_navigation_wraps() {
1350 let mut mart = MartState::new(stock(&[Item::Potion]));
1351 let mut p = MockMart::new(1000);
1352
1353 mart.update_frame(menu_up(), &mut p);
1355 assert!(matches!(
1356 mart.phase,
1357 MartPhase::MainMenu {
1358 cursor: MartTopChoice::Quit,
1359 }
1360 ));
1361
1362 mart.update_frame(menu_down(), &mut p);
1364 assert!(matches!(
1365 mart.phase,
1366 MartPhase::MainMenu {
1367 cursor: MartTopChoice::Buy,
1368 }
1369 ));
1370 }
1371
1372 #[test]
1373 fn mart_confirm_no_returns_to_select_item() {
1374 let mut mart = MartState::new(stock(&[Item::Potion]));
1375 let mut p = MockMart::new(1000);
1376
1377 mart.update_frame(menu_confirm(), &mut p); mart.update_frame(menu_confirm(), &mut p); mart.update_frame(menu_confirm(), &mut p); mart.update_frame(menu_up(), &mut p);
1383 assert!(matches!(
1384 mart.phase,
1385 MartPhase::Buy(BuyMenuState::Confirm {
1386 selected: ConfirmChoice::No,
1387 ..
1388 })
1389 ));
1390
1391 let old_money = p.money;
1393 assert_eq!(mart.update_frame(menu_confirm(), &mut p), MartUpdate::Continue);
1394 assert_eq!(p.money, old_money);
1395 assert!(matches!(
1396 mart.phase,
1397 MartPhase::Buy(BuyMenuState::SelectItem { cursor: 0 })
1398 ));
1399 }
1400
1401 struct DriverGame;
1404
1405 impl ItemProvider for DriverGame {
1406 type Item = Item;
1407 type Effect = ();
1408 type Monster = ();
1409 type CustomKind = ();
1410 fn item_name(&self, _i: &Item) -> &str {
1411 "X"
1412 }
1413 fn item_description(&self, _i: &Item) -> &str {
1414 "X"
1415 }
1416 fn item_effect(&self, _i: &Item) {}
1417 fn item_price(&self, item: &Item) -> u32 {
1418 MockMart::price(*item).unwrap_or(0)
1419 }
1420 fn can_use_outside_battle(&self, _i: &Item) -> bool {
1421 true
1422 }
1423 fn can_use_in_battle(&self, _i: &Item) -> bool {
1424 true
1425 }
1426 fn use_on_monster(&self, _i: &Item, _m: &mut ()) -> ItemResult {
1427 ItemResult::NoEffect
1428 }
1429 fn consume(&self, _i: &Item) -> bool {
1430 true
1431 }
1432 fn item_kind(&self, _i: &Item) -> ItemKind<()> {
1433 ItemKind::Consumable
1434 }
1435 }
1436
1437 impl ShopProvider for DriverGame {
1438 type Item = Item;
1439 type ShopId = u8;
1440 fn shop_inventory(&self, _shop_id: &u8) -> Vec<(Item, u32)> {
1441 vec![(Item::Potion, 300)]
1442 }
1443 fn shop_name(&self, _shop_id: &u8) -> &str {
1444 "Mart"
1445 }
1446 fn buy_price(&self, item: &Item) -> u32 {
1447 MockMart::price(*item).unwrap_or(0)
1448 }
1449 fn can_sell(&self, item: &Item) -> bool {
1451 *item != Item::KeyRelic
1452 }
1453 }
1454
1455 #[test]
1456 fn mart_driver_commit_buy_routes_through_buy_driver() {
1457 let game = DriverGame;
1458 let mut bag = Inventory::<Item, 20>::new();
1459 let mut money = 1000u32;
1460 let mut backend = MartDriver {
1461 provider: &game,
1462 shop_id: 0u8,
1463 money: &mut money,
1464 bag: &mut bag,
1465 };
1466 assert_eq!(
1467 backend.commit_buy(Item::Potion, 2),
1468 BuyResult::Success { total_cost: 600 }
1469 );
1470 assert_eq!(*backend.money, 400);
1471 assert!(backend.bag.contains(&Item::Potion, 2));
1472
1473 assert_eq!(
1475 backend.commit_buy(Item::Potion, 99),
1476 BuyResult::NotEnoughMoney
1477 );
1478 assert_eq!(*backend.money, 400);
1479 }
1480
1481 #[test]
1482 fn mart_driver_commit_sell_routes_through_sell_driver() {
1483 let game = DriverGame;
1484 let mut bag = Inventory::<Item, 20>::new();
1485 bag.add(Item::Potion, 5).unwrap();
1486 bag.add(Item::KeyRelic, 1).unwrap();
1487 let mut money = 0u32;
1488 let mut backend = MartDriver {
1489 provider: &game,
1490 shop_id: 0u8,
1491 money: &mut money,
1492 bag: &mut bag,
1493 };
1494 assert_eq!(
1496 backend.commit_sell(0, 2),
1497 SellResult::Success { total_value: 300 }
1498 );
1499 assert_eq!(*backend.money, 300);
1500 assert!(backend.bag.contains(&Item::Potion, 3));
1501
1502 assert_eq!(backend.commit_sell(1, 1), SellResult::Unsellable);
1504 assert!(backend.bag.contains(&Item::KeyRelic, 1));
1505
1506 assert_eq!(backend.commit_sell(9, 1), SellResult::NotInBag);
1508 }
1509
1510 #[test]
1511 fn mart_state_full_flow_via_mart_driver() {
1512 let game = DriverGame;
1514 let mut bag = Inventory::<Item, 20>::new();
1515 let mut money = 1000u32;
1516 let mut backend = MartDriver {
1517 provider: &game,
1518 shop_id: 0u8,
1519 money: &mut money,
1520 bag: &mut bag,
1521 };
1522 let mut mart = MartState::new(stock(&[Item::Potion]));
1523
1524 mart.update_frame(menu_confirm(), &mut backend); mart.update_frame(menu_confirm(), &mut backend); mart.update_frame(menu_confirm(), &mut backend); assert_eq!(
1528 mart.update_frame(menu_confirm(), &mut backend),
1529 MartUpdate::PlaySound(MartSound::Purchase)
1530 );
1531 assert_eq!(*backend.money, 700);
1532 assert!(backend.bag.contains(&Item::Potion, 1));
1533 }
1534}