From 1ceeaaeca08e66ab6507bc4a2ead97ddca05e170 Mon Sep 17 00:00:00 2001 From: EZ Date: Sun, 5 Apr 2026 13:41:29 -0700 Subject: [PATCH] Add Recipe Purchaser Add recipe purchaser to Pokopia Cloud Island Reset --- .../Inference/PokemonPokopia_PCDetection.cpp | 219 +++++++++++++++++- .../Inference/PokemonPokopia_PCDetection.h | 111 +++++++++ .../PokemonPokopia_SelectionArrowDetector.cpp | 7 +- .../PokemonPokopia_CloudIslandReset.cpp | 106 ++++++++- .../PokemonPokopia_CloudIslandReset.h | 10 +- .../Programs/PokemonPokopia_PCNavigation.cpp | 95 ++++++++ .../Programs/PokemonPokopia_PCNavigation.h | 29 +++ 7 files changed, 568 insertions(+), 9 deletions(-) diff --git a/SerialPrograms/Source/PokemonPokopia/Inference/PokemonPokopia_PCDetection.cpp b/SerialPrograms/Source/PokemonPokopia/Inference/PokemonPokopia_PCDetection.cpp index fd981a03c3..8a35957c90 100644 --- a/SerialPrograms/Source/PokemonPokopia/Inference/PokemonPokopia_PCDetection.cpp +++ b/SerialPrograms/Source/PokemonPokopia/Inference/PokemonPokopia_PCDetection.cpp @@ -8,8 +8,12 @@ #include "Common/Cpp/Exceptions.h" #include "Kernels/Waterfill/Kernels_Waterfill_Types.h" #include "CommonFramework/GlobalSettingsPanel.h" +#include "CommonFramework/Tools/GlobalThreadPools.h" #include "CommonTools/ImageMatch/WaterfillTemplateMatcher.h" #include "CommonTools/Images/WaterfillUtilities.h" +#include "CommonTools/Images/SolidColorTest.h" +#include "CommonTools/OCR/OCR_NumberReader.h" +#include "CommonTools/OCR/OCR_RawOCR.h" #include "PokemonPokopia_PCDetection.h" namespace PokemonAutomation{ @@ -38,8 +42,6 @@ class InfoIconMatcher : public ImageMatch::WaterfillTemplateMatcher{ } }; - - InfoIconDetector::InfoIconDetector( Color color, VideoOverlay* overlay, @@ -209,6 +211,219 @@ bool StampDetector::detect(const ImageViewRGB32& screen){ return found; } +class RecipeIconMatcher : public ImageMatch::WaterfillTemplateMatcher{ +public: + // Match the yellow negative space for the recipe icon + RecipeIconMatcher(const char* path) + : WaterfillTemplateMatcher( + path, + Color(210, 180, 0), Color(255, 250, 160), 2100 + ) + { + m_aspect_ratio_lower = 0.9; + m_aspect_ratio_upper = 1.1; + m_area_ratio_lower = 0.85; + m_area_ratio_upper = 1.1; + } + + static const RecipeIconMatcher& single_matcher(){ + static RecipeIconMatcher matcher("PokemonPokopia/Recipe.png"); + return matcher; + } + static const RecipeIconMatcher& double_matcher(){ + static RecipeIconMatcher matcher("PokemonPokopia/DoubleRecipe.png"); + return matcher; + } + static const RecipeIconMatcher& triple_matcher(){ + static RecipeIconMatcher matcher("PokemonPokopia/TripleRecipe.png"); + return matcher; + } + static const RecipeIconMatcher& quad_matcher(){ + static RecipeIconMatcher matcher("PokemonPokopia/QuadRecipe.png"); + return matcher; + } +}; + +RecipeIconDetector::RecipeIconDetector( + Color color, + VideoOverlay* overlay, + const ImageFloatBox& box +) + : m_color(color) + , m_overlay(overlay) + , m_arrow_box(box) +{} +void RecipeIconDetector::make_overlays(VideoOverlaySet& items) const{ + items.add(m_color, m_arrow_box); +} +bool RecipeIconDetector::detect(const ImageViewRGB32& screen){ + double screen_rel_size = (screen.height() / 1080.0); + double screen_rel_size_2 = screen_rel_size * screen_rel_size; + + double min_area_1080p = 500; + double rmsd_threshold = 95; + size_t min_area = size_t(screen_rel_size_2 * min_area_1080p); + + const std::vector> FILTERS = { + {0xffd2b400, 0xfffffaa0} // RGB(210, 180, 0), RGB(255, 250, 160) + }; + + const std::vector> matchers = { + {RecipeIconMatcher::single_matcher(), RecipeType::SINGLE}, + {RecipeIconMatcher::double_matcher(), RecipeType::DOUBLE}, + {RecipeIconMatcher::triple_matcher(), RecipeType::TRIPLE}, + {RecipeIconMatcher::quad_matcher(), RecipeType::QUAD}, + }; + + bool found = false; + m_recipe_type = RecipeType::NOT_RECIPE; + + for (const auto& matcher_info : matchers) { + found = match_template_by_waterfill( + screen.size(), + extract_box_reference(screen, m_arrow_box), + matcher_info.first, + FILTERS, + {min_area, SIZE_MAX}, + rmsd_threshold, + [&](Kernels::Waterfill::WaterfillObject& object) -> bool { + m_last_detected = translate_to_parent(screen, m_arrow_box, object); + return true; + } + ); + if (found) { + m_recipe_type = matcher_info.second; + break; + } + } + + if (m_overlay){ + if (found){ + m_last_detected_box.emplace(*m_overlay, m_last_detected, COLOR_GREEN); + }else{ + m_last_detected_box.reset(); + } + } + + return found; +} + +CoinCountDetector::CoinCountDetector(Logger& logger) +: m_logger(logger) +// location in the shop menu +, m_coin_count_box{0.900000, 0.053000, 0.075000, 0.035000} +{} + +void CoinCountDetector::make_overlays(VideoOverlaySet& items) const{ + items.add(COLOR_WHITE, m_coin_count_box); +} + +bool CoinCountDetector::detect(const ImageViewRGB32& screen){ + const ImageViewRGB32 coin_image_crop = extract_box_reference(screen, m_coin_count_box); + + const bool text_inside_range = true; + const bool prioritize_numeric_only_results = true; + const size_t width_max = SIZE_MAX; + // The coin crop includes the "," in coin numbers like "1,000". + // We have to use `min_digit_area` to filter out "," when doing OCR. + // The min digit area computation is that any dot with size smaller than coin_image_crop.height()/5 is filtered out when OCR. + const size_t min_digit_area = coin_image_crop.height()*coin_image_crop.height() / 25; + m_coin_count = 0; + const std::vector> filters = { + {0xff808080, 0xffffffff}, + {0xffa0a0a0, 0xffffffff}, + {0xffc0c0c0, 0xffffffff}, + {0xffe0e0e0, 0xffffffff}, + {0xfff0f0f0, 0xffffffff}, + }; + int number = OCR::read_number_waterfill_multifilter( + m_logger, + GlobalThreadPools::computation_realtime(), + coin_image_crop, filters, + text_inside_range, prioritize_numeric_only_results, width_max, min_digit_area + ); + if (number <= 0 || number > 999999){ + return false; + } + m_coin_count = static_cast(number); + return true; +} + +CoinCountWatcher::CoinCountWatcher(Logger& logger) +: CoinCountDetector(logger), VisualInferenceCallback("CoinCountWatcher") +{} + +void CoinCountWatcher::make_overlays(VideoOverlaySet& items) const{ + CoinCountDetector::make_overlays(items); +} + +bool CoinCountWatcher::process_frame(const ImageViewRGB32& frame, WallClock timestamp){ + return detect(frame); +} + +class CoinIconMatcher : public ImageMatch::WaterfillTemplateMatcher{ +public: + CoinIconMatcher() + : WaterfillTemplateMatcher( + "PokemonPokopia/Coin.png", + Color(210, 150, 30), Color(255, 240, 140), 900 + ) + { + m_aspect_ratio_lower = 0.9; + m_aspect_ratio_upper = 1.1; + m_area_ratio_lower = 0.85; + m_area_ratio_upper = 1.1; + } +}; + +CoinIconDetector::CoinIconDetector( + Color color, + VideoOverlay* overlay, + const ImageFloatBox& box +) + : m_color(color) + , m_overlay(overlay) + , m_arrow_box(box) +{} +void CoinIconDetector::make_overlays(VideoOverlaySet& items) const{ + items.add(m_color, m_arrow_box); +} +bool CoinIconDetector::detect(const ImageViewRGB32& screen){ + double screen_rel_size = (screen.height() / 1080.0); + double screen_rel_size_2 = screen_rel_size * screen_rel_size; + + double min_area_1080p = 200; + double rmsd_threshold = 100; + size_t min_area = size_t(screen_rel_size_2 * min_area_1080p); + + const std::vector> FILTERS = { + {0xffd2961e, 0xfffff08c} // RGB(210, 150, 30), RGB(255, 240, 140) + }; + + bool found = match_template_by_waterfill( + screen.size(), + extract_box_reference(screen, m_arrow_box), + CoinIconMatcher(), + FILTERS, + {min_area, SIZE_MAX}, + rmsd_threshold, + [&](Kernels::Waterfill::WaterfillObject& object) -> bool { + m_last_detected = translate_to_parent(screen, m_arrow_box, object); + return true; + } + ); + + if (m_overlay){ + if (found){ + m_last_detected_box.emplace(*m_overlay, m_last_detected, COLOR_GREEN); + }else{ + m_last_detected_box.reset(); + } + } + + return found; +} + } diff --git a/SerialPrograms/Source/PokemonPokopia/Inference/PokemonPokopia_PCDetection.h b/SerialPrograms/Source/PokemonPokopia/Inference/PokemonPokopia_PCDetection.h index 1ad2ca16d1..9ae6ccc22b 100644 --- a/SerialPrograms/Source/PokemonPokopia/Inference/PokemonPokopia_PCDetection.h +++ b/SerialPrograms/Source/PokemonPokopia/Inference/PokemonPokopia_PCDetection.h @@ -107,6 +107,117 @@ class StampWatcher : public DetectorToFinder{ {} }; +enum class RecipeType{ + SINGLE, + DOUBLE, + TRIPLE, + QUAD, + NOT_RECIPE +}; + +class RecipeIconDetector : public StaticScreenDetector{ +public: + RecipeIconDetector( + Color color, + VideoOverlay* overlay, + const ImageFloatBox& box + ); + + const ImageFloatBox& last_detected() const { return m_last_detected; } + const RecipeType& recipe_type() const { return m_recipe_type; } + + virtual void make_overlays(VideoOverlaySet& items) const override; + + // This is not const so that detectors can save/cache state. + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + friend class RecipeWatcher; + + const Color m_color; + VideoOverlay* m_overlay; + const ImageFloatBox m_arrow_box; + + RecipeType m_recipe_type; + ImageFloatBox m_last_detected; + std::optional m_last_detected_box; +}; +class RecipeIconWatcher : public DetectorToFinder{ +public: + RecipeIconWatcher( + Color color, + VideoOverlay* overlay, + const ImageFloatBox& box, + std::chrono::milliseconds hold_duration = std::chrono::milliseconds(250) + ) + : DetectorToFinder("RecipeIconWatcher", hold_duration, color, overlay, box) + {} +}; + +class CoinCountDetector : public StaticScreenDetector{ +public: + CoinCountDetector(Logger& logger); + + virtual void make_overlays(VideoOverlaySet& items) const override; + + // This is not const so that detectors can save/cache state. + virtual bool detect(const ImageViewRGB32& screen) override; + + uint32_t coin_count() const { return m_coin_count; } + +private: + Logger& m_logger; + ImageFloatBox m_coin_count_box; + + uint32_t m_coin_count = 0; +}; + +class CoinCountWatcher : public CoinCountDetector, public VisualInferenceCallback{ +public: + CoinCountWatcher(Logger& logger); + + virtual void make_overlays(VideoOverlaySet& items) const override; + + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; +}; + +class CoinIconDetector : public StaticScreenDetector{ +public: + CoinIconDetector( + Color color, + VideoOverlay* overlay, + const ImageFloatBox& box + ); + + const ImageFloatBox& last_detected() const { return m_last_detected; } + + virtual void make_overlays(VideoOverlaySet& items) const override; + + // This is not const so that detectors can save/cache state. + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + friend class CoinIconWatcher; + + const Color m_color; + VideoOverlay* m_overlay; + const ImageFloatBox m_arrow_box; + + ImageFloatBox m_last_detected; + std::optional m_last_detected_box; +}; +class CoinIconWatcher : public DetectorToFinder{ +public: + CoinIconWatcher( + Color color, + VideoOverlay* overlay, + const ImageFloatBox& box, + std::chrono::milliseconds hold_duration = std::chrono::milliseconds(250) + ) + : DetectorToFinder("CoinIconWatcher", hold_duration, color, overlay, box) + {} +}; + } diff --git a/SerialPrograms/Source/PokemonPokopia/Inference/PokemonPokopia_SelectionArrowDetector.cpp b/SerialPrograms/Source/PokemonPokopia/Inference/PokemonPokopia_SelectionArrowDetector.cpp index e17387bfca..d74d5369ac 100644 --- a/SerialPrograms/Source/PokemonPokopia/Inference/PokemonPokopia_SelectionArrowDetector.cpp +++ b/SerialPrograms/Source/PokemonPokopia/Inference/PokemonPokopia_SelectionArrowDetector.cpp @@ -20,8 +20,8 @@ class SelectionArrowMatcher : public ImageMatch::WaterfillTemplateMatcher{ public: SelectionArrowMatcher(const char* path) : WaterfillTemplateMatcher( - path, - Color(230, 100, 30), Color(255, 190, 190), + path, + Color(240, 170, 0), Color(255, 255, 200), 100 ) { @@ -76,8 +76,7 @@ bool SelectionArrowDetector::detect(const ImageViewRGB32& screen){ size_t min_area = size_t(screen_rel_size_2 * min_area_1080p); const std::vector> FILTERS = { - {0xffe6641e, 0xffffbebe}, // RGB(230, 100, 30), RGB(255, 190, 190) - {0xffe65a46, 0xffff9696} // RGB(230, 90, 70), RGB(255, 150, 150) + {0xfff0aa00, 0xffffffc8} // RGB(240, 170, 0), RGB(255, 255, 200) }; bool found = match_template_by_waterfill( diff --git a/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_CloudIslandReset.cpp b/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_CloudIslandReset.cpp index 0dc78de5fb..7e5cd23ff7 100644 --- a/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_CloudIslandReset.cpp +++ b/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_CloudIslandReset.cpp @@ -45,15 +45,18 @@ class CloudIslandReset_Descriptor::Stats : public StatsTracker{ Stats() : resets(m_stats["Resets"]) , mew_stamps(m_stats["Mew Stamps"]) + , recipes_purchased(m_stats["Recipes Purchased"]) , errors(m_stats["Errors"]) { m_display_order.emplace_back("Resets"); m_display_order.emplace_back("Mew Stamps"); + m_display_order.emplace_back("Recipes Purchased"); m_display_order.emplace_back("Errors", HIDDEN_IF_ZERO); } std::atomic& resets; std::atomic& mew_stamps; + std::atomic& recipes_purchased; std::atomic& errors; }; std::unique_ptr CloudIslandReset_Descriptor::make_stats() const{ @@ -70,6 +73,25 @@ CloudIslandReset::CloudIslandReset() 500, 0 ) + , COLLECT_MEW_STAMPS( + "Collect Mew Stamps:
" + "Whether to collect Mew stamps from the Cloud Islands", + LockMode::UNLOCK_WHILE_RUNNING, + true + ) + , BUY_RECIPES( + "Buy Recipes:
" + "Whether to buy recipes from the Cloud Island shop.", + LockMode::UNLOCK_WHILE_RUNNING, + false + ) + , SPEND_LIMIT( + "Spend Limit:
" + "The limit for how many Life Coins should remain after buying recipes.", + LockMode::UNLOCK_WHILE_RUNNING, + 5000, + 0 + ) , GO_HOME_WHEN_DONE(false) , NOTIFICATION_STATUS_UPDATE("Status Update", true, false, std::chrono::seconds(3600)) , NOTIFICATIONS({ @@ -80,6 +102,9 @@ CloudIslandReset::CloudIslandReset() { PA_ADD_OPTION(STOP_AFTER_CURRENT); PA_ADD_OPTION(NUM_RESETS); + PA_ADD_OPTION(COLLECT_MEW_STAMPS); + PA_ADD_OPTION(BUY_RECIPES); + PA_ADD_OPTION(SPEND_LIMIT); PA_ADD_OPTION(GO_HOME_WHEN_DONE); PA_ADD_OPTION(NOTIFICATIONS); } @@ -290,6 +315,67 @@ bool CloudIslandReset::add_todays_stamp(SingleSwitchProgramEnvironment& env, Pro return true; } +bool CloudIslandReset::buy_recipes(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + CloudIslandReset_Descriptor::Stats& stats = env.current_stats(); + CoinCountWatcher coin_watcher(env.console); + int ret; + + open_menu_option(env.console, context, PCMenuOption::SHOP); + env.console.log("Opened shop menu"); + + for (int i = 0; i < 5; i++){ + if (!item_is_available(env.console, context, i)){ + env.console.log("Recipe in index " + std::to_string(i) + " is not available for purchase, skipping"); + continue; + } + RecipeType recipe_type = item_is_recipe(env.console, context, i); + // recipe_type = RecipeType::SINGLE; // Bypass for testing + // if (i != 1) { continue; } // Bypass for testing + switch (recipe_type){ + // For now, treat all recipes types with the same priority + // TODO: Recipes costs are assumed to be 400 here but recipes can be on sale and cheaper ones can be prioritized + case RecipeType::SINGLE: + case RecipeType::DOUBLE: + case RecipeType::TRIPLE: + case RecipeType::QUAD: + env.console.log("Recipe is available in index " + std::to_string(i)); + ret = wait_until( + env.console, context, + 30s, + {coin_watcher} + ); + if (ret != 0){ + env.console.log("Failed to read coin count in shop menu"); + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "buy_recipes() failed to read coin count in shop menu", + env.console + ); + } + env.console.log("Detected coin count: " + std::to_string(coin_watcher.coin_count())); + // For now, assume all recipes are the full 400 coins + if (coin_watcher.coin_count() - 400 < SPEND_LIMIT){ + env.console.log("Spend limit reached, not buying more recipes"); + return true; + } + buy_item(env.console, context, i); + stats.recipes_purchased++; + env.update_stats(); + break; + case RecipeType::NOT_RECIPE: + env.console.log("Index " + std::to_string(i) + " does not contain a recipe, skipping"); + continue; + default: + throw InternalProgramError( + nullptr, PA_CURRENT_FUNCTION, + "Unexpected recipe type detected in buy_recipes(): " + std::to_string((int)recipe_type) + ); + } + } + exit_pc(env.console, context); + return false; +} + void CloudIslandReset::leave_cloud_island(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ CloudIslandReset_Descriptor::Stats& stats = env.current_stats(); @@ -330,16 +416,32 @@ void CloudIslandReset::program(SingleSwitchProgramEnvironment& env, ProControlle DeferredStopButtonOption::ResetOnExit reset_on_exit(STOP_AFTER_CURRENT); + bool all_stamps_mew = false; + bool spend_limit_reached = false; + while (NUM_RESETS != 0 && stats.resets < NUM_RESETS){ delete_cloud_island_save(env, context); create_cloud_island_after_delete(env, context); stats.resets++; open_cloud_island_pc(env, context); - bool all_stamps_mew = add_todays_stamp(env, context); + if (COLLECT_MEW_STAMPS && !all_stamps_mew){ + all_stamps_mew = add_todays_stamp(env, context); + } + else { + exit_pc(env.console, context); // Exit PC since first access always force opens stamp card + } + + if (BUY_RECIPES && !spend_limit_reached){ + open_cloud_island_pc(env, context); + spend_limit_reached = buy_recipes(env, context); + } leave_cloud_island(env, context); - if (STOP_AFTER_CURRENT.should_stop() || all_stamps_mew){ + bool collect_mew_stamps_done = !COLLECT_MEW_STAMPS || (COLLECT_MEW_STAMPS && all_stamps_mew); + bool buy_recipes_done = !BUY_RECIPES || (BUY_RECIPES && spend_limit_reached); + + if (STOP_AFTER_CURRENT.should_stop() || (collect_mew_stamps_done && buy_recipes_done)){ break; } } diff --git a/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_CloudIslandReset.h b/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_CloudIslandReset.h index 091e5c79cf..17a35a1231 100644 --- a/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_CloudIslandReset.h +++ b/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_CloudIslandReset.h @@ -51,16 +51,24 @@ class CloudIslandReset : public SingleSwitchProgramInstance{ void open_cloud_island_pc(SingleSwitchProgramEnvironment& env, ProControllerContext& context); // Add today's stamp to the stamp collection. If full, replace a lower value one - // Exit the stamp menu when done + // Exit the PC when done // Return true if all stamps are Mew bool add_todays_stamp(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + // Open the shop menu from the main PC menu and buy all available recipes until the spend limit reached + // Exit the PC when done + // Return true if the spend limit is reached and the program should stop buying recipes + bool buy_recipes(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + // Starting from the overworld on a cloud Island, return home void leave_cloud_island(SingleSwitchProgramEnvironment& env, ProControllerContext& context); private: DeferredStopButtonOption STOP_AFTER_CURRENT; SimpleIntegerOption NUM_RESETS; + BooleanCheckBoxOption COLLECT_MEW_STAMPS; + BooleanCheckBoxOption BUY_RECIPES; + SimpleIntegerOption SPEND_LIMIT; GoHomeWhenDoneOption GO_HOME_WHEN_DONE; EventNotificationOption NOTIFICATION_STATUS_UPDATE; diff --git a/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_PCNavigation.cpp b/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_PCNavigation.cpp index 2ab7e2c62d..a7b0f7cdd2 100644 --- a/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_PCNavigation.cpp +++ b/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_PCNavigation.cpp @@ -50,6 +50,39 @@ ImageFloatBox get_pc_menu_option_box(PCMenuOption option, bool is_palette_town=f ); } +// Get the box location for the daily item type (paintable, recipe, none) in the shop menu based on the index (0-4) +ImageFloatBox get_shop_daily_item_type_box(int item_index){ + return ImageFloatBox( + SHOP_DAILY_ITEM_TYPE_BOX_1.x + item_index * SHOP_DAILY_ITEM_OFFSET.first, + SHOP_DAILY_ITEM_TYPE_BOX_1.y + item_index * SHOP_DAILY_ITEM_OFFSET.second, + SHOP_DAILY_ITEM_TYPE_BOX_1.width, + SHOP_DAILY_ITEM_TYPE_BOX_1.height + ); +} + +// Get the box location for the daily item coin icon in the shop menu based on the index (0-4) +ImageFloatBox get_shop_daily_item_coin_icon_box(int item_index){ + return ImageFloatBox( + SHOP_DAILY_ITEM_COIN_ICON_BOX_1.x + item_index * SHOP_DAILY_ITEM_OFFSET.first, + SHOP_DAILY_ITEM_COIN_ICON_BOX_1.y + item_index * SHOP_DAILY_ITEM_OFFSET.second, + SHOP_DAILY_ITEM_COIN_ICON_BOX_1.width, + SHOP_DAILY_ITEM_COIN_ICON_BOX_1.height + ); +} + +std::vector get_shop_daily_item_selection_boxes(){ + std::vector boxes; + for (size_t i = 0; i < 5; i++){ + boxes.emplace_back( + SHOP_DAILY_ITEM_SELECTOR_BOX_1.x + i * SHOP_DAILY_ITEM_OFFSET.first, + SHOP_DAILY_ITEM_SELECTOR_BOX_1.y + i * SHOP_DAILY_ITEM_OFFSET.second, + SHOP_DAILY_ITEM_SELECTOR_BOX_1.width, + SHOP_DAILY_ITEM_SELECTOR_BOX_1.height + ); + } + return boxes; +} + // Interact when the A button prompt appears // Watches for the A button prompt and wait for it to clear bool interact_button_a_prompt(ConsoleHandle& console, ProControllerContext& context){ @@ -603,6 +636,68 @@ void continue_until_prompt( } } +RecipeType item_is_recipe(ConsoleHandle& console, ProControllerContext& context, int item_index){ + RecipeIconDetector recipe_icon_detector( + COLOR_YELLOW, + &console.overlay(), + get_shop_daily_item_type_box(item_index) + ); + VideoSnapshot screen = console.video().snapshot(); + if (recipe_icon_detector.detect(screen)){ + console.log("Detected recipe icon for item " + std::to_string(item_index)); + return recipe_icon_detector.recipe_type(); + } + else{ + console.log("No recipe icon detected for item " + std::to_string(item_index)); + return RecipeType::NOT_RECIPE; + } +} + +bool item_is_available(ConsoleHandle& console, ProControllerContext& context, int item_index){ + CoinIconDetector coin_icon_detector( + COLOR_YELLOW, + &console.overlay(), + get_shop_daily_item_coin_icon_box(item_index) + ); + VideoSnapshot screen = console.video().snapshot(); + if (coin_icon_detector.detect(screen)){ + console.log("Detected coin icon for item " + std::to_string(item_index) + ", item is available"); + return true; + } + else{ + console.log("No coin icon detected for item " + std::to_string(item_index) + ", item is not available"); + return false; + } +} + +void buy_item(ConsoleHandle& console, ProControllerContext& context, int item_index){ + std::vector selection_boxes = get_shop_daily_item_selection_boxes(); + SelectionArrowWatcher item_selector( + COLOR_YELLOW, &console.overlay(), + SelectionArrowType::DOWN, + selection_boxes[item_index] + ); + generic_select_and_open(console, context, selection_boxes, item_index, SelectionArrowType::DOWN); + + int ret = run_until( + console, context, + [&](ProControllerContext& context){ + for (int i = 0; i < 20; i++){ + pbf_press_button(context, BUTTON_A, 160ms, 1500ms); + } + }, + {item_selector} + ); + if (ret != 0){ + console.log("Failed to detect shop after purchase"); + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "buy_item() failed to detect shop after purchase", + console + ); + } +} + Stamp get_stamp( ConsoleHandle& console, ProControllerContext& context, diff --git a/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_PCNavigation.h b/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_PCNavigation.h index 4b71803737..3f51f7d850 100644 --- a/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_PCNavigation.h +++ b/SerialPrograms/Source/PokemonPokopia/Programs/PokemonPokopia_PCNavigation.h @@ -39,6 +39,12 @@ const ImageFloatBox PC_PALETTE_GET_ITEMS_BOX{0.376000, 0.648500, 0.034000, 0.052 const ImageFloatBox PC_PALETTE_STAMP_CARD_BOX{0.590000, 0.648500, 0.034000, 0.052500}; const ImageFloatBox PC_PALETTE_APPRAISE_BOX{0.802500, 0.648500, 0.034000, 0.052500}; +// Shop Menu items. Apply the offset to get boxes for items 2-5 +const ImageFloatBox SHOP_DAILY_ITEM_TYPE_BOX_1{0.125000, 0.300000, 0.034000, 0.064000}; +const ImageFloatBox SHOP_DAILY_ITEM_SELECTOR_BOX_1{0.165000, 0.170000, 0.034000, 0.052500}; // Issue when detecting orange arrow on orange label in shop menu +const ImageFloatBox SHOP_DAILY_ITEM_COIN_ICON_BOX_1(0.133000, 0.377000, 0.025000, 0.045000); +const std::pair SHOP_DAILY_ITEM_OFFSET(0.158000, 0); + // Link Play menu options const ImageFloatBox LINK_PLAY_INVITE_BOX{0.048500, 0.249000, 0.027500, 0.057500}; const ImageFloatBox LINK_PLAY_VISIT_BOX{0.048500, 0.443000, 0.027500, 0.057500}; @@ -130,6 +136,29 @@ void continue_until_prompt( const ImageFloatBox prompt_box ); +// Check if the daily shop item at the specified index is a recipe +// Return single, double, triple, or not a recipe +RecipeType item_is_recipe( + ConsoleHandle& console, + ProControllerContext& context, + int item_index +); + +// Check if the daily shop item at the specified index is available for purchase +bool item_is_available( + ConsoleHandle& console, + ProControllerContext& context, + int item_index +); + +// Buy the daily shop item at the specified index +// TODO: Handle the case where you cannot afford the item. This is usually gated elsewhere to ensure this case is never reached +void buy_item( + ConsoleHandle& console, + ProControllerContext& context, + int item_index +); + // Get the stamp in the specified area Stamp get_stamp( ConsoleHandle& console,