diff --git a/modules/console.py b/modules/console.py index c7dd35573..dff89cafc 100644 --- a/modules/console.py +++ b/modules/console.py @@ -91,7 +91,7 @@ def print_stats(total_stats: dict, pokemon: Pokemon, session_pokemon: set, encou case "basic": console.print( f"{rich_name}: PID: {str(hex(pokemon.personality_value)[2:]).upper()} | " - f"Lv.: {pokemon.level:,} | " + f"Lv: {pokemon.level:,} | " f"Item: {pokemon.held_item.name if pokemon.held_item else '-'} | " f"Nature: {pokemon.nature.name} | " f"Ability: {pokemon.ability.name} | " diff --git a/modules/context.py b/modules/context.py index c87e33391..e2230831e 100644 --- a/modules/context.py +++ b/modules/context.py @@ -39,7 +39,7 @@ def reload_config(self) -> str: raise error message = ( "[bold red]The configuration could not be loaded, no changes have been made.[/]\n" - "[bold yellow]This is Probably due to a malformed file." + "[bold yellow]This is probably due to a malformed file." "For more information run the bot with the --debug flag.[/]" ) return message diff --git a/modules/data/map.py b/modules/data/map.py index a1a942e25..a08d33c18 100644 --- a/modules/data/map.py +++ b/modules/data/map.py @@ -368,9 +368,9 @@ class MapRSE(Enum): ARTISAN_CAVE_A = (24, 100) UNDERWATER_L = (24, 101) MARINE_CAVE = (24, 102) # Outside Kyogre Cave - MARINE_CAVE_A = (24, 103) # Kyogre Cave + MARINE_CAVE_A = (24, 103) # Main Kyogre Cave TERRA_CAVE = (24, 104) # Outside Groudon Cave - TERRA_CAVE_A = (24, 105) # Groudon Cave + TERRA_CAVE_A = (24, 105) # Main Groudon Cave ALTERING_CAVE = (24, 106) METEOR_FALLS_D = (24, 107) @@ -510,8 +510,8 @@ class MapRSE(Enum): NAVEL_ROCK_E = (26, 71) NAVEL_ROCK_F = (26, 72) NAVEL_ROCK_G = (26, 73) - NAVEL_ROCK_H = (26, 74) # Ho-Oh - NAVEL_ROCK_I = (26, 75) # Ho-Oh + NAVEL_ROCK_H = (26, 74) # Below Ho-Oh Room + NAVEL_ROCK_I = (26, 75) # Main Ho-Oh Room NAVEL_ROCK_J = (26, 76) NAVEL_ROCK_K = (26, 77) NAVEL_ROCK_L = (26, 78) @@ -522,8 +522,8 @@ class MapRSE(Enum): NAVEL_ROCK_Q = (26, 83) NAVEL_ROCK_R = (26, 84) NAVEL_ROCK_S = (26, 85) - NAVEL_ROCK_T = (26, 86) # Lugia - NAVEL_ROCK_U = (26, 87) # Lugia + NAVEL_ROCK_T = (26, 86) # Above Lugia Room + NAVEL_ROCK_U = (26, 87) # Main Lugia Room TRAINER_HILL_F = (26, 88) ROUTE_104_C = (27, 0) @@ -723,12 +723,12 @@ class MapFRLG(Enum): THREE_ISLE_PATH = (2, 34) TANOBY_KEY = (2, 35) NAVEL_ROCK = (2, 36) - NAVEL_ROCK_A = (2, 37) - NAVEL_ROCK_B = (2, 38) + NAVEL_ROCK_A = (2, 37) # Main Ho-Oh Room + NAVEL_ROCK_B = (2, 38) # Main Lugia Room NAVEL_ROCK_C = (2, 39) NAVEL_ROCK_D = (2, 40) NAVEL_ROCK_E = (2, 41) - NAVEL_ROCK_F = (2, 42) + NAVEL_ROCK_F = (2, 42) # Below Ho-Oh Room NAVEL_ROCK_G = (2, 43) NAVEL_ROCK_H = (2, 44) NAVEL_ROCK_I = (2, 45) @@ -739,7 +739,7 @@ class MapFRLG(Enum): NAVEL_ROCK_N = (2, 50) NAVEL_ROCK_O = (2, 51) NAVEL_ROCK_P = (2, 52) - NAVEL_ROCK_Q = (2, 53) + NAVEL_ROCK_Q = (2, 53) # Above Lugia Room NAVEL_ROCK_R = (2, 54) NAVEL_ROCK_S = (2, 55) BIRTH_ISLAND = (2, 56) diff --git a/modules/encounter.py b/modules/encounter.py index a0d036f70..b18b46808 100644 --- a/modules/encounter.py +++ b/modules/encounter.py @@ -27,7 +27,22 @@ def encounter_pokemon(pokemon: Pokemon) -> None: total_stats.log_encounter(pokemon, config.catch_block.block_list, custom_filter_result) - context.message = f"Encountered a {pokemon.species.name} with a shiny value of {pokemon.shiny_value:,}!" + encounter_summary = ( + f"Encountered a {pokemon.species.name} with a shiny value of {pokemon.shiny_value:,}!\n\n" + f"PID: {str(hex(pokemon.personality_value)[2:]).upper()} | " + f"Lv: {pokemon.level:,} | " + f"Item: {pokemon.held_item.name if pokemon.held_item else '-'} | " + f"Nature: {pokemon.nature.name} | " + f"Ability: {pokemon.ability.name} \n" + f"IVs: HP: {pokemon.ivs.hp} | " + f"ATK: {pokemon.ivs.attack} | " + f"DEF: {pokemon.ivs.defence} | " + f"SPATK: {pokemon.ivs.special_attack} | " + f"SPDEF: {pokemon.ivs.special_defence} | " + f"SPD: {pokemon.ivs.speed} | " + f"Sum: {pokemon.ivs.sum()}" + ) + context.message = encounter_summary battle_type_flags = get_battle_type_flags() @@ -38,17 +53,19 @@ def encounter_pokemon(pokemon: Pokemon) -> None: save_pk3(pokemon) state_tag = "shiny" console.print("[bold yellow]Shiny found!") - context.message = "Shiny found! Bot has been switched to manual mode so you can catch it." + context.message = ( + f"Shiny found! The bot has been switched to manual mode so you can catch it.\n{encounter_summary}" + ) alert_title = "Shiny found!" - alert_message = f"Found a shiny {pokemon.species.name}. 🥳" + alert_message = f"Found a ✨shiny {pokemon.species.name}✨! 🥳" elif custom_found: if not config.logging.save_pk3.all and config.logging.save_pk3.custom: save_pk3(pokemon) state_tag = "customfilter" console.print("[bold green]Custom filter Pokemon found!") - context.message = f"Custom filter triggered ({custom_filter_result})! Bot has been switched to manual mode so you can catch it." + context.message = f"Custom filter triggered ({custom_filter_result})! The bot has been switched to manual mode so you can catch it.\n{encounter_summary}" alert_title = "Custom filter triggered!" alert_message = f"Found a {pokemon.species.name} that matched one of your filters. ({custom_filter_result})" @@ -56,7 +73,7 @@ def encounter_pokemon(pokemon: Pokemon) -> None: elif BattleTypeFlag.ROAMER in battle_type_flags: state_tag = "roamer" console.print("[bold pink]Roaming Pokemon found!") - context.message = f"Roaming Pokemon found! Bot has been switched to manual mode so you can catch it." + context.message = f"Roaming Pokemon found! The bot has been switched to manual mode so you can catch it.\n{encounter_summary}" alert_title = "Roaming Pokemon found!" alert_message = f"Encountered a roaming {pokemon.species.name}." diff --git a/modules/files.py b/modules/files.py index eac6013dc..3ee5615e6 100644 --- a/modules/files.py +++ b/modules/files.py @@ -72,10 +72,10 @@ def save_pk3(pokemon: Pokemon) -> None: binary_file.write(pokemon.data) -def get_rng_state_history(name: str) -> list: +def get_rng_state_history() -> list: default = [] try: - file = read_file(context.profile.path / "rng" / f"{name}.json") + file = read_file(context.profile.path / "soft_reset_frames.json") data = json.loads(file) if file else default return data except SystemExit: @@ -84,8 +84,8 @@ def get_rng_state_history(name: str) -> list: return default -def save_rng_state_history(name: str, data: list) -> bool: - if write_file(context.profile.path / "rng" / f"{name}.json", json.dumps(data)): +def save_rng_state_history(data: list) -> bool: + if write_file(context.profile.path / "soft_reset_frames.json", json.dumps(data)): return True else: return False diff --git a/modules/libmgba.py b/modules/libmgba.py index 7b3a077ea..7bbb5534a 100644 --- a/modules/libmgba.py +++ b/modules/libmgba.py @@ -431,7 +431,7 @@ def press_button(self, button: str = None, inputs: int = 0): :param button: A GBA button to be pressed, if pressed on previous frame it will be released :param inputs: Alternate raw input bitfield """ - self._pressed_inputs |= (input_map[button] ^ self._prev_pressed_inputs) if not inputs else inputs + self._pressed_inputs |= (self._prev_pressed_inputs & input_map[button]) ^ input_map[button] def hold_button(self, button: str = None, inputs: int = 0): """ diff --git a/modules/main.py b/modules/main.py index 405168027..baa40d7b3 100644 --- a/modules/main.py +++ b/modules/main.py @@ -45,7 +45,7 @@ def main_loop() -> None: if ( not mode and get_game_state() == GameState.BATTLE - and context.bot_mode not in ["Starters", "Legendary Birds"] + and context.bot_mode not in ["Starters", "Static Soft Resets"] ): if opponent_changed(): encounter_pokemon(get_opponent()) @@ -78,10 +78,15 @@ def main_loop() -> None: mode = ModeBunnyHop() - case "Legendary Birds": - from modules.modes.legendary_birds import ModeLegendaryBirds + case "Static Soft Resets": + from modules.modes.soft_resets import ModeStaticSoftResets - mode = ModeLegendaryBirds() + mode = ModeStaticSoftResets() + + case "Tower Duo": + from modules.modes.tower_duo import ModeTowerDuo + + mode = ModeTowerDuo() case "Ancient Legendaries": from modules.modes.ancient_legendaries import ModeAncientLegendaries diff --git a/modules/modes/__init__.py b/modules/modes/__init__.py index 8a4aaa0ea..80de5d329 100644 --- a/modules/modes/__init__.py +++ b/modules/modes/__init__.py @@ -1,3 +1,12 @@ """Contains modes of operation for the bot.""" -available_bot_modes = ["Manual", "Spin", "Starters", "Fishing", "Bunny Hop", "Legendary Birds", "Ancient Legendaries"] +available_bot_modes = [ + "Manual", + "Spin", + "Starters", + "Fishing", + "Bunny Hop", + "Static Soft Resets", + "Tower Duo", + "Ancient Legendaries", +] diff --git a/modules/modes/ancient_legendaries.py b/modules/modes/ancient_legendaries.py index 676e2024b..31e1e4b53 100644 --- a/modules/modes/ancient_legendaries.py +++ b/modules/modes/ancient_legendaries.py @@ -4,18 +4,22 @@ from modules.context import context from modules.data.map import MapRSE from modules.gui.multi_select_window import MultiSelector, Selection, MultiSelectWindow -from modules.memory import get_game_state, GameState, get_event_flag, read_symbol +from modules.memory import get_game_state, GameState, get_event_flag from modules.navigation import follow_path from modules.player import get_player_avatar class ModeAncientLegendariesStates(Enum): - INTERACT = auto() LEAVE_ROOM = auto() + INTERACT = auto() class ModeAncientLegendaries: def __init__(self): + if context.rom.game_title != "POKEMON EMER": # TODO add RS support + context.message("Only Emerald is supported, RS coming soon.") + return + if not context.selected_pokemon: player = get_player_avatar() sprites = Path(__file__).parent.parent.parent / "sprites" / "pokemon" / "normal" @@ -27,7 +31,6 @@ def __init__(self): and not get_event_flag("FLAG_DEFEATED_KYOGRE") and not get_event_flag("FLAG_LEGENDARY_BATTLE_COMPLETED") and player.map_group_and_number == MapRSE.MARINE_CAVE_A.value - and not player.local_coordinates == (9, 26) # Tile that triggers Kyogre to initiate battle and 5 <= player.local_coordinates[0] <= 14 and 26 <= player.local_coordinates[1] <= 27 ) @@ -38,8 +41,7 @@ def __init__(self): and not get_event_flag("FLAG_DEFEATED_GROUDON") and not get_event_flag("FLAG_LEGENDARY_BATTLE_COMPLETED") and player.map_group_and_number == MapRSE.TERRA_CAVE_A.value - and not player.local_coordinates == (17, 26) # Tile that triggers Groudon to initiate battle - and 11 <= player.local_coordinates[0] <= 20 + and 11 <= player.local_coordinates[0] <= 17 and 26 <= player.local_coordinates[1] <= 27 ) ), @@ -48,8 +50,8 @@ def __init__(self): context.rom.game_title == "POKEMON EMER" and not get_event_flag("FLAG_DEFEATED_RAYQUAZA") and player.map_group_and_number == MapRSE.SKY_PILLAR_G.value - and player.local_coordinates == (14, 7) - and player.facing_direction == "Up" + and player.local_coordinates[0] == 14 + and 4 <= player.local_coordinates[1] <= 12 ) ), } @@ -60,10 +62,7 @@ def __init__(self): button_enable=conditions["Kyogre"], button_tooltip="Select Kyogre" if conditions["Kyogre"] - else ( - "Invalid location:\n" - "Place the player anywhere on the platform in Marine Cave, in front of Kyogre" - ), + else "Invalid location:\nPlace the player on the platform in Marine Cave, in front of Kyogre", sprite=sprites / "Kyogre.png", ), Selection( @@ -71,10 +70,7 @@ def __init__(self): button_enable=conditions["Groudon"], button_tooltip="Select Groudon" if conditions["Groudon"] - else ( - "Invalid location:\n" - "Place the player anywhere on the platform in Terra Cave, in front of Groudon" - ), + else "Invalid location:\nPlace the player on the platform in Terra Cave, in front of Groudon", sprite=sprites / "Groudon.png", ), Selection( @@ -92,24 +88,7 @@ def __init__(self): self.state: ModeAncientLegendariesStates = ModeAncientLegendariesStates.LEAVE_ROOM - match context.selected_pokemon: - case "Kyogre": - if not get_event_flag("FLAG_HIDE_MARINE_CAVE_KYOGRE") and not get_event_flag( - "FLAG_LEGENDARY_BATTLE_COMPLETED" - ): - self.state: ModeAncientLegendariesStates = ModeAncientLegendariesStates.INTERACT - case "Groudon": - if not get_event_flag("FLAG_HIDE_TERRA_CAVE_GROUDON") and not get_event_flag( - "FLAG_LEGENDARY_BATTLE_COMPLETED" - ): - self.state: ModeAncientLegendariesStates = ModeAncientLegendariesStates.INTERACT - case "Rayquaza": - if not get_event_flag("FLAG_HIDE_SKY_PILLAR_TOP_RAYQUAZA_STILL"): - self.state: ModeAncientLegendariesStates = ModeAncientLegendariesStates.INTERACT - case _: - return - - def update_state(self, state: ModeAncientLegendariesStates): + def update_state(self, state: ModeAncientLegendariesStates) -> None: self.state: ModeAncientLegendariesStates = state def step(self): @@ -117,53 +96,12 @@ def step(self): player_avatar = get_player_avatar() match self.state, context.selected_pokemon: - case ModeAncientLegendariesStates.INTERACT, "Kyogre": - match get_game_state(): - case GameState.OVERWORLD: - if get_event_flag("FLAG_HIDE_MARINE_CAVE_KYOGRE"): - self.update_state(ModeAncientLegendariesStates.LEAVE_ROOM) - continue - else: - follow_path( # TODO follow_path() needs reworking (not a generator) - [(player_avatar.local_coordinates[0], 26), (9, 26)] - ) - case GameState.BATTLE: - return - - case ModeAncientLegendariesStates.INTERACT, "Groudon": - match get_game_state(): - case GameState.OVERWORLD: - if get_event_flag("FLAG_HIDE_TERRA_CAVE_GROUDON"): - self.update_state(ModeAncientLegendariesStates.LEAVE_ROOM) - continue - else: - follow_path( # TODO follow_path() needs reworking (not a generator) - [(player_avatar.local_coordinates[0], 26), (17, 26)] - ) - case GameState.BATTLE: - return - - case ModeAncientLegendariesStates.INTERACT, "Rayquaza": - match get_game_state(): - case GameState.OVERWORLD: - if ( - get_event_flag("FLAG_HIDE_SKY_PILLAR_TOP_RAYQUAZA_STILL") - and int.from_bytes(read_symbol("gObjectEvents", 0, 1)) - != 1 # TODO look into decoding gObjectEvents properly - https://github.com/pret/pokeemerald/blob/2304283c3ef2675be5999349673b02796db0827d/include/global.fieldmap.h#L168 - ): - self.update_state(ModeAncientLegendariesStates.LEAVE_ROOM) - continue - else: - context.emulator.press_button("A") - case GameState.BATTLE: - return - + # Kyogre case ModeAncientLegendariesStates.LEAVE_ROOM, "Kyogre": if player_avatar.local_coordinates == (9, 26): - context.emulator.hold_button("Down") context.emulator.press_button("B") + context.emulator.press_button("Down") else: - context.emulator.release_button("Down") follow_path( # TODO follow_path() needs reworking (not a generator) [ (player_avatar.local_coordinates[0], 27), @@ -181,14 +119,28 @@ def step(self): (14, 27), ] ) - return + self.update_state(ModeAncientLegendariesStates.INTERACT) + continue + case ModeAncientLegendariesStates.INTERACT, "Kyogre": + match get_game_state(): + case GameState.OVERWORLD: + if get_event_flag("FLAG_HIDE_MARINE_CAVE_KYOGRE"): + self.update_state(ModeAncientLegendariesStates.LEAVE_ROOM) + continue + else: + follow_path( # TODO follow_path() needs reworking (not a generator) + [(player_avatar.local_coordinates[0], 26), (9, 26)] + ) + case GameState.BATTLE: + return + + # Groudon case ModeAncientLegendariesStates.LEAVE_ROOM, "Groudon": if player_avatar.local_coordinates == (17, 26): - context.emulator.hold_button("Left") context.emulator.press_button("B") + context.emulator.press_button("Left") else: - context.emulator.release_button("Left") follow_path( # TODO follow_path() needs reworking (not a generator) [ (player_avatar.local_coordinates[0], 26), @@ -206,22 +158,49 @@ def step(self): (11, 26), ] ) - return + self.update_state(ModeAncientLegendariesStates.INTERACT) + continue + + case ModeAncientLegendariesStates.INTERACT, "Groudon": + match get_game_state(): + case GameState.OVERWORLD: + if get_event_flag("FLAG_HIDE_TERRA_CAVE_GROUDON"): + self.update_state(ModeAncientLegendariesStates.LEAVE_ROOM) + continue + else: + follow_path( # TODO follow_path() needs reworking (not a generator) + [(player_avatar.local_coordinates[0], 26), (17, 26)] + ) + case GameState.BATTLE: + return + # Rayquaza case ModeAncientLegendariesStates.LEAVE_ROOM, "Rayquaza": - follow_path( # TODO follow_path() needs reworking (not a generator) - [ - (14, 11), - (12, 11), - (12, 15), - (16, 15), - (16, -99, MapRSE.SKY_PILLAR_F.value), - (10, -99, MapRSE.SKY_PILLAR_G.value), - (12, 15), - (12, 11), - (14, 11), - (14, 7), - ] - ) - return + if player_avatar.local_coordinates[1] <= 7: + context.emulator.press_button("B") + context.emulator.press_button("Down") + else: + follow_path( # TODO follow_path() needs reworking (not a generator) + [ + (14, 11), + (12, 11), + (12, 15), + (16, 15), + (16, -99, MapRSE.SKY_PILLAR_F.value), + (10, -99, MapRSE.SKY_PILLAR_G.value), + (12, 15), + (12, 11), + (14, 11), + (14, 7), + ] + ) + self.update_state(ModeAncientLegendariesStates.INTERACT) + continue + + case ModeAncientLegendariesStates.INTERACT, "Rayquaza": + match get_game_state(): + case GameState.OVERWORLD: + context.emulator.press_button("A") + case GameState.BATTLE: + return yield diff --git a/modules/modes/legendary_birds.py b/modules/modes/legendary_birds.py deleted file mode 100644 index 6e4041e44..000000000 --- a/modules/modes/legendary_birds.py +++ /dev/null @@ -1,195 +0,0 @@ -import random -from enum import Enum, auto -from pathlib import Path - -from modules.context import context -from modules.data.map import MapFRLG -from modules.encounter import encounter_pokemon -from modules.files import get_rng_state_history, save_rng_state_history -from modules.gui.multi_select_window import Selection, MultiSelector, MultiSelectWindow -from modules.memory import ( - read_symbol, - get_game_state, - GameState, - write_symbol, - unpack_uint32, - pack_uint32, - get_event_flag, -) -from modules.player import get_player_avatar -from modules.pokemon import get_opponent -from modules.tasks import task_is_active - -config = context.config - - -class ModeLegendaryBirdsStates(Enum): - RESET = auto() - TITLE = auto() - OVERWORLD = auto() - INJECT_RNG = auto() - RNG_CHECK = auto() - BATTLE = auto() - OPPONENT_CRY_START = auto() - OPPONENT_CRY_END = auto() - LOG_BIRD = auto() - - -class ModeLegendaryBirds: - def __init__(self) -> None: - if not context.selected_pokemon: - player_avatar = get_player_avatar() - sprites = Path(__file__).parent.parent.parent / "sprites" / "pokemon" / "normal" - conditions = { - "Articuno": bool( - ( - context.rom.game_title in ["POKEMON FIRE", "POKEMON LEAF"] - and not get_event_flag("FLAG_FOUGHT_ARTICUNO") - and player_avatar.map_group_and_number == MapFRLG.SEAFOAM_ISLANDS_D.value - and ( - (player_avatar.local_coordinates == (9, 3) and player_avatar.facing_direction == "Up") - or (player_avatar.local_coordinates == (8, 2) and player_avatar.facing_direction == "Right") - or (player_avatar.local_coordinates == (9, 1) and player_avatar.facing_direction == "Down") - or (player_avatar.local_coordinates == (10, 2) and player_avatar.facing_direction == "Left") - ) - ) - ), - "Zapdos": bool( - ( - context.rom.game_title in ["POKEMON FIRE", "POKEMON LEAF"] - and not get_event_flag("FLAG_FOUGHT_ZAPDOS") - and player_avatar.map_group_and_number == MapFRLG.POWER_PLANT.value - and ( - (player_avatar.local_coordinates == (5, 12) and player_avatar.facing_direction == "Up") - or ( - player_avatar.local_coordinates == (4, 11) and player_avatar.facing_direction == "Right" - ) - or (player_avatar.local_coordinates == (5, 10) and player_avatar.facing_direction == "Down") - or (player_avatar.local_coordinates == (6, 11) and player_avatar.facing_direction == "Left") - ) - ) - ), - "Moltres": bool( - ( - context.rom.game_title in ["POKEMON FIRE", "POKEMON LEAF"] - and not get_event_flag("FLAG_FOUGHT_MOLTRES") - and player_avatar.map_group_and_number == MapFRLG.MT_EMBER_E.value - and ( - (player_avatar.local_coordinates == (9, 7) and player_avatar.facing_direction == "Up") - or (player_avatar.local_coordinates == (8, 6) and player_avatar.facing_direction == "Right") - or (player_avatar.local_coordinates == (9, 5) and player_avatar.facing_direction == "Down") - or (player_avatar.local_coordinates == (10, 6) and player_avatar.facing_direction == "Left") - ) - ) - ), - } - - selections = [ - Selection( - button_label="Articuno", - button_enable=conditions["Articuno"], - button_tooltip="Select Articuno" - if conditions["Articuno"] - else "Invalid location:\nPlace the player and save, facing Articuno in Seafoam Islands", - sprite=sprites / "Articuno.png", - ), - Selection( - button_label="Zapdos", - button_enable=conditions["Zapdos"], - button_tooltip="Select Zapdos" - if conditions["Zapdos"] - else "Invalid location:\nPlace the player and save, facing Zapdos in the Power Plant", - sprite=sprites / "Zapdos.png", - ), - Selection( - button_label="Moltres", - button_enable=conditions["Moltres"], - button_tooltip="Select Moltres" - if conditions["Moltres"] - else "Invalid location:\nPlace the player and save, facing Moltres at the top of Mt. Ember", - sprite=sprites / "Moltres.png", - ), - ] - - options = MultiSelector("Select a legendary bird...", selections) - MultiSelectWindow(context.gui.window, options) - - if context.selected_pokemon not in ["Articuno", "Zapdos", "Moltres"]: - return - - if not config.cheats.random_soft_reset_rng: - self.rng_history: list = get_rng_state_history(context.selected_pokemon) - - self.state: ModeLegendaryBirdsStates = ModeLegendaryBirdsStates.RESET - - def update_state(self, state: ModeLegendaryBirdsStates): - self.state: ModeLegendaryBirdsStates = state - - def step(self): - while True: - match self.state: - case ModeLegendaryBirdsStates.RESET: - context.emulator.reset() - self.update_state(ModeLegendaryBirdsStates.TITLE) - - case ModeLegendaryBirdsStates.TITLE: - match get_game_state(): - case GameState.TITLE_SCREEN: - context.emulator.press_button(random.choice(["A", "Start", "Left", "Right", "Up"])) - case GameState.MAIN_MENU: - if task_is_active("Task_HandleMenuInput"): - context.message = "Waiting for a unique frame before continuing..." - self.update_state(ModeLegendaryBirdsStates.RNG_CHECK) - continue - - case ModeLegendaryBirdsStates.RNG_CHECK: - if config.cheats.random_soft_reset_rng: - self.update_state(ModeLegendaryBirdsStates.OVERWORLD) - else: - rng = unpack_uint32(read_symbol("gRngValue")) - if rng in self.rng_history: - pass - else: - self.rng_history.append(rng) - save_rng_state_history(context.selected_pokemon, self.rng_history) - self.update_state(ModeLegendaryBirdsStates.OVERWORLD) - continue - - case ModeLegendaryBirdsStates.OVERWORLD: - if not task_is_active("Task_DrawFieldMessageBox"): - context.emulator.press_button("A") - else: - self.update_state(ModeLegendaryBirdsStates.INJECT_RNG) - continue - - case ModeLegendaryBirdsStates.INJECT_RNG: - if config.cheats.random_soft_reset_rng: - write_symbol("gRngValue", pack_uint32(random.randint(0, 2**32 - 1))) - self.update_state(ModeLegendaryBirdsStates.BATTLE) - - case ModeLegendaryBirdsStates.BATTLE: - if get_game_state() != GameState.BATTLE: - context.emulator.press_button("A") - else: - self.update_state(ModeLegendaryBirdsStates.OPPONENT_CRY_START) - continue - - case ModeLegendaryBirdsStates.OPPONENT_CRY_START: - if not task_is_active("Task_DuckBGMForPokemonCry"): - context.emulator.press_button("B") - else: - self.update_state(ModeLegendaryBirdsStates.OPPONENT_CRY_END) - continue - - case ModeLegendaryBirdsStates.OPPONENT_CRY_END: # Ensures starter sprite is fully visible before resetting - if task_is_active("Task_DuckBGMForPokemonCry"): - pass - else: - self.update_state(ModeLegendaryBirdsStates.LOG_BIRD) - continue - - case ModeLegendaryBirdsStates.LOG_BIRD: - encounter_pokemon(get_opponent()) - return - - yield diff --git a/modules/modes/soft_resets.py b/modules/modes/soft_resets.py new file mode 100644 index 000000000..4c309f8d0 --- /dev/null +++ b/modules/modes/soft_resets.py @@ -0,0 +1,141 @@ +import random +from enum import Enum, auto + +from modules.context import context +from modules.encounter import encounter_pokemon +from modules.files import get_rng_state_history, save_rng_state_history +from modules.memory import ( + read_symbol, + get_game_state, + GameState, + write_symbol, + unpack_uint32, + pack_uint32, +) +from modules.pokemon import get_opponent, opponent_changed +from modules.tasks import task_is_active + +config = context.config + + +class ModeStaticSoftResetsStates(Enum): + RESET = auto() + TITLE = auto() + WAIT_FRAMES = auto() + OVERWORLD = auto() + INJECT_RNG = auto() + RNG_CHECK = auto() + BATTLE = auto() + OPPONENT_CRY_START = auto() + OPPONENT_CRY_END = auto() + LOG_OPPONENT = auto() + + +class ModeStaticSoftResets: + def __init__(self) -> None: + if not config.cheats.random_soft_reset_rng: + self.rng_history: list = get_rng_state_history() + + self.frame_count = None + self.state: ModeStaticSoftResetsStates = ModeStaticSoftResetsStates.RESET + + def update_state(self, state: ModeStaticSoftResetsStates) -> None: + self.state: ModeStaticSoftResetsStates = state + + def wait_frames(self, frames: int) -> bool: + if not self.frame_count: + self.frame_count = context.emulator.get_frame_count() + elif context.emulator.get_frame_count() < self.frame_count + frames: + return False + else: + self.frame_count = 0 + return True + + def step(self): + while True: + match self.state: + case ModeStaticSoftResetsStates.RESET: + context.emulator.reset() + self.update_state(ModeStaticSoftResetsStates.TITLE) + + case ModeStaticSoftResetsStates.TITLE: + match context.rom.game_title, get_game_state(): + case "POKEMON RUBY" | "POKEMON SAPP" | "POKEMON EMER", GameState.TITLE_SCREEN | GameState.MAIN_MENU: + context.emulator.press_button("A") + + case "POKEMON RUBY" | "POKEMON SAPP" | "POKEMON EMER", GameState.OVERWORLD: + context.message = "Waiting for a unique frame before continuing..." + self.update_state(ModeStaticSoftResetsStates.RNG_CHECK) + continue + + case "POKEMON FIRE" | "POKEMON LEAF", GameState.TITLE_SCREEN: + context.emulator.press_button(random.choice(["A", "Start", "Left", "Right", "Up"])) + + case "POKEMON FIRE" | "POKEMON LEAF", GameState.MAIN_MENU: + if task_is_active("Task_HandleMenuInput"): + context.message = "Waiting for a unique frame before continuing..." + self.update_state(ModeStaticSoftResetsStates.RNG_CHECK) + continue + + case ModeStaticSoftResetsStates.RNG_CHECK: + if config.cheats.random_soft_reset_rng: + self.update_state(ModeStaticSoftResetsStates.WAIT_FRAMES) + else: + rng = unpack_uint32(read_symbol("gRngValue")) + if rng in self.rng_history or ( + task_is_active("Task_ExitNonDoor") + or task_is_active("task_map_chg_seq_0807E20C") + or task_is_active("task_map_chg_seq_0807E2CC") + ): + pass + else: + self.rng_history.append(rng) + save_rng_state_history(self.rng_history) + self.update_state(ModeStaticSoftResetsStates.WAIT_FRAMES) + continue + + case ModeStaticSoftResetsStates.WAIT_FRAMES: + if self.wait_frames(5): + self.update_state(ModeStaticSoftResetsStates.INJECT_RNG) + else: + pass + + case ModeStaticSoftResetsStates.INJECT_RNG: + if config.cheats.random_soft_reset_rng: + write_symbol("gRngValue", pack_uint32(random.randint(0, 2**32 - 1))) + self.update_state(ModeStaticSoftResetsStates.OVERWORLD) + + case ModeStaticSoftResetsStates.OVERWORLD: + if not opponent_changed(): + context.emulator.press_button("A") + else: + self.update_state(ModeStaticSoftResetsStates.BATTLE) + continue + + case ModeStaticSoftResetsStates.BATTLE: + if get_game_state() != GameState.BATTLE: + context.emulator.press_button("A") + else: + self.update_state(ModeStaticSoftResetsStates.OPPONENT_CRY_START) + continue + + case ModeStaticSoftResetsStates.OPPONENT_CRY_START: + if not task_is_active("Task_DuckBGMForPokemonCry"): + context.emulator.press_button("B") + else: + self.update_state(ModeStaticSoftResetsStates.OPPONENT_CRY_END) + continue + + # Ensure opponent sprite is fully visible before resetting + case ModeStaticSoftResetsStates.OPPONENT_CRY_END: + if task_is_active("Task_DuckBGMForPokemonCry"): + pass + else: + self.update_state(ModeStaticSoftResetsStates.LOG_OPPONENT) + continue + + case ModeStaticSoftResetsStates.LOG_OPPONENT: + encounter_pokemon(get_opponent()) + return + + yield diff --git a/modules/modes/starters.py b/modules/modes/starters.py index 87ec4db87..98c1c905a 100644 --- a/modules/modes/starters.py +++ b/modules/modes/starters.py @@ -254,13 +254,15 @@ def __init__(self) -> None: self.task_confirm: str = "TASK_STARTERCHOOSE5" self.task_ball_throw: str = "SUB_814146C" self.task_map_popup: str = "TASK_MAPNAMEPOPUP" + else: + return if not config.cheats.random_soft_reset_rng: - self.rng_history: list = get_rng_state_history(context.selected_pokemon) + self.rng_history: list = get_rng_state_history() self.state: ModeStarterStates = ModeStarterStates.RESET - def update_state(self, state: ModeStarterStates): + def update_state(self, state: ModeStarterStates) -> None: self.state: ModeStarterStates = state def step(self): @@ -293,7 +295,7 @@ def step(self): pass else: self.rng_history.append(rng) - save_rng_state_history(context.selected_pokemon, self.rng_history) + save_rng_state_history(self.rng_history) self.update_state(ModeStarterStates.OVERWORLD) continue @@ -434,7 +436,7 @@ def step(self): pass else: self.rng_history.append(rng) - save_rng_state_history(context.selected_pokemon, self.rng_history) + save_rng_state_history(self.rng_history) self.update_state(ModeStarterStates.CONFIRM_STARTER) continue @@ -529,7 +531,7 @@ def step(self): pass else: self.rng_history.append(rng) - save_rng_state_history(context.selected_pokemon, self.rng_history) + save_rng_state_history(self.rng_history) self.update_state(ModeStarterStates.CONFIRM_STARTER) continue diff --git a/modules/modes/tower_duo.py b/modules/modes/tower_duo.py new file mode 100644 index 000000000..8b3f1478f --- /dev/null +++ b/modules/modes/tower_duo.py @@ -0,0 +1,172 @@ +from enum import Enum, auto +from pathlib import Path + +from modules.context import context +from modules.data.map import MapRSE, MapFRLG +from modules.gui.multi_select_window import MultiSelector, Selection, MultiSelectWindow +from modules.memory import get_game_state, GameState, get_event_flag +from modules.navigation import follow_path +from modules.player import get_player_avatar + + +class ModeTowerDuoStates(Enum): + INTERACT = auto() + LEAVE_ROOM = auto() + + +class ModeTowerDuo: + def __init__(self): + if not context.selected_pokemon: + player = get_player_avatar() + sprites = Path(__file__).parent.parent.parent / "sprites" / "pokemon" / "normal" + + conditions = { + "Lugia": bool( + ( + context.rom.game_title in ["POKEMON EMER", "POKEMON FIRE", "POKEMON LEAF"] + and ( + ( + not get_event_flag("FLAG_CAUGHT_LUGIA") + and player.map_group_and_number == MapRSE.NAVEL_ROCK_U.value + and player.local_coordinates[0] == 11 # Lugia Y coord + and 14 <= player.local_coordinates[1] <= 20 # Anywhere on the Y coord + ) + or ( + not get_event_flag("FLAG_FOUGHT_LUGIA") + and player.map_group_and_number == MapFRLG.NAVEL_ROCK_B.value + and player.local_coordinates[0] == 10 # Lugia Y coord + and 16 <= player.local_coordinates[1] <= 21 # Anywhere on the Y coord + ) + ) + ) + ), + "Ho-Oh": bool( + ( + context.rom.game_title in ["POKEMON EMER", "POKEMON FIRE", "POKEMON LEAF"] + and ( + ( + not get_event_flag("FLAG_CAUGHT_HO_OH") + and player.map_group_and_number == MapRSE.NAVEL_ROCK_I.value + and player.local_coordinates[0] == 12 # Ho-Oh Y coord + and 10 <= player.local_coordinates[1] <= 21 # Anywhere on the Y coord + ) + or ( + not get_event_flag("FLAG_FOUGHT_HO_OH") + and player.map_group_and_number == MapFRLG.NAVEL_ROCK_A.value + and player.local_coordinates[0] == 9 # Ho-Oh Y coord + and 13 <= player.local_coordinates[1] <= 19 # Anywhere on the Y coord + ) + ) + ) + ), + } + + selections = [ + Selection( + button_label="Lugia", + button_enable=conditions["Lugia"], + button_tooltip="Select Lugia" + if conditions["Lugia"] + else "Invalid location:\nPlace the player directly in front of Lugia, in Navel Rock", + sprite=sprites / "Lugia.png", + ), + Selection( + button_label="Ho-Oh", + button_enable=conditions["Ho-Oh"], + button_tooltip="Select Ho-Oh" + if conditions["Ho-Oh"] + else "Invalid location:\nPlace the player directly in front of Ho-Oh, on Navel Rock", + sprite=sprites / "Ho-Oh.png", + ), + ] + + options = MultiSelector("Select a tower duo legendary...", selections) + MultiSelectWindow(context.gui.window, options) + + if context.selected_pokemon in ["Lugia", "Ho-Oh"]: + self.state: ModeTowerDuoStates = ModeTowerDuoStates.LEAVE_ROOM + else: + return + + def update_state(self, state: ModeTowerDuoStates) -> None: + self.state: ModeTowerDuoStates = state + + def step(self): + while True: + player_avatar = get_player_avatar() + + match self.state, context.rom.game_title, context.selected_pokemon: + case ModeTowerDuoStates.LEAVE_ROOM, "POKEMON EMER", "Lugia": + if player_avatar.local_coordinates == (11, 14): + context.emulator.press_button("B") + context.emulator.press_button("Down") + else: + follow_path( # TODO follow_path() needs reworking (not a generator) + [ + (11, 19), + (99, 19, MapRSE.NAVEL_ROCK_T.value), + (4, 5), + (99, 5, MapRSE.NAVEL_ROCK_U.value), + (11, 19), + (11, 14), + ] + ) + self.update_state(ModeTowerDuoStates.INTERACT) + + case ModeTowerDuoStates.LEAVE_ROOM, "POKEMON FIRE" | "POKEMON LEAF", "Lugia": + if player_avatar.local_coordinates == (10, 16): + context.emulator.press_button("B") + context.emulator.press_button("Down") + else: + follow_path( # TODO follow_path() needs reworking (not a generator) + [ + (10, 20), + (99, 20, MapFRLG.NAVEL_ROCK_Q.value), + (3, 4), + (99, 4, MapFRLG.NAVEL_ROCK_B.value), + (10, 20), + (10, 16), + ] + ) + self.update_state(ModeTowerDuoStates.INTERACT) + + case ModeTowerDuoStates.LEAVE_ROOM, "POKEMON EMER", "Ho-Oh": + if player_avatar.local_coordinates == (12, 10): + context.emulator.press_button("B") + context.emulator.press_button("Down") + else: + follow_path( # TODO follow_path() needs reworking (not a generator) + [ + (12, 20), + (99, 20, MapRSE.NAVEL_ROCK_H.value), + (4, 5), + (99, 5, MapRSE.NAVEL_ROCK_I.value), + (12, 20), + (12, 10), + ] + ) + self.update_state(ModeTowerDuoStates.INTERACT) + + case ModeTowerDuoStates.LEAVE_ROOM, "POKEMON FIRE" | "POKEMON LEAF", "Ho-Oh": + if player_avatar.local_coordinates == (9, 12): + context.emulator.press_button("B") + context.emulator.press_button("Down") + else: + follow_path( # TODO follow_path() needs reworking (not a generator) + [ + (9, 18), + (99, 18, MapFRLG.NAVEL_ROCK_F.value), + (3, 4), + (99, 4, MapFRLG.NAVEL_ROCK_A.value), + (9, 18), + (9, 12), + ] + ) + self.update_state(ModeTowerDuoStates.INTERACT) + + case ModeTowerDuoStates.INTERACT, "POKEMON EMER" | "POKEMON FIRE" | "POKEMON LEAF", "Lugia" | "Ho-Oh": + if get_game_state() != GameState.BATTLE: + context.emulator.press_button("A") + else: + return + yield diff --git a/profiles/keys.yml b/profiles/keys.yml index e84158a1f..557820679 100644 --- a/profiles/keys.yml +++ b/profiles/keys.yml @@ -52,6 +52,9 @@ emulator: # Open load save state selection menu load_state: Ctrl+L + # Reload config from disk + reload_config: Ctrl+C + # -- Ignore this unless you want to do programming on the bot code -- # Allows you to put the emulator into 'stepping mode', where frames need to be advanced manually using a button, useful for analysing memory values toggle_stepping_mode: Ctrl+P