From 416642dbbc6fe0341c50c80d5633e15612ba3a75 Mon Sep 17 00:00:00 2001 From: theAstrogoth Date: Wed, 8 Apr 2026 08:27:35 -0500 Subject: [PATCH] navigation stuff --- .../PokemonFRLG/PokemonFRLG_Navigation.cpp | 499 ++++++++++++++---- .../PokemonFRLG/PokemonFRLG_Navigation.h | 53 +- 2 files changed, 449 insertions(+), 103 deletions(-) diff --git a/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Navigation.cpp b/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Navigation.cpp index 91b8a670e..ef9d36139 100644 --- a/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Navigation.cpp +++ b/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Navigation.cpp @@ -25,6 +25,8 @@ #include "PokemonFRLG/Inference/Menus/PokemonFRLG_LoadMenuDetector.h" #include "PokemonFRLG/Inference/Menus/PokemonFRLG_SummaryDetector.h" #include "PokemonFRLG/Inference/Menus/PokemonFRLG_PartyMenuDetector.h" +#include "PokemonFRLG/Inference/Map/PokemonFRLG_MapDetector.h" +#include "PokemonFRLG/Inference/PokemonFRLG_BattlePokemonDetector.h" #include "PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.h" #include "PokemonFRLG_Navigation.h" @@ -330,6 +332,85 @@ bool handle_encounter(ConsoleHandle& console, ProControllerContext& context, boo return false; } +int spam_first_move(ConsoleHandle& console, ProControllerContext& context){ + uint16_t errors = 0; + uint16_t times_moved = 0; + while (true){ + if (errors > 5) { + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "spam_first_move(): Failed to use move 5 times.", + console + ); + } else if (times_moved > 50){ + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "spam_first_move(): More than 50 move uses detected.", + console + ); + } + + BattleMenuWatcher battle_menu(COLOR_RED); + BattleFaintWatcher pokemon_fainted(COLOR_RED); + BattleOpponentFaintWatcher opponent_fainted(COLOR_RED); + BlackScreenWatcher battle_ended(COLOR_RED); + BattleOutOfPpWatcher out_of_pp(COLOR_RED); + AdvanceBattleDialogWatcher out_of_pp_dialog(COLOR_RED); + + int ret = run_until( + console, context, + [](ProControllerContext& context){ + pbf_wait(context, 20000ms); + context.wait_for_all_requests(); + }, + { battle_menu, pokemon_fainted, opponent_fainted, battle_ended } + ); + + int ret2; + switch (ret){ + case 0: + console.log("Using first move."); + context.wait_for_all_requests(); + ret2 = run_until( + console, context, + [](ProControllerContext& context){ + pbf_press_button(context, BUTTON_A, 200ms, 300ms); // leave enough time for PP color to be detected + pbf_press_button(context, BUTTON_A, 200ms, 300ms); + pbf_mash_button(context, BUTTON_A, 500ms); + context.wait_for_all_requests(); + }, + { out_of_pp, out_of_pp_dialog } + ); + if (ret2 < 0){ + times_moved++; + } else { + console.log("Out of PP, fleeing battle."); + pbf_mash_button(context, BUTTON_B, 2000ms); + flee_battle(console, context); + context.wait_for_all_requests(); + return 3; + } + continue; + case 1: + console.log("Player Pokemon fainted."); + return 1; + case 2: + console.log("Opponent fainted."); + return 0; + case 3: + console.log("Battle ended"); // the opponent probably fled + pbf_wait(context, 2000ms); + context.wait_for_all_requests(); + return 2; + default: + console.log("Failed to detect move use."); + pbf_mash_button(context, BUTTON_B, 2000ms); // get back to the top-level battle menu + context.wait_for_all_requests(); + continue; + } + } +} + void flee_battle(ConsoleHandle& console, ProControllerContext& context){ console.log("Navigate to Run."); pbf_press_dpad(context, DPAD_RIGHT, 160ms, 160ms); @@ -373,65 +454,79 @@ void flee_battle(ConsoleHandle& console, ProControllerContext& context){ } } -bool exit_wild_battle(ConsoleHandle& console, ProControllerContext& context, bool stop_on_move_learn){ - - BlackScreenWatcher battle_exited(COLOR_RED); - - context.wait_for_all_requests(); - int ret = run_until( - console, context, - [](ProControllerContext& context) { - pbf_mash_button(context, BUTTON_B, 20000ms); - }, - { battle_exited } - ); - - if (ret == 0) { - pbf_wait(context, 500ms); - context.wait_for_all_requests(); - console.log("Battle exited."); - return false; - } - - console.log("Loop detected."); - - // there are two dialog selection boxes in a row - // we need to decline the first one and accept the second one - // the first one will occur after an Advance Battle Dialog +bool exit_wild_battle(ConsoleHandle& console, ProControllerContext& context, bool stop_on_move_learn, bool prevent_evolution){ + // For move learning, there are two dialog selection boxes in a row + // we need to decline the first one and accept the second one, so mashing B won't work + // The first one will occur after an Advance Battle Dialog uint16_t errors = 0; uint16_t loops = 0; + bool first_attempt = true; bool rejected_first_box = false; + bool move_learned = false; while (true){ if (errors > 5 || loops > 5){ OperationFailedException::fire( ErrorReport::SEND_ERROR_REPORT, - "Failed to exit battle.", + "exit_wild_battle(): Failed to exit battle.", console ); } + BlackScreenWatcher battle_exited(COLOR_RED); AdvanceBattleDialogWatcher advance_dialog(COLOR_RED); BattleLearnDialogWatcher move_learn_select(COLOR_RED); context.wait_for_all_requests(); WallClock deadline = current_time() + 30s; - ret = run_until( - console, context, - [deadline, rejected_first_box](ProControllerContext& context) { - pbf_wait(context, 1000ms); // give the watchers a chance to detect something - while (current_time() < deadline){ - pbf_press_button(context, rejected_first_box ? BUTTON_A : BUTTON_B, 200ms, 1800ms); - } - }, - { battle_exited, advance_dialog, move_learn_select } - ); + int ret; + if (first_attempt){ + ret = run_until( + console, context, + [](ProControllerContext& context) { + pbf_mash_button(context, BUTTON_B, 20000ms); + }, + { battle_exited } + ); + }else{ + ret = run_until( + console, context, + [deadline, rejected_first_box](ProControllerContext& context) { + pbf_wait(context, 1000ms); // give the watchers a chance to detect something + while (current_time() < deadline){ + pbf_press_button(context, rejected_first_box ? BUTTON_A : BUTTON_B, 200ms, 1800ms); + } + }, + { battle_exited, advance_dialog, move_learn_select } + ); + } + + BattleDialogWatcher evolution_started(COLOR_RED); + StartMenuWatcher start_menu_open(COLOR_RED); + int ret2; switch (ret){ case 0: - pbf_wait(context, 500ms); - context.wait_for_all_requests(); + // check for the evolution screen + context.wait_for_all_requests(); + ret2 = run_until( + console, context, + [](ProControllerContext& context) { + pbf_wait(context, 2000ms); + }, + { evolution_started } + ); + + if (ret2 == 0){ + console.log("Evolution detected."); + if (!prevent_evolution){ + // make sure B isn't pressed too soon, which would cancel the evolution + pbf_wait(context, 20000ms); + } + rejected_first_box = false; + continue; // press B as in other cases, and handle any move learning loops that might come up + } console.log("Battle exited."); - return rejected_first_box; + return move_learned; case 1: console.log("Battle Advance arrow detected."); pbf_press_button(context, BUTTON_B, 200ms, 800ms); @@ -444,18 +539,40 @@ bool exit_wild_battle(ConsoleHandle& console, ProControllerContext& context, boo }else if (rejected_first_box) { loops++; console.log("Declined to learn new move."); - pbf_press_button(context, BUTTON_A, 200ms, 200ms); - pbf_mash_button(context, BUTTON_B, 2000ms); + pbf_press_button(context, BUTTON_A, 200ms, 0ms); }else{ pbf_press_button(context, BUTTON_B, 200ms, 0ms); rejected_first_box = true; + move_learned = true; } continue; default: + if (first_attempt){ + console.log("Loop detected."); + first_attempt = false; + continue; + } console.log("Failed to detect expected battle dialogs."); errors++; // attempt to exit any screen that might be open (party, bag, etc) pbf_mash_button(context, BUTTON_B, 500ms); + // overworld detection: look for the start menu + context.wait_for_all_requests(); + ret2 = run_until( + console, context, + [](ProControllerContext& context) { + pbf_press_button(context, BUTTON_PLUS, 200ms, 800ms); + pbf_press_button(context, BUTTON_PLUS, 200ms, 800ms); + pbf_press_button(context, BUTTON_PLUS, 200ms, 800ms); + }, + { start_menu_open } + ); + if (ret2 == 0){ + pbf_mash_button(context, BUTTON_B, 500ms); + context.wait_for_all_requests(); + console.log("Battle exited."); + return move_learned; + } context.wait_for_all_requests(); rejected_first_box = false; continue; @@ -463,14 +580,68 @@ bool exit_wild_battle(ConsoleHandle& console, ProControllerContext& context, boo } } -void use_teleport(ConsoleHandle& console, ProControllerContext& context){ +void open_party_menu_from_overworld(ConsoleHandle& console, ProControllerContext& context){ + uint16_t errors = 0; + bool start_menu_is_open = false; + while (true){ + if (errors > 5){ + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "open_party_menu_from_overworld(): Failed to open party menu 5 times in a row.", + console + ); + } + + context.wait_for_all_requests(); + if (!start_menu_is_open){ + open_start_menu(console, context); // This is unavoidable since we cannot detect the overworld. + start_menu_is_open = true; + } + + StartMenuWatcher start_menu(COLOR_RED); + PartyMenuWatcher party_menu(COLOR_RED); + + int ret = wait_until( + console, context, 10000ms, + { start_menu, party_menu } + ); + + switch (ret){ + case 0: + ret = move_cursor_to_position(console, context, SelectionArrowPositionStartMenu::POKEMON); + if (ret < 0){ + console.log("Failed to navigate to POKEMON on the start menu."); + errors++; + context.wait_for_all_requests(); + pbf_mash_button(context, BUTTON_B, 2000ms); + start_menu_is_open = false; + } else { + console.log("Navigated to POKEMON on the start menu"); + context.wait_for_all_requests(); + pbf_press_button(context, BUTTON_A, 200ms, 1300ms); + } + continue; + case 1: + console.log("Party menu opened."); + return; + default: + console.log("Failed to open party menu."); + errors++; + pbf_mash_button(context, BUTTON_B, 2000ms); + start_menu_is_open = false; + continue; + } + } +} + +void use_teleport_from_overworld(ConsoleHandle& console, ProControllerContext& context){ uint16_t errors = 0; while (true){ if (errors > 5){ OperationFailedException::fire( ErrorReport::SEND_ERROR_REPORT, - "Failed to use Teleport 5 times in a row.", + "use_teleport_from_overworld(): Failed to use Teleport 5 times in a row.", console ); } @@ -525,6 +696,168 @@ void use_teleport(ConsoleHandle& console, ProControllerContext& context){ } } +void open_fly_map_from_overworld(ConsoleHandle& console, ProControllerContext& context){ + uint16_t errors = 0; + while (true){ + if (errors > 5){ + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "open_fly_map_from_overworld(): Failed to open Fly map 5 times in a row.", + console + ); + } + + open_party_menu_from_overworld(console, context); + // navigate to last party slot + pbf_move_left_joystick(context, {0, +1}, 200ms, 300ms); + pbf_move_left_joystick(context, {0, +1}, 200ms, 300ms); + + PartySelectionWatcher fly_user_selected(COLOR_RED); + + context.wait_for_all_requests(); + int ret = run_until( + console, context, + [](ProControllerContext& context) { + pbf_press_button(context, BUTTON_A, 200ms, 1800ms); + }, + { fly_user_selected } + ); + + if (ret < 0){ + console.log("Failed to select Fly user."); + errors++; + pbf_mash_button(context, BUTTON_B, 3000ms); + continue; + } + + // select Fly (2nd option, but maybe other HMs could change this) + KantoMapWatcher map_opened(COLOR_RED); + context.wait_for_all_requests(); + pbf_move_left_joystick(context, {0, -1}, 200ms, 300ms); + pbf_press_button(context, BUTTON_A, 200ms, 0ms); + ret = wait_until( + console, context, 20000ms, + {map_opened} + ); + + if (ret < 0){ + console.log("Failed to detect Kanto map"); + errors++; + pbf_mash_button(context, BUTTON_B, 4000ms); + continue; + } + + pbf_wait(context, 1000ms); + context.wait_for_all_requests(); + console.log("Kanto map detected."); + return; + } +} + +void fly_from_kanto_map(ConsoleHandle& console, ProControllerContext& context, KantoFlyLocation destination){ + uint64_t errors = 0; + + while (true){ + if (errors > 5){ + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "fly_from_kanto_map(): Failed to inititate Fly five times in a row.", + console + ); + } + + pbf_move_left_joystick(context, {-1, +1}, 4000ms, 500ms); + context.wait_for_all_requests(); + + // blindly move the cursor to the specified fly spot + switch (destination){ + case KantoFlyLocation::pallettown: + pbf_move_left_joystick(context, {0, -1}, 850ms, 100ms); + pbf_move_left_joystick(context, {+1, 0}, 317ms, 100ms); + break; + case KantoFlyLocation::viridiancity: + pbf_move_left_joystick(context, {0, -1}, 633ms, 100ms); + pbf_move_left_joystick(context, {+1, 0}, 317ms, 100ms); + break; + case KantoFlyLocation::pewtercity: + pbf_move_left_joystick(context, {0, -1}, 317ms, 100ms); + pbf_move_left_joystick(context, {+1, 0}, 317ms, 100ms); + break; + case KantoFlyLocation::route4: + pbf_move_left_joystick(context, {0, -1}, 233ms, 100ms); + pbf_move_left_joystick(context, {+1, 0}, 633ms, 100ms); + break; + case KantoFlyLocation::ceruleancity: + pbf_move_left_joystick(context, {0, -1}, 233ms, 100ms); + pbf_move_left_joystick(context, {+1, 0}, 1150ms, 100ms); + break; + case KantoFlyLocation::vermilioncity: + pbf_move_left_joystick(context, {0, -1}, 700ms, 100ms); + pbf_move_left_joystick(context, {+1, 0}, 1150ms, 100ms); + break; + case KantoFlyLocation::route10: + pbf_move_left_joystick(context, {0, -1}, 233ms, 100ms); + pbf_move_left_joystick(context, {+1, 0}, 1500ms, 100ms); + break; + case KantoFlyLocation::lavendertown: + pbf_move_left_joystick(context, {0, -1}, 467ms, 100ms); + pbf_move_left_joystick(context, {+1, 0}, 1500ms, 100ms); + break; + case KantoFlyLocation::celadoncity: + pbf_move_left_joystick(context, {0, -1}, 467ms, 100ms); + pbf_move_left_joystick(context, {+1, 0}, 900ms, 100ms); + break; + case KantoFlyLocation::saffroncity: + pbf_move_left_joystick(context, {0, -1}, 467ms, 100ms); + pbf_move_left_joystick(context, {+1, 0}, 1150ms, 100ms); + break; + case KantoFlyLocation::fuschiacity: + pbf_move_left_joystick(context, {0, -1}, 967ms, 100ms); + pbf_move_left_joystick(context, {+1, 0}, 967ms, 100ms); + break; + case KantoFlyLocation::cinnabarisland: + pbf_move_left_joystick(context, {0, -1}, 1100ms, 100ms); + pbf_move_left_joystick(context, {+1, 0}, 317ms, 100ms); + break; + case KantoFlyLocation::indigoplateau: + pbf_move_left_joystick(context, {0, -1}, 200ms, 100ms); + pbf_move_left_joystick(context, {+1, 0}, 150ms, 100ms); + break; + default: + OperationFailedException::fire( + ErrorReport::SEND_ERROR_REPORT, + "fly_from_kanto_map(): Unimplemented Kanto fly target.", + console + ); + } + + BlackScreenWatcher fly_initiated(COLOR_RED); + context.wait_for_all_requests(); + int ret = run_until( + console, context, + [](ProControllerContext& context) { + // walk up to counter and initiate dialog + pbf_mash_button(context, BUTTON_A, 5000ms); + }, + { fly_initiated } + ); + + pbf_wait(context, 8000ms); + context.wait_for_all_requests(); + + if (ret == 0) { + console.log("Fly initiated."); + return; + }else{ + errors++; + console.log("Failed to detect black screen within 5 seconds of attempting to fly."); + continue; + } + + } + +} + void enter_leave_pokecenter(ConsoleHandle& console, ProControllerContext& context, bool leave){ uint16_t errors = 0; @@ -532,7 +865,7 @@ void enter_leave_pokecenter(ConsoleHandle& console, ProControllerContext& contex if (errors > 5){ OperationFailedException::fire( ErrorReport::SEND_ERROR_REPORT, - leave ? "Failed to exit PokeCenter." : "Failed to enter PokeCenter.", + leave ? "leave_pokecenter(): Failed to exit PokeCenter." : "enter_pokecenter(): Failed to enter PokeCenter.", console ); } @@ -576,7 +909,7 @@ void heal_at_pokecenter(ConsoleHandle& console, ProControllerContext& context){ if (errors > 5) { OperationFailedException::fire( ErrorReport::SEND_ERROR_REPORT, - "Failed to initiate PokeCenter dialog.", + "heal_at_pokecenter(): Failed to initiate PokeCenter dialog.", console ); } @@ -609,58 +942,36 @@ void heal_at_pokecenter(ConsoleHandle& console, ProControllerContext& context){ } } -void open_party_menu_from_overworld(ConsoleHandle& console, ProControllerContext& context){ - uint16_t errors = 0; - bool start_menu_is_open = false; - while (true){ - if (errors > 5){ - OperationFailedException::fire( - ErrorReport::SEND_ERROR_REPORT, - "Failed to open party menu 5 times in a row.", - console - ); - } - - context.wait_for_all_requests(); - if (!start_menu_is_open){ - open_start_menu(console, context); // This is unavoidable since we cannot detect the overworld. - start_menu_is_open = true; - } - - StartMenuWatcher start_menu(COLOR_RED); - PartyMenuWatcher party_menu(COLOR_RED); +int grass_spin(ConsoleHandle& console, ProControllerContext& context, bool leftright, Seconds timeout){ + BlackScreenWatcher battle_triggered(COLOR_RED); + BattleDialogWatcher battle_entered(COLOR_RED); - int ret = wait_until( - console, context, 10000ms, - { start_menu, party_menu } - ); + context.wait_for_all_requests(); + console.log("Starting grass spin."); + WallClock deadline = current_time() + timeout; - switch (ret){ - case 0: - ret = move_cursor_to_position(console, context, SelectionArrowPositionStartMenu::POKEMON); - if (ret < 0){ - console.log("Failed to navigate to POKEMON on the start menu."); - errors++; - context.wait_for_all_requests(); - pbf_mash_button(context, BUTTON_B, 2000ms); - start_menu_is_open = false; - } else { - console.log("Navigated to POKEMON on the start menu"); - context.wait_for_all_requests(); - pbf_press_button(context, BUTTON_A, 200ms, 1300ms); + int ret = run_until( + console, context, + [leftright, deadline](ProControllerContext& context) { + while (current_time() < deadline){ + if (leftright){ + pbf_move_left_joystick(context, {+1, 0}, 33ms, 150ms); + pbf_move_left_joystick(context, {-1, 0}, 33ms, 150ms); + }else{ + pbf_move_left_joystick(context, {0, +1}, 33ms, 150ms); + pbf_move_left_joystick(context, {0, -1}, 33ms, 150ms); + } } - continue; - case 1: - console.log("Party menu opened."); - return; - default: - console.log("Failed to open party menu."); - errors++; - pbf_mash_button(context, BUTTON_B, 2000ms); - start_menu_is_open = false; - continue; - } + }, + { battle_triggered, battle_entered } + ); + + if (ret < 0){ + return -1; } + + bool encounter_shiny = handle_encounter(console, context, true); + return encounter_shiny ? 1 : 0; } void home_black_border_check(ConsoleHandle& console, ProControllerContext& context){ @@ -681,7 +992,7 @@ void home_black_border_check(ConsoleHandle& console, ProControllerContext& conte console.log("Entered game."); }else{ console.log("Non-Switch device selected in Settings."); - console.log("Skipping black border and aspect ratio checks.", COLOR_BLUE); + console.log("Skipping black border check.", COLOR_BLUE); } } diff --git a/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Navigation.h b/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Navigation.h index d593bffd9..682a11ba1 100644 --- a/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Navigation.h +++ b/SerialPrograms/Source/PokemonFRLG/PokemonFRLG_Navigation.h @@ -19,6 +19,23 @@ namespace NintendoSwitch{ using ProControllerContext = ControllerContext; namespace PokemonFRLG{ +enum class KantoFlyLocation{ + pallettown, + viridiancity, + pewtercity, + route4, + ceruleancity, + vermilioncity, + route10, + lavendertown, + celadoncity, + saffroncity, + fuschiacity, + cinnabarisland, + indigoplateau, +}; + + // Press A+B+Select+Start at the same time to soft reset, then re-enters the game. // There are two random waits, one before pressing start and another after loading in the game. // This is to prevent repeatedly getting the same pokemon, due to FRLG's RNG. @@ -32,30 +49,48 @@ uint64_t open_slot_six(ConsoleHandle& console, ProControllerContext& context); // For soft resets, send_out_lead as false and then soft_reset() to save time. bool handle_encounter(ConsoleHandle& console, ProControllerContext& context, bool send_out_lead); +// Mash A to keep using the first move until a Pokemon faints (either the player's or the opponent) +// Flees the battle if out of PP. +// returns 0 if the opponent fainted, 1 if the player Pokemon fainted, 2 if the battle was fled (by either Pokemon), +// and 3 if the battle was fled and the player Pokemon is out of PP. +int spam_first_move(ConsoleHandle& console, ProControllerContext& context); + // Run from battle. Cursor must start on the FIGHT button. Assumes fleeing will always work. (Smoke Ball) void flee_battle(ConsoleHandle& console, ProControllerContext& context); // Exit a wild battle after winning. Checks if a Pokemon is learning a new move. -// Set stop_on_move_learn to true to cause this to exit early when a move is being learned without declining it -// Assumes that the Pokemon will not evolve -bool exit_wild_battle(ConsoleHandle& console, ProControllerContext& context, bool stop_on_move_learn); +// If stop_on_move_learn is true, this exits early when a move is being learned without declining it. +// Otherwise, this returns to the overworld +bool exit_wild_battle(ConsoleHandle& console, ProControllerContext& context, bool stop_on_move_learn, bool prevent_evolution); + +// Starting from the start menu, a sub-screen of the start menu, or the overworld, navigate to the party screen +void open_party_menu_from_overworld(ConsoleHandle& console, ProControllerContext& context); // Uses Teleport to return to a PokeCenter. // Assumes that Teleport is usable and the last party member has it learned -void use_teleport(ConsoleHandle& console, ProControllerContext& context); +void use_teleport_from_overworld(ConsoleHandle& console, ProControllerContext& context); + +// Navigates to the fly map. Assumes that Fly is usable and the last member of your party has it learned +void open_fly_map_from_overworld(ConsoleHandle& console, ProControllerContext& context); + +// Starting from the Kanto Fly map, fly to a specified location. +void fly_from_kanto_map(ConsoleHandle& console, ProControllerContext& context, KantoFlyLocation destination); -// Enter a pokecenter. Assumes the player is standing in front of its door +// Enter a PokeCenter. Assumes the player is standing in front of its door void enter_pokecenter(ConsoleHandle& console, ProControllerContext& context); -// Leave a pokecenter. Assumes the player is standing directly north of the exit +// Leave a PokeCenter. Assumes the player is standing directly north of the exit void leave_pokecenter(ConsoleHandle& console, ProControllerContext& context); // Approach the counter and heal at a PokeCenter. Assumed the player is directly south of the nurse -// Combine with enter_pokecenter, leave_pokecenter, and use_teleport for automating healing your party +// Combine with enter_pokecenter, leave_pokecenter, and use_teleport_from_overworld for automating healing your party void heal_at_pokecenter(ConsoleHandle& console, ProControllerContext& context); -// Starting from the start menu, a sub-screen of the start menu, or the overworld, navigate to the party screen -void open_party_menu_from_overworld(ConsoleHandle& console, ProControllerContext& context); +// Trigger encounters in grass without moving by tapping the left thumbstick back and forth +// Can be used to alternate left/right and up/down. It is important that the player is not facing +// the same direction as the first thumbstick press. +// returns -1 if no encounter is triggered, 0 if a non-shiny is encounter, and 1 if a shiny is encountered +int grass_spin(ConsoleHandle& console, ProControllerContext& context, bool leftright, Seconds timeout = 60s); // Go to home to check that scaling is 100%. Then resume game. void home_black_border_check(ConsoleHandle& console, ProControllerContext& context);