From 150442447e78e9538670ddf97d11b8dd2af05b34 Mon Sep 17 00:00:00 2001 From: thewhitewolfking <337211553+thewhitewolfking@users.noreply.github.com> Date: Wed, 7 Oct 2026 20:31:35 -0700 Subject: [PATCH] FRLG Elite Four Farmer: move cursor detection, opponent name reading, early reset on faint --- ...kemonFRLG_BattleSelectionArrowDetector.cpp | 77 +++++++ ...PokemonFRLG_BattleSelectionArrowDetector.h | 28 +++ .../PokemonFRLG/PokemonFRLG_Tests.cpp | 2 + .../Farming/PokemonFRLG_EliteFourFarmer.cpp | 202 ++++++++++++++---- .../Farming/PokemonFRLG_EliteFourFarmer.h | 2 + .../PokemonFRLG_BattleMenuNavigation.cpp | 11 + .../PokemonFRLG_BattleMenuNavigation.h | 7 + 7 files changed, 289 insertions(+), 40 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.cpp index a6404f7851..a28c23362a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.cpp @@ -5,6 +5,8 @@ */ #include "Common/Cpp/Exceptions.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" #include "CommonTools/ImageMatch/WaterfillTemplateMatcher.h" #include "CommonTools/Images/WaterfillUtilities.h" #include "VideoGames/PokemonFRLG/PokemonFRLG_Settings.h" @@ -65,6 +67,21 @@ ImageFloatBox BattleSelectionArrowDetector::box_for_option(SafariBattleMenuOptio } throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Invalid FRLG Safari Battle Menu Option"); } +ImageFloatBox BattleSelectionArrowDetector::box_for_option(BattleMoveOption option){ + switch (option){ + case BattleMoveOption::MOVE_1: + return ImageFloatBox(0.028, 0.765, 0.040, 0.080); + case BattleMoveOption::MOVE_2: + return ImageFloatBox(0.328, 0.765, 0.040, 0.080); + case BattleMoveOption::MOVE_3: + return ImageFloatBox(0.028, 0.865, 0.040, 0.080); + case BattleMoveOption::MOVE_4: + return ImageFloatBox(0.328, 0.865, 0.040, 0.080); + default: + break; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Invalid FRLG Battle Move Option"); +} ImageFloatBox BattleSelectionArrowDetector::box_for_option(BattleConfirmationOption option){ switch (option){ case BattleConfirmationOption::YES: @@ -104,6 +121,15 @@ BattleSelectionArrowDetector::BattleSelectionArrowDetector( , m_overlay(overlay) , m_arrow_box(box_for_option(option)) {} +BattleSelectionArrowDetector::BattleSelectionArrowDetector( + Color color, + VideoOverlay* overlay, + BattleMoveOption option +) + : m_color(color) + , m_overlay(overlay) + , m_arrow_box(box_for_option(option)) +{} BattleSelectionArrowDetector::BattleSelectionArrowDetector( Color color, VideoOverlay* overlay @@ -166,6 +192,57 @@ bool BattleSelectionArrowDetector::detect(const ImageViewRGB32& screen){ } + +// Each image shows the FIGHT move list with the arrow on one move. The arrow +// must be found in that move's box and in none of the other three. Searching +// the whole move list (as the cursor navigation does) must also find it in +// the right place, not on the move names. +class Test_BattleMoveSelectionArrow : public UnitTest{ +public: + Test_BattleMoveSelectionArrow( + const std::string& image, + BattleMoveOption expected + ) + : UnitTest("PokemonFRLG::BattleMoveSelectionArrow - " + image) + , m_image(UNIT_TEST_RESOURCE_PATH() + image) + , m_expected(expected) + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ + ImageRGB32 image(m_image); + for (int i = 0; i < BATTLE_MOVE_OPTION_COUNT; i++){ + BattleMoveOption option = static_cast(i); + BattleSelectionArrowDetector detector(COLOR_RED, nullptr, option); + if (detector.detect(image) != (option == m_expected)){ + return false; + } + } + + BattleSelectionArrowDetector whole_list(COLOR_RED, nullptr, BATTLE_MOVE_ARROW_BOX); + if (!whole_list.detect(image)){ + return false; + } + const ImageFloatBox& found = whole_list.last_detected(); + const ImageFloatBox expected = BattleSelectionArrowDetector::box_for_option(m_expected); + double cx = found.x + found.width / 2; + double cy = found.y + found.height / 2; + return expected.x <= cx && cx <= expected.x + expected.width + && expected.y <= cy && cy <= expected.y + expected.height; + }; + +private: + std::string m_image; + BattleMoveOption m_expected; +}; + +void add_tests_BattleMoveSelectionArrow(UnitTestDatabase& database){ + database.add("PokemonFRLG/BattleMoveSelectionArrow/MoveArrow-TopLeft.png", BattleMoveOption::MOVE_1); + database.add("PokemonFRLG/BattleMoveSelectionArrow/MoveArrow-TopRight.png", BattleMoveOption::MOVE_2); + database.add("PokemonFRLG/BattleMoveSelectionArrow/MoveArrow-BottomLeft.png", BattleMoveOption::MOVE_3); + database.add("PokemonFRLG/BattleMoveSelectionArrow/MoveArrow-BottomRight.png", BattleMoveOption::MOVE_4); +} + + } } } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.h index a2987c9820..a36d4e1f09 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.h @@ -7,6 +7,7 @@ #ifndef PokemonAutomation_PokemonFRLG_BattleSelectionArrowDetector_H #define PokemonAutomation_PokemonFRLG_BattleSelectionArrowDetector_H +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" #include "CommonTools/VisualDetector.h" @@ -32,12 +33,22 @@ enum class SafariBattleMenuOption{ RUN, }; +// The four moves in the FIGHT move list, in the same row-major order. +const int BATTLE_MOVE_OPTION_COUNT = 4; +enum class BattleMoveOption{ + MOVE_1, // top left + MOVE_2, // top right + MOVE_3, // bottom left + MOVE_4, // bottom right +}; + enum class BattleConfirmationOption{ YES, NO, }; const ImageFloatBox BATTLE_MENU_ARROW_BOX(0.525, 0.741, 0.273, 0.212); +const ImageFloatBox BATTLE_MOVE_ARROW_BOX(0.025, 0.741, 0.373, 0.212); class BattleSelectionArrowDetector : public StaticScreenDetector{ public: @@ -56,6 +67,11 @@ class BattleSelectionArrowDetector : public StaticScreenDetector{ VideoOverlay* overlay, SafariBattleMenuOption option ); + BattleSelectionArrowDetector( + Color color, + VideoOverlay* overlay, + BattleMoveOption option + ); BattleSelectionArrowDetector( Color color, VideoOverlay* overlay @@ -68,6 +84,7 @@ class BattleSelectionArrowDetector : public StaticScreenDetector{ static ImageFloatBox box_for_option(BattleMenuOption option); static ImageFloatBox box_for_option(SafariBattleMenuOption option); + static ImageFloatBox box_for_option(BattleMoveOption option); static ImageFloatBox box_for_option(BattleConfirmationOption option); const ImageFloatBox& last_detected() const { return m_last_detected; } @@ -113,6 +130,14 @@ class BattleSelectionArrowWatcher : public DetectorToFinder #include #include +#include #include #include +#include "Common/Cpp/Exceptions.h" #include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" #include "CommonFramework/Notifications/ProgramNotifications.h" #include "CommonFramework/ProgramStats/StatsTracking.h" #include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" #include "CommonTools/Async/InferenceRoutines.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "Pokemon/Pokemon_Strings.h" #include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_BattleDialogs.h" #include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_DialogDetector.h" #include "VideoGames/PokemonFRLG/Inference/Map/PokemonFRLG_PokemonLeagueDetectors.h" +#include "VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_PartyMenuDetector.h" #include "VideoGames/PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.h" +#include "VideoGames/PokemonFRLG/Inference/PokemonFRLG_WildEncounterReader.h" #include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.h" #include "VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h" #include "PokemonFRLG_EliteFourFarmer.h" @@ -59,17 +66,20 @@ struct EliteFourFarmer_Descriptor::Stats : public StatsTracker{ Stats() : wins(m_stats["Wins"]) , battles(m_stats["Battles Won"]) + , losses(m_stats["Losses"]) , resets(m_stats["Resets"]) , errors(m_stats["Errors"]) { m_display_order.emplace_back("Wins"); m_display_order.emplace_back("Battles Won"); + m_display_order.emplace_back("Losses", HIDDEN_IF_ZERO); m_display_order.emplace_back("Resets", HIDDEN_IF_ZERO); m_display_order.emplace_back("Errors", HIDDEN_IF_ZERO); } std::atomic& wins; std::atomic& battles; + std::atomic& losses; std::atomic& resets; std::atomic& errors; }; @@ -81,6 +91,20 @@ std::unique_ptr EliteFourFarmer_Descriptor::make_stats() const{ EliteFourFarmer::EliteFourFarmer() : STOP_AFTER_CURRENT("Win") + , LANGUAGE( + "Game Language:
" + "Used to read the opposing Pokemon's name.", + { + Language::English, + Language::Japanese, + Language::Spanish, + Language::French, + Language::German, + Language::Italian, + }, + LockMode::LOCK_WHILE_RUNNING, + true + ) , ATTACKER( "Attacker:
" "The Pokemon in slot 1 that battles. It must be level 100 with 252 EVs in Sp. Atk and Speed, " @@ -119,6 +143,7 @@ EliteFourFarmer::EliteFourFarmer() }) { PA_ADD_OPTION(STOP_AFTER_CURRENT); + PA_ADD_OPTION(LANGUAGE); PA_ADD_OPTION(ATTACKER); PA_ADD_OPTION(STARTER); PA_ADD_OPTION(NUM_WINS); @@ -660,23 +685,29 @@ void wait_for_hall_of_fame_save(ConsoleHandle& console, ProControllerContext& co } // From the battle menu, choose FIGHT and then the move in the given slot. -// Pressing left and up first always puts the move cursor on the top-left -// slot, whatever slot was used last turn, so no cursor tracking is needed. +// The arrow is read from the screen, so only the presses needed to reach +// the move are made (none if the arrow is already on it). void use_move_in_slot(ConsoleHandle& console, ProControllerContext& context, int slot){ if (!move_cursor_to_option(console, context, BattleMenuOption::FIGHT)){ console.log("Could not confirm the cursor is on FIGHT. Pressing A anyway.", COLOR_RED); } - pbf_press_button(context, BUTTON_A, 200ms, 600ms); // open the move list + pbf_press_button(context, BUTTON_A, 200ms, 300ms); // open the move list + context.wait_for_all_requests(); - pbf_press_dpad(context, DPAD_LEFT, 160ms, 240ms); - pbf_press_dpad(context, DPAD_UP, 160ms, 240ms); - if (slot & 1){ - pbf_press_dpad(context, DPAD_RIGHT, 160ms, 240ms); - } - if (slot & 2){ - pbf_press_dpad(context, DPAD_DOWN, 160ms, 240ms); + if (!move_cursor_to_option(console, context, static_cast(slot))){ + // Could not see the arrow. Fall back to pressing left and up, which + // always lands on the top-left move, then moving to the slot. + console.log("Could not find the move cursor. Moving to the move without it.", COLOR_RED); + pbf_press_dpad(context, DPAD_LEFT, 160ms, 240ms); + pbf_press_dpad(context, DPAD_UP, 160ms, 240ms); + if (slot & 1){ + pbf_press_dpad(context, DPAD_RIGHT, 160ms, 240ms); + } + if (slot & 2){ + pbf_press_dpad(context, DPAD_DOWN, 160ms, 240ms); + } + context.wait_for_all_requests(); } - context.wait_for_all_requests(); // The PP text turns red when the highlighted move is out of PP. BattleOutOfPpWatcher out_of_pp(COLOR_RED); @@ -693,13 +724,42 @@ void use_move_in_slot(ConsoleHandle& console, ProControllerContext& context, int context.wait_for_all_requests(); } +enum class TrainerBattleResult{ + WON, + ATTACKER_FAINTED, +}; + +// Read the opposing Pokemon's species from its name. It is matched against +// every species in the attacker's move table, not just this trainer's, so an +// unexpected opponent is reported by name instead of being misread. +// Returns an empty string if it can't be read. +std::string read_opponent( + ConsoleHandle& console, Language language, + const std::set& candidates +){ + WildEncounterReader reader(COLOR_RED); + VideoOverlaySet overlays(console.overlay()); + reader.make_overlays(overlays); + VideoSnapshot screen = console.video().snapshot(); + return reader.read_encounter(console.logger(), language, screen, candidates).name; +} + // Fight one trainer from the battle menu until the battle ends. -void run_battle( - ConsoleHandle& console, ProControllerContext& context, +TrainerBattleResult run_battle( + ConsoleHandle& console, ProControllerContext& context, Language language, const AttackerPlan& plan, const std::vector& order, Trainer trainer ){ - size_t opponent = 0; // index into the send-out order + // Times the opponent's Pokemon left the field. This counts switch-outs + // as well as faints, since both look the same on screen. + size_t left_field = 0; size_t turns = 0; + std::string last_species; // the opponent we attacked last turn + bool same_opponent = false; // true if it did not leave the field + + std::set candidates; + for (const auto& item : plan.move_for){ + candidates.insert(item.first); + } while (true){ if (turns > 3 * order.size()){ @@ -710,8 +770,20 @@ void run_battle( ); } - // We are at the battle menu. - const std::string& species = order[std::min(opponent, order.size() - 1)]; + // We are at the battle menu. Read who we are facing. If that fails, + // assume the last opponent if it never left the field, otherwise + // fall back to the expected send-out order. + std::string species = read_opponent(console, language, candidates); + if (species.empty()){ + if (same_opponent){ + species = last_species; + console.log("Could not read the opponent's name. Assuming " + species + " is still out.", COLOR_RED); + }else{ + species = order[std::min(left_field, order.size() - 1)]; + console.log("Could not read the opponent's name. Assuming " + species + " from the send-out order.", COLOR_RED); + } + } + last_species = species; auto iter = plan.move_for.find(species); if (iter == plan.move_for.end()){ OperationFailedExceptionWithScreenshot::fire( @@ -723,25 +795,22 @@ void run_battle( Move move = iter->second; int slot = slot_of(plan, move); console.log( - std::string(trainer_name(trainer)) + " #" + std::to_string(opponent + 1) + + std::string(trainer_name(trainer)) + ", turn " + std::to_string(turns + 1) + " (" + species + "): using " + move_name(move) + " (slot " + std::to_string(slot + 1) + ")" ); use_move_in_slot(console, context, slot); turns++; // Mash B through the battle text until the battle menu comes back - // (next opponent) or the battle ends (fade to black). - // - // Every move is a guaranteed OHKO at the required stats, so each - // return to the battle menu means the next opponent is out. The - // faint watcher only feeds the log: if it never fires, either the - // detector missed it or the opponent really survived. + // (next opponent, or the same one if it survived) or the battle ends + // (fade to black). The next turn reads the name again, so a survivor + // is attacked again. bool faint_seen = false; auto report_missed_faint = [&]{ if (!faint_seen){ console.log( - "Did not see the opponent faint. If the battle runs long, the opponent " - "survived: check the attacker's Sp. Atk, nature and moves.", + "Did not see the opponent faint. It may have survived: " + "check the attacker's Sp. Atk, nature and moves.", COLOR_RED ); } @@ -751,9 +820,10 @@ void run_battle( while (waiting){ BattleMenuWatcher battle_menu(COLOR_RED); BlackScreenWatcher battle_ended(COLOR_RED); + BattleFaintWatcher attacker_fainted(COLOR_RED); BattleOpponentFaintWatcher opponent_fainted(COLOR_RED); - std::vector callbacks{battle_menu, battle_ended}; + std::vector callbacks{battle_menu, battle_ended, attacker_fainted}; if (!faint_seen){ callbacks.emplace_back(opponent_fainted); } @@ -767,26 +837,49 @@ void run_battle( ); switch (ret){ - case 0: // Battle menu: the next opponent is out. + case 0: // Battle menu: our next turn. report_missed_faint(); - opponent++; + same_opponent = !faint_seen; waiting = false; break; - case 1: // Battle over. - report_missed_faint(); - opponent++; - if (opponent != order.size()){ + case 1:{ // Fade to black. + // A won battle ends with the last opponent fainting, then a + // fade to the overworld. If no faint was seen this turn, the + // fade may instead be the game opening the party screen + // because our attacker fainted (this can happen before the + // attacker faint watcher triggers). + if (!faint_seen){ + PartyMenuWatcher party_menu(COLOR_RED); + if (wait_until(console, context, 5000ms, { party_menu }) == 0){ + console.log( + std::string("The attacker fainted against ") + trainer_name(trainer) + + " (the party screen opened). Check its Sp. Atk, Speed, nature and moves against the requirements.", + COLOR_RED + ); + return TrainerBattleResult::ATTACKER_FAINTED; + } + } + if (turns != order.size()){ console.log( - "Battle ended after " + std::to_string(opponent) + " attacks (expected " + - std::to_string(order.size()) + ").", + "Battle took " + std::to_string(turns) + " attacks (" + + std::to_string(order.size()) + " if every attack is a one-hit KO).", COLOR_RED ); } console.log(std::string("Defeated ") + trainer_name(trainer) + "."); - return; - case 2: // Opponent fainted. Keep waiting for the menu or the end. - console.log("Opponent fainted."); + return TrainerBattleResult::WON; + } + case 2: // Our attacker fainted. The run is lost; no need to wait it out. + console.log( + std::string("The attacker fainted against ") + trainer_name(trainer) + + ". Check its Sp. Atk, nature and moves against the requirements.", + COLOR_RED + ); + return TrainerBattleResult::ATTACKER_FAINTED; + case 3: // Opponent fainted (or switched out). Keep waiting for the menu or the end. + console.log("Opponent left the field (fainted or switched out)."); faint_seen = true; + left_field++; break; default: OperationFailedExceptionWithScreenshot::fire( @@ -816,6 +909,7 @@ void EliteFourFarmer::program(SingleSwitchProgramEnvironment& env, ProController }; uint32_t consecutive_errors = 0; + uint32_t consecutive_losses = 0; while (true){ send_program_status_notification(env, NOTIFICATION_STATUS_UPDATE); if (NUM_WINS != 0 && stats.wins >= NUM_WINS){ @@ -825,13 +919,18 @@ void EliteFourFarmer::program(SingleSwitchProgramEnvironment& env, ProController try{ travel_to_lorelei(env.console, context); + bool lost = false; for (size_t i = 0; i < TRAINERS.size(); i++){ Trainer trainer = TRAINERS[i]; if (i > 0){ walk_to_next_room(env.console, context, trainer); } start_battle(env.console, context, trainer); - run_battle(env.console, context, plan, expected_order(plan, starter, trainer), trainer); + TrainerBattleResult result = run_battle(env.console, context, LANGUAGE, plan, expected_order(plan, starter, trainer), trainer); + if (result == TrainerBattleResult::ATTACKER_FAINTED){ + lost = true; + break; + } stats.battles++; env.update_stats(); @@ -840,6 +939,28 @@ void EliteFourFarmer::program(SingleSwitchProgramEnvironment& env, ProController } } + if (lost){ + // A lost run is not an error: reset right away and try again. + stats.losses++; + env.update_stats(); + consecutive_losses++; + if (consecutive_losses >= 5){ + throw UserSetupError( + env.console, + "The attacker lost 5 runs in a row. Check its moves and move order, " + "its Sp. Atk, Speed and nature, and the Attacker and Your Starter options." + ); + } + env.log("Run lost. Soft resetting and trying again.", COLOR_RED); + soft_reset(env.console, context); + stats.resets++; + env.update_stats(); + if (STOP_AFTER_CURRENT.should_stop()){ + break; + } + continue; + } + // The game is saved once the Hall of Fame save message is gone, // so reset right away. wait_for_hall_of_fame_save(env.console, context); @@ -847,6 +968,7 @@ void EliteFourFarmer::program(SingleSwitchProgramEnvironment& env, ProController stats.wins++; env.update_stats(); consecutive_errors = 0; + consecutive_losses = 0; env.log("Soft resetting to Pallet Town..."); soft_reset(env.console, context); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/Farming/PokemonFRLG_EliteFourFarmer.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/Farming/PokemonFRLG_EliteFourFarmer.h index c6dffeb6a9..d28abedc73 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/Farming/PokemonFRLG_EliteFourFarmer.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/Farming/PokemonFRLG_EliteFourFarmer.h @@ -14,6 +14,7 @@ #include "Common/Cpp/Options/EnumDropdownOption.h" #include "Common/Cpp/Options/SimpleIntegerOption.h" #include "CommonFramework/Notifications/EventNotificationsTable.h" +#include "CommonTools/Options/LanguageOCROption.h" #include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" #include "NintendoSwitch/Options/NintendoSwitch_GoHomeWhenDoneOption.h" @@ -57,6 +58,7 @@ class EliteFourFarmer : public SingleSwitchProgramInstance{ private: DeferredStopButtonOption STOP_AFTER_CURRENT; + OCR::LanguageOCROption LANGUAGE; EnumDropdownOption ATTACKER; EnumDropdownOption STARTER; SimpleIntegerOption NUM_WINS; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.cpp index cf9c07cd2c..bb7ac34c24 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.cpp @@ -96,6 +96,17 @@ bool move_cursor_to_option(ConsoleHandle& console, ProControllerContext& context ); } +bool move_cursor_to_option(ConsoleHandle& console, ProControllerContext& context, BattleMoveOption destination){ + return move_cursor_2d_impl( + console, context, + BATTLE_MOVE_ARROW_BOX, + BATTLE_MOVE_OPTION_COUNT, + BATTLE_MENU_COLS, + [](int i){ return BattleSelectionArrowDetector::box_for_option(static_cast(i)); }, + static_cast(destination) + ); +} + bool move_cursor_to_option(ConsoleHandle& console, ProControllerContext& context, SafariBattleMenuOption destination){ return move_cursor_2d_impl( console, context, diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.h index 57ec502438..cf1b7fa8e4 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.h @@ -29,6 +29,13 @@ bool move_cursor_to_option( BattleMenuOption destination ); +// Starting from the FIGHT move list, move the selection arrow to the specified move. +// Returns true if successful, false otherwise (e.g. if selection arrow is not detected). +bool move_cursor_to_option( + ConsoleHandle& console, ProControllerContext& context, + BattleMoveOption destination +); + // Starting from the Safari Zone battle command menu, move the selection arrow to the specified option. // Returns true if successful, false otherwise (e.g. if selection arrow is not detected). bool move_cursor_to_option(