diff --git a/modules/battle.py b/modules/battle.py index 1c5ba093..64febb97 100644 --- a/modules/battle.py +++ b/modules/battle.py @@ -30,8 +30,6 @@ from modules.pokemon import get_party, get_opponent, Pokemon, Move, LearnedMove from modules.tasks import get_task, get_tasks, task_is_active -config = context.config - class BattleState(IntEnum): # out-of-battle states @@ -220,7 +218,7 @@ def get_next_func(self): case "None": self.current_step = "init_learn_move" case "init_learn_move": - match config.battle.new_move: + match context.config.battle.new_move: case "stop": context.message = "New move trying to be learned, switching to manual mode..." context.bot_mode = "Manual" @@ -434,7 +432,7 @@ def select_option(self): def handle_evolution(self): while self.battle_state == BattleState.EVOLVING: self.update_battle_state() - if config.battle.stop_evolution: + if context.config.battle.stop_evolution: context.emulator.press_button("B") yield else: @@ -470,10 +468,10 @@ def determine_battle_menu_action(self): :return: an ordered pair containing A) the name of the action to take (fight, switch, flee, etc.) and B) the index of the desired choice. """ - if not config.battle.battle or not can_battle_happen(): + if not context.config.battle.battle or not can_battle_happen(): self.choice = "flee" self.idx = -1 - elif config.battle.replace_lead_battler and self.should_rotate_lead: + elif context.config.battle.replace_lead_battler and self.should_rotate_lead: mon_to_switch = self.get_mon_to_switch() if mon_to_switch is None: self.choice = "flee" @@ -482,11 +480,11 @@ def determine_battle_menu_action(self): self.choice = "switch" self.idx = mon_to_switch else: - match config.battle.battle_method: + match context.config.battle.battle_method: case "strongest": move = self.get_strongest_move() if move == -1: - if config.battle.replace_lead_battler: + if context.config.battle.replace_lead_battler: mon_to_switch = self.get_mon_to_switch() if mon_to_switch is None: self.choice = "flee" @@ -537,7 +535,7 @@ def get_mon_to_switch(self, show_messages=True) -> int | None: Pokémon seem to be fit to fight. :return: the index of the Pokémon to switch with the active Pokémon """ - match config.battle.switch_strategy: + match context.config.battle.switch_strategy: case "first_available": for i in range(len(self.party)): if self.party[i] == self.current_battler or self.party[i].is_egg: @@ -556,7 +554,7 @@ def get_mon_to_switch(self, show_messages=True) -> int | None: @staticmethod def is_valid_move(move: Move) -> bool: - return move is not None and move.name not in config.battle.banned_moves and move.base_power > 0 + return move is not None and move.name not in context.config.battle.banned_moves and move.base_power > 0 @staticmethod def get_move_power(move: LearnedMove, battler: Pokemon, target: Pokemon): @@ -644,7 +642,7 @@ def handle_battler_faint(self): function that handles lead battler fainting """ context.message = "Lead Pokémon fainted!" - match config.battle.faint_action: + match context.config.battle.faint_action: case "stop": context.message = "Switching to manual mode..." context.bot_mode = "Manual" @@ -671,10 +669,10 @@ def handle_battler_faint(self): new_lead = self.get_mon_to_switch() if new_lead is None: context.message = "No viable pokemon to switch in!" - faint_action_default = str(config.battle.faint_action) - config.battle.faint_action = "flee" + faint_action_default = str(context.config.battle.faint_action) + context.config.battle.faint_action = "flee" self.handle_battler_faint() - config.battle.faint_action = faint_action_default + context.config.battle.faint_action = faint_action_default return False switcher = send_out_pokemon(new_lead) for i in switcher: @@ -858,7 +856,7 @@ def calculate_new_move_viability(mon: Pokemon, new_move: Move) -> int: """ # exit learning move if new move is banned or has 0 power - if new_move.base_power == 0 or new_move.name in config.battle.banned_moves: + if new_move.base_power == 0 or new_move.name in context.config.battle.banned_moves: context.message = f"{new_move.name} has base power of 0, so {mon.name} will skip learning it." return 4 # get the effective power of each move @@ -877,7 +875,7 @@ def calculate_new_move_viability(mon: Pokemon, new_move: Move) -> int: power = move.base_power * attack_bonus if move.type in mon.species.types: power *= 1.5 - if move.name in config.battle.banned_moves: + if move.name in context.config.battle.banned_moves: power = 0 move_power.append(power) # find the weakest move of the bunch @@ -907,7 +905,7 @@ def calculate_new_move_viability(mon: Pokemon, new_move: Move) -> int: power = move.base_power * attack_bonus if move.type in mon.species.types: power *= 1.5 - if move.name in config.battle.banned_moves: + if move.name in context.config.battle.banned_moves: power = 0 redundant_move_power.append(power) weakest_move_power = min(redundant_move_power) @@ -950,7 +948,7 @@ def can_battle_happen() -> bool: if ( move is not None and move.move.base_power > 0 - and move.move.name not in config.battle.banned_moves + and move.move.name not in context.config.battle.banned_moves and move.pp > 0 ): return True @@ -1072,17 +1070,20 @@ def execute_menu_action(decision: tuple): return -def check_lead_can_battle(): +def check_lead_can_battle() -> bool: """ Determines whether the lead Pokémon is fit to fight """ + if len(get_party()) < 1: + return False + lead = get_party()[0] lead_has_moves = False for move in lead.moves: if ( move is not None and move.move.base_power > 0 - and move.move.name not in config.battle.banned_moves + and move.move.name not in context.config.battle.banned_moves and move.pp > 0 ): lead_has_moves = True @@ -1115,7 +1116,9 @@ def mon_has_enough_hp(mon: Pokemon) -> bool: def move_is_usable(m: LearnedMove) -> bool: - return m is not None and m.move.base_power > 0 and m.pp > 0 and m.move.name not in config.battle.banned_moves + return ( + m is not None and m.move.base_power > 0 and m.pp > 0 and m.move.name not in context.config.battle.banned_moves + ) class RotatePokemon(BaseMenuNavigator): diff --git a/modules/config/schemas_v1.py b/modules/config/schemas_v1.py index 69ec8063..75c7e638 100644 --- a/modules/config/schemas_v1.py +++ b/modules/config/schemas_v1.py @@ -98,9 +98,12 @@ class Discord(BaseConfig): pokemon_encounter_milestones: DiscordWebhook = Field(default_factory=lambda: DiscordWebhook(interval=10000)) shiny_pokemon_encounter_milestones: DiscordWebhook = Field(default_factory=lambda: DiscordWebhook(interval=5)) total_encounter_milestones: DiscordWebhook = Field(default_factory=lambda: DiscordWebhook(interval=25000)) - phase_summary: DiscordWebhook = Field(default_factory=lambda: DiscordWebhook()) + phase_summary: DiscordWebhook = Field( + default_factory=lambda: DiscordWebhook(first_interval=8192, consequent_interval=5000) + ) anti_shiny_pokemon_encounter: DiscordWebhook = Field(default_factory=lambda: DiscordWebhook()) custom_filter_pokemon_encounter: DiscordWebhook = Field(default_factory=lambda: DiscordWebhook()) + pickup: DiscordWebhook = Field(default_factory=lambda: DiscordWebhook(interval=10)) class DiscordWebhook(BaseConfig): @@ -110,8 +113,8 @@ class DiscordWebhook(BaseConfig): model_config = ConfigDict(coerce_numbers_to_str=True) enable: bool = False - first_interval: PositiveInt | None = 0 # Only used by phase_summary. - consequent_interval: PositiveInt | None = 0 # Only used by phase_summary. + first_interval: PositiveInt | None = 0 + consequent_interval: PositiveInt | None = 0 interval: PositiveInt = 0 ping_mode: Literal["user", "role", None] = None ping_id: str | None = None diff --git a/modules/console.py b/modules/console.py index dff89caf..24f56a3e 100644 --- a/modules/console.py +++ b/modules/console.py @@ -63,11 +63,10 @@ def sv_colour(value: int) -> str: def print_stats(total_stats: dict, pokemon: Pokemon, session_pokemon: set, encounter_rate: int) -> None: type_colour = pokemon.species.types[0].name.lower() rich_name = f"[{type_colour}]{pokemon.species.name}[/]" - console.print("\n") - console.rule(f"{rich_name} encountered at {pokemon.location_met}", style=type_colour) match context.config.logging.console.encounter_data: case "verbose": + console.rule(f"\n{rich_name} encountered at {pokemon.location_met}", style=type_colour) pokemon_table = Table() pokemon_table.add_column("PID", justify="center", width=10) pokemon_table.add_column("Level", justify="center") @@ -89,6 +88,7 @@ def print_stats(total_stats: dict, pokemon: Pokemon, session_pokemon: set, encou ) console.print(pokemon_table) case "basic": + console.rule(f"\n{rich_name} encountered at {pokemon.location_met}", style=type_colour) console.print( f"{rich_name}: PID: {str(hex(pokemon.personality_value)[2:]).upper()} | " f"Lv: {pokemon.level:,} | " @@ -222,7 +222,5 @@ def print_stats(total_stats: dict, pokemon: Pokemon, session_pokemon: set, encou f"Total Shiny Average: {total_stats['totals'].get('shiny_average', 'N/A')})" ) - console.print(f"[yellow]Encounter rate[/]: {encounter_rate:,}/h") - console = Console(theme=theme) diff --git a/modules/discord.py b/modules/discord.py index 8557fdba..fa829758 100644 --- a/modules/discord.py +++ b/modules/discord.py @@ -2,7 +2,9 @@ from pathlib import Path from pypresence import Presence from discord_webhook import DiscordWebhook, DiscordEmbed + from modules.context import context +from modules.version import pokebot_version def discord_message( @@ -48,6 +50,10 @@ def discord_message( if embed_footer: embed_obj.set_footer(text=embed_footer) + else: + embed_obj.set_footer( + text=f"ID: {context.config.discord.bot_id} | {context.rom.game_name}\nPokéBot {pokebot_version}" + ) embed_obj.set_timestamp() webhook.add_embed(embed_obj) diff --git a/modules/encounter.py b/modules/encounter.py index b18b4680..9c81df1a 100644 --- a/modules/encounter.py +++ b/modules/encounter.py @@ -15,17 +15,16 @@ def encounter_pokemon(pokemon: Pokemon) -> None: :return: """ - config = context.config - if config.logging.save_pk3.all: + if context.config.logging.save_pk3.all: save_pk3(pokemon) if pokemon.is_shiny: - config.reload_file("catch_block") + context.config.reload_file("catch_block") custom_filter_result = total_stats.custom_catch_filters(pokemon) custom_found = isinstance(custom_filter_result, str) - total_stats.log_encounter(pokemon, config.catch_block.block_list, custom_filter_result) + total_stats.log_encounter(pokemon, context.config.catch_block.block_list, custom_filter_result) encounter_summary = ( f"Encountered a {pokemon.species.name} with a shiny value of {pokemon.shiny_value:,}!\n\n" @@ -49,7 +48,7 @@ def encounter_pokemon(pokemon: Pokemon) -> None: # TODO temporary until auto-catch is ready if pokemon.is_shiny or custom_found or BattleTypeFlag.ROAMER in battle_type_flags: if pokemon.is_shiny: - if not config.logging.save_pk3.all and config.logging.save_pk3.shiny: + if not context.config.logging.save_pk3.all and context.config.logging.save_pk3.shiny: save_pk3(pokemon) state_tag = "shiny" console.print("[bold yellow]Shiny found!") @@ -61,7 +60,7 @@ def encounter_pokemon(pokemon: Pokemon) -> None: 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: + if not context.config.logging.save_pk3.all and context.config.logging.save_pk3.custom: save_pk3(pokemon) state_tag = "customfilter" console.print("[bold green]Custom filter Pokemon found!") @@ -83,7 +82,7 @@ def encounter_pokemon(pokemon: Pokemon) -> None: alert_title = None alert_message = None - if not custom_found and pokemon.species.name in config.catch_block.block_list: + if not custom_found and pokemon.species.name in context.config.catch_block.block_list: console.print(f"[bold yellow]{pokemon.species.name} is on the catch block list, skipping encounter...") else: filename_suffix = f"{state_tag}_{pokemon.species.safe_name}" @@ -91,7 +90,7 @@ def encounter_pokemon(pokemon: Pokemon) -> None: # TEMPORARY until auto-battle/auto-catch is done # if the mon is saved and imported, no need to catch it by hand - if config.logging.import_pk3: + if context.config.logging.import_pk3: pokemon_storage = get_pokemon_storage() if pokemon_storage.contains_pokemon(pokemon): diff --git a/modules/items.py b/modules/items.py index 3224ba0b..12ca22d9 100644 --- a/modules/items.py +++ b/modules/items.py @@ -45,6 +45,7 @@ class Item: index: int name: str + sprite_name: str price: int type: ItemType pocket: ItemPocket @@ -61,6 +62,7 @@ def from_dict(cls, index: int, data: dict) -> "Item": return Item( index=index, name=data["name"], + sprite_name=data["name"].replace("'", "-").replace(".", "-"), price=data["price"], type=item_type, pocket=ItemPocket(data["pocket"]), diff --git a/modules/main.py b/modules/main.py index 9e368ed4..54e41944 100644 --- a/modules/main.py +++ b/modules/main.py @@ -23,21 +23,18 @@ def main_loop() -> None: """ from modules.encounter import encounter_pokemon # prevents instantiating TotalStats class before profile selected - encounter_counter = 0 pickup_checked = False lead_rotated = False try: mode = None - config = context.config - - if config.discord.rich_presence: + if context.config.discord.rich_presence: from modules.discord import discord_rich_presence Thread(target=discord_rich_presence).start() - if config.obs.http_server.enable: + if context.config.obs.http_server.enable: from modules.web.http import http_server Thread(target=http_server).start() @@ -56,7 +53,6 @@ def main_loop() -> None: pickup_checked = False lead_rotated = False encounter_pokemon(get_opponent()) - encounter_counter += 1 if context.bot_mode != "Manual": mode = BattleHandler() @@ -64,16 +60,18 @@ def main_loop() -> None: if mode: mode = None - elif ( - not mode and config.battle.pickup and should_check_for_pickup(encounter_counter) and not pickup_checked - ): - mode = MenuWrapper(CheckForPickup(encounter_counter)) + elif not mode and context.config.battle.pickup and should_check_for_pickup() and not pickup_checked: pickup_checked = True - encounter_counter = 0 + mode = MenuWrapper(CheckForPickup()) - elif not mode and config.battle.replace_lead_battler and not check_lead_can_battle() and not lead_rotated: - mode = MenuWrapper(RotatePokemon()) + elif ( + not mode + and context.config.battle.replace_lead_battler + and not check_lead_can_battle() + and not lead_rotated + ): lead_rotated = True + mode = MenuWrapper(RotatePokemon()) elif not mode: match context.bot_mode: diff --git a/modules/menuing.py b/modules/menuing.py index a2fcd14e..8a282651 100644 --- a/modules/menuing.py +++ b/modules/menuing.py @@ -11,8 +11,6 @@ from modules.pokemon import get_party from modules.tasks import task_is_active -config = context.config - def party_menu_is_open() -> bool: """ @@ -411,15 +409,16 @@ class CheckForPickup(BaseMenuNavigator): class that handles pickup farming. """ - def __init__(self, encounter_total: int): + def __init__(self): super().__init__() self.party = get_party() self.pokemon_with_pickup = 0 self.pokemon_with_pickup_and_item = [] + self.picked_up_items = [] self.current_mon = -1 self.pickup_threshold_met = None self.check_threshold_met = False - self.check_pickup_threshold(encounter_total) + self.check_pickup_threshold() self.checked = False self.game = context.rom.game_title self.party_menu_opener = None @@ -431,21 +430,30 @@ def get_pokemon_with_pickup_and_item(self): self.pokemon_with_pickup += 1 if mon.held_item is not None: self.pokemon_with_pickup_and_item.append(i) + self.picked_up_items.append(mon.held_item) - def check_pickup_threshold(self, encounter_total): - if config.cheats.faster_pickup: + def check_pickup_threshold(self): + from modules.stats import total_stats + + if context.config.cheats.faster_pickup: self.check_threshold_met = True self.checked = True else: - self.check_threshold_met = encounter_total >= config.battle.pickup_check_frequency + self.check_threshold_met = ( + total_stats.get_session_encounters() % context.config.battle.pickup_check_frequency == 0 + ) self.get_pokemon_with_pickup_and_item() - if config.battle.pickup_threshold > self.pokemon_with_pickup > 0: + if context.config.battle.pickup_threshold > self.pokemon_with_pickup > 0: threshold = self.pokemon_with_pickup - context.message = f"Number of pickup pokemon is {threshold}, which is lower than config. Using\nparty value of {threshold} instead." + context.message = ( + f"Number of pickup pokemon is {threshold}, which is lower than config. " + f"Using party value of {threshold} instead." + ) else: - threshold = config.battle.pickup_threshold + threshold = context.config.battle.pickup_threshold self.pickup_threshold_met = self.check_threshold_met and len(self.pokemon_with_pickup_and_item) >= threshold if self.pickup_threshold_met: + total_stats.update_pickup_items(self.picked_up_items) context.message = "Pickup threshold is met! Gathering items." def open_party_menu(self): @@ -474,7 +482,7 @@ def return_to_party_menu(self): def should_open_party_menu(self): if ( - not config.cheats.faster_pickup + not context.config.cheats.faster_pickup and self.check_threshold_met and not self.checked and self.pokemon_with_pickup > 0 @@ -588,7 +596,12 @@ def step(self): yield _ -def should_check_for_pickup(x: int): - if config.cheats.faster_pickup or x >= config.battle.pickup_check_frequency: +def should_check_for_pickup(): + from modules.stats import total_stats + + if ( + context.config.cheats.faster_pickup + or total_stats.get_session_encounters() % context.config.battle.pickup_check_frequency == 0 + ): return True return False diff --git a/modules/modes/soft_resets.py b/modules/modes/soft_resets.py index 4c309f8d..843ae5c8 100644 --- a/modules/modes/soft_resets.py +++ b/modules/modes/soft_resets.py @@ -15,8 +15,6 @@ from modules.pokemon import get_opponent, opponent_changed from modules.tasks import task_is_active -config = context.config - class ModeStaticSoftResetsStates(Enum): RESET = auto() @@ -33,7 +31,7 @@ class ModeStaticSoftResetsStates(Enum): class ModeStaticSoftResets: def __init__(self) -> None: - if not config.cheats.random_soft_reset_rng: + if not context.config.cheats.random_soft_reset_rng: self.rng_history: list = get_rng_state_history() self.frame_count = None @@ -78,7 +76,7 @@ def step(self): continue case ModeStaticSoftResetsStates.RNG_CHECK: - if config.cheats.random_soft_reset_rng: + if context.config.cheats.random_soft_reset_rng: self.update_state(ModeStaticSoftResetsStates.WAIT_FRAMES) else: rng = unpack_uint32(read_symbol("gRngValue")) @@ -101,7 +99,7 @@ def step(self): pass case ModeStaticSoftResetsStates.INJECT_RNG: - if config.cheats.random_soft_reset_rng: + if context.config.cheats.random_soft_reset_rng: write_symbol("gRngValue", pack_uint32(random.randint(0, 2**32 - 1))) self.update_state(ModeStaticSoftResetsStates.OVERWORLD) diff --git a/modules/modes/starters.py b/modules/modes/starters.py index 98c1c905..0439cd05 100644 --- a/modules/modes/starters.py +++ b/modules/modes/starters.py @@ -14,8 +14,6 @@ from modules.pokemon import get_party, opponent_changed from modules.tasks import get_task, task_is_active -config = context.config - class Regions(Enum): KANTO_STARTERS = auto() @@ -257,7 +255,7 @@ def __init__(self) -> None: else: return - if not config.cheats.random_soft_reset_rng: + if not context.config.cheats.random_soft_reset_rng: self.rng_history: list = get_rng_state_history() self.state: ModeStarterStates = ModeStarterStates.RESET @@ -287,7 +285,7 @@ def step(self): continue case ModeStarterStates.RNG_CHECK: - if config.cheats.random_soft_reset_rng: + if context.config.cheats.random_soft_reset_rng: self.update_state(ModeStarterStates.OVERWORLD) else: rng = unpack_uint32(read_symbol("gRngValue")) @@ -308,7 +306,7 @@ def step(self): continue case ModeStarterStates.INJECT_RNG: - if config.cheats.random_soft_reset_rng: + if context.config.cheats.random_soft_reset_rng: write_symbol("gRngValue", pack_uint32(random.randint(0, 2**32 - 1))) self.update_state(ModeStarterStates.SELECT_STARTER) @@ -334,7 +332,7 @@ def step(self): continue case ModeStarterStates.EXIT_MENUS: - if not config.cheats.fast_check_starters: + if not context.config.cheats.fast_check_starters: if player_avatar.facing_direction != "Down": context.emulator.press_button("B") context.emulator.hold_button("Down") @@ -415,7 +413,7 @@ def step(self): continue case ModeStarterStates.INJECT_RNG: - if config.cheats.random_soft_reset_rng: + if context.config.cheats.random_soft_reset_rng: write_symbol("gRngValue", pack_uint32(random.randint(0, 2**32 - 1))) self.update_state(ModeStarterStates.YES_NO) @@ -428,7 +426,7 @@ def step(self): continue case ModeStarterStates.RNG_CHECK: - if config.cheats.random_soft_reset_rng: + if context.config.cheats.random_soft_reset_rng: self.update_state(ModeStarterStates.CONFIRM_STARTER) else: rng = unpack_uint32(read_symbol("gRngValue")) @@ -448,7 +446,7 @@ def step(self): continue case ModeStarterStates.EXIT_MENUS: - if config.cheats.fast_check_starters: + if context.config.cheats.fast_check_starters: self.update_state(ModeStarterStates.CHECK_STARTER) continue else: @@ -497,7 +495,7 @@ def step(self): continue case ModeStarterStates.INJECT_RNG: - if config.cheats.random_soft_reset_rng: + if context.config.cheats.random_soft_reset_rng: write_symbol("gRngValue", pack_uint32(random.randint(0, 2**32 - 1))) self.update_state(ModeStarterStates.BAG_MENU) @@ -523,7 +521,7 @@ def step(self): continue case ModeStarterStates.RNG_CHECK: - if config.cheats.random_soft_reset_rng: + if context.config.cheats.random_soft_reset_rng: self.update_state(ModeStarterStates.CONFIRM_STARTER) else: rng = unpack_uint32(read_symbol("gRngValue")) @@ -536,7 +534,7 @@ def step(self): continue case ModeStarterStates.CONFIRM_STARTER: - if config.cheats.fast_check_starters: + if context.config.cheats.fast_check_starters: if len(get_party()) > 0: self.update_state(ModeStarterStates.LOG_STARTER) context.emulator.press_button("A") diff --git a/modules/stats.py b/modules/stats.py index 85faa5cf..988a3607 100644 --- a/modules/stats.py +++ b/modules/stats.py @@ -3,23 +3,28 @@ import math import sys import time +import random import importlib from collections import deque from threading import Thread from datetime import datetime +from collections import Counter from modules.console import console, print_stats from modules.context import context from modules.csv import log_encounter_to_csv +from modules.discord import discord_message from modules.files import read_file, write_file from modules.memory import get_game_state, GameState from modules.pokemon import Pokemon +from modules.runtime import get_sprites_path class TotalStats: def __init__(self): self.session_encounters: int = 0 self.session_pokemon: set = set() + self.discord_picked_up_items: dict = {} self.encounter_log: deque[dict] = deque(maxlen=10) self.encounter_timestamps: deque[float] = deque(maxlen=100) self.cached_timestamp: str = "" @@ -75,6 +80,9 @@ def append_shiny_log(self, pokemon: Pokemon) -> None: self.shiny_log["shiny_log"].append(self.get_log_obj(pokemon)) write_file(self.files["shiny_log"], json.dumps(self.shiny_log, indent=4, sort_keys=True)) + def get_session_encounters(self) -> int: + return self.session_encounters + def get_total_stats(self) -> dict: return self.total_stats @@ -339,7 +347,7 @@ def log_encounter(self, pokemon: Pokemon, block_list: list, custom_filter_result print_stats(self.total_stats, pokemon, self.session_pokemon, self.get_encounter_rate()) - # Run custom code in custom_hooks in a thread + # Run custom code/Discord webhooks in custom_hooks in a thread to not hold up bot hook = ( Pokemon(pokemon.data), copy.deepcopy(self.total_stats), @@ -355,5 +363,56 @@ def log_encounter(self, pokemon: Pokemon, block_list: list, custom_filter_result # Save stats file write_file(self.files["totals"], json.dumps(self.total_stats, indent=4, sort_keys=True)) + def update_pickup_items(self, picked_up_items) -> None: + self.total_stats["totals"]["pickup"] = self.total_stats["totals"].get("pickup", {}) + + item_names = [i.name for i in picked_up_items] + + item_count = {} + for item_name in item_names: + item_count[item_name] = item_count.get(item_name, 0) + 1 + + pickup_stats = {} + for item, count in item_count.items(): + pickup_stats |= {f"{item}": count} + + self.total_stats["totals"]["pickup"] = Counter(self.total_stats["totals"]["pickup"]) + Counter(pickup_stats) + + # Save stats file + write_file(self.files["totals"], json.dumps(self.total_stats, indent=4, sort_keys=True)) + + if context.config.discord.pickup.enable: + self.discord_picked_up_items = Counter(self.discord_picked_up_items) + Counter(item_count) + + if sum(self.discord_picked_up_items.values()) >= context.config.discord.pickup.interval: + sprite_names = [i.sprite_name for i in picked_up_items] + + item_list = [] + for item, count in self.discord_picked_up_items.items(): + item_list.append(f"{item} ({count})") + + self.discord_picked_up_items = {} + + def pickup_discord_webhook(): + discord_ping = "" + match context.config.discord.pickup.ping_mode: + case "role": + discord_ping = f"📢 <@&{context.config.discord.pickup.ping_id}>" + case "user": + discord_ping = f"📢 <@{context.config.discord.pickup.ping_id}>" + + discord_message( + webhook_url=context.config.discord.pickup.webhook_url, + content=discord_ping, + embed=True, + embed_title="🦝 Pickup notification", + embed_description="New items have been picked by your team!", + embed_fields={"Items:": "\n".join(item_list)}, + embed_thumbnail=get_sprites_path() / "items" / f"{random.choice(sprite_names)}.png", + embed_color="fc6203", + ) + + Thread(target=pickup_discord_webhook).start() + total_stats = TotalStats() diff --git a/profiles/customhooks.py b/profiles/customhooks.py index b8d1304e..28d89fe2 100644 --- a/profiles/customhooks.py +++ b/profiles/customhooks.py @@ -8,9 +8,6 @@ from modules.discord import discord_message from modules.pokemon import Pokemon from modules.runtime import get_sprites_path -from modules.version import pokebot_version - -config = context.config def custom_hooks(hook) -> None: @@ -32,7 +29,7 @@ def custom_hooks(hook) -> None: # Discord messages def IVField() -> str: # Formatted IV table - if config.discord.iv_format == "formatted": + if context.config.discord.iv_format == "formatted": iv_field = ( "```" "╔═══╤═══╤═══╤═══╤═══╤═══╗\n" @@ -81,26 +78,23 @@ def PhaseSummary() -> dict: ), } - def Footer() -> str: - return f"ID: {config.discord.bot_id} | {context.rom.game_name}\nPokéBot {pokebot_version}" - try: # Discord shiny Pokémon encountered - if config.discord.shiny_pokemon_encounter.enable and pokemon.is_shiny: + if context.config.discord.shiny_pokemon_encounter.enable and pokemon.is_shiny: # Discord pings discord_ping = "" - match config.discord.shiny_pokemon_encounter.ping_mode: + match context.config.discord.shiny_pokemon_encounter.ping_mode: case "role": - discord_ping = f"📢 <@&{config.discord.shiny_pokemon_encounter.ping_id}>" + discord_ping = f"📢 <@&{context.config.discord.shiny_pokemon_encounter.ping_id}>" case "user": - discord_ping = f"📢 <@{config.discord.shiny_pokemon_encounter.ping_id}>" + discord_ping = f"📢 <@{context.config.discord.shiny_pokemon_encounter.ping_id}>" block = ( "\n❌Skipping catching shiny (on catch block list)!" if pokemon.species.name in block_list else "" ) discord_message( - webhook_url=config.discord.shiny_pokemon_encounter.webhook_url, + webhook_url=context.config.discord.shiny_pokemon_encounter.webhook_url, content=f"Encountered a shiny ✨ {pokemon.species.name} ✨! {block}\n{discord_ping}", embed=True, embed_title="Shiny encountered!", @@ -116,7 +110,6 @@ def Footer() -> str: } | PhaseSummary(), embed_thumbnail=get_sprites_path() / "pokemon" / "shiny" / f"{pokemon.species.safe_name}.png", - embed_footer=Footer(), embed_color="ffd242", ) except: @@ -125,25 +118,24 @@ def Footer() -> str: try: # Discord Pokémon encounter milestones if ( - config.discord.pokemon_encounter_milestones.enable + context.config.discord.pokemon_encounter_milestones.enable and stats["pokemon"][pokemon.species.name].get("encounters", -1) - % config.discord.pokemon_encounter_milestones.interval + % context.config.discord.pokemon_encounter_milestones.interval == 0 ): # Discord pings discord_ping = "" - match config.discord.pokemon_encounter_milestones.ping_mode: + match context.config.discord.pokemon_encounter_milestones.ping_mode: case "role": - discord_ping = f"📢 <@&{config.discord.pokemon_encounter_milestones.ping_id}>" + discord_ping = f"📢 <@&{context.config.discord.pokemon_encounter_milestones.ping_id}>" case "user": - discord_ping = f"📢 <@{config.discord.pokemon_encounter_milestones.ping_id}>" + discord_ping = f"📢 <@{context.config.discord.pokemon_encounter_milestones.ping_id}>" discord_message( - webhook_url=config.discord.pokemon_encounter_milestones.webhook_url, + webhook_url=context.config.discord.pokemon_encounter_milestones.webhook_url, content=f"🎉 New milestone achieved!\n{discord_ping}", embed=True, embed_description=f"{stats['pokemon'][pokemon.species.name].get('encounters', 0):,} {pokemon.species.name} encounters!", embed_thumbnail=get_sprites_path() / "pokemon" / "normal" / f"{pokemon.species.safe_name}.png", - embed_footer=Footer(), embed_color="50C878", ) except: @@ -152,26 +144,25 @@ def Footer() -> str: try: # Discord shiny Pokémon encounter milestones if ( - config.discord.shiny_pokemon_encounter_milestones.enable + context.config.discord.shiny_pokemon_encounter_milestones.enable and pokemon.is_shiny and stats["pokemon"][pokemon.species.name].get("shiny_encounters", -1) - % config.discord.shiny_pokemon_encounter_milestones.interval + % context.config.discord.shiny_pokemon_encounter_milestones.interval == 0 ): # Discord pings discord_ping = "" - match config.discord.shiny_pokemon_encounter_milestones.ping_mode: + match context.config.discord.shiny_pokemon_encounter_milestones.ping_mode: case "role": - discord_ping = f"📢 <@&{config.discord.shiny_pokemon_encounter_milestones.ping_id}>" + discord_ping = f"📢 <@&{context.config.discord.shiny_pokemon_encounter_milestones.ping_id}>" case "user": - discord_ping = f"📢 <@{config.discord.shiny_pokemon_encounter_milestones.ping_id}>" + discord_ping = f"📢 <@{context.config.discord.shiny_pokemon_encounter_milestones.ping_id}>" discord_message( - webhook_url=config.discord.shiny_pokemon_encounter_milestones.webhook_url, + webhook_url=context.config.discord.shiny_pokemon_encounter_milestones.webhook_url, content=f"🎉 New milestone achieved!\n{discord_ping}", embed=True, embed_description=f"{stats['pokemon'][pokemon.species.name].get('shiny_encounters', 0):,} shiny ✨ {pokemon.species.name} ✨ encounters!", embed_thumbnail=get_sprites_path() / "pokemon" / "shiny" / f"{pokemon.species.safe_name}.png", - embed_footer=Footer(), embed_color="ffd242", ) except: @@ -180,16 +171,17 @@ def Footer() -> str: try: # Discord total encounter milestones if ( - config.discord.total_encounter_milestones.enable - and stats["totals"].get("encounters", -1) % config.discord.total_encounter_milestones.interval == 0 + context.config.discord.total_encounter_milestones.enable + and stats["totals"].get("encounters", -1) % context.config.discord.total_encounter_milestones.interval + == 0 ): # Discord pings discord_ping = "" - match config.discord.total_encounter_milestones.ping_mode: + match context.config.discord.total_encounter_milestones.ping_mode: case "role": - discord_ping = f"📢 <@&{config.discord.total_encounter_milestones.ping_id}>" + discord_ping = f"📢 <@&{context.config.discord.total_encounter_milestones.ping_id}>" case "user": - discord_ping = f"📢 <@{config.discord.total_encounter_milestones.ping_id}>" + discord_ping = f"📢 <@{context.config.discord.total_encounter_milestones.ping_id}>" embed_thumbnail = random.choice( [ @@ -211,12 +203,11 @@ def Footer() -> str: ) discord_message( - webhook_url=config.discord.total_encounter_milestones.webhook_url, + webhook_url=context.config.discord.total_encounter_milestones.webhook_url, content=f"🎉 New milestone achieved!\n{discord_ping}", embed=True, embed_description=f"{stats['totals'].get('encounters', 0):,} total encounters!", embed_thumbnail=get_sprites_path() / "items" / f"{embed_thumbnail}.png", - embed_footer=Footer(), embed_color="50C878", ) except: @@ -225,31 +216,31 @@ def Footer() -> str: try: # Discord phase encounter notifications if ( - config.discord.phase_summary.enable + context.config.discord.phase_summary.enable and not pokemon.is_shiny and ( - stats["totals"].get("phase_encounters", -1) == config.discord.phase_summary.first_interval + stats["totals"].get("phase_encounters", -1) == context.config.discord.phase_summary.first_interval or ( - stats["totals"].get("phase_encounters", -1) > config.discord.phase_summary.first_interval + stats["totals"].get("phase_encounters", -1) + > context.config.discord.phase_summary.first_interval and stats["totals"].get("phase_encounters", -1) - % config.discord.phase_summary.consequent_interval + % context.config.discord.phase_summary.consequent_interval == 0 ) ) ): # Discord pings discord_ping = "" - match config.discord.phase_summary.ping_mode: + match context.config.discord.phase_summary.ping_mode: case "role": - discord_ping = f"📢 <@&{config.discord.phase_summary.ping_id}>" + discord_ping = f"📢 <@&{context.config.discord.phase_summary.ping_id}>" case "user": - discord_ping = f"📢 <@{config.discord.phase_summary.ping_id}>" + discord_ping = f"📢 <@{context.config.discord.phase_summary.ping_id}>" discord_message( - webhook_url=config.discord.phase_summary.webhook_url, + webhook_url=context.config.discord.phase_summary.webhook_url, content=f"💀 The current phase has reached {stats['totals'].get('phase_encounters', 0):,} encounters!\n{discord_ping}", embed=True, embed_fields=PhaseSummary(), - embed_footer=Footer(), embed_color="D70040", ) except: @@ -257,16 +248,16 @@ def Footer() -> str: try: # Discord anti-shiny Pokémon encountered - if config.discord.anti_shiny_pokemon_encounter.enable and pokemon.is_anti_shiny: + if context.config.discord.anti_shiny_pokemon_encounter.enable and pokemon.is_anti_shiny: # Discord pings discord_ping = "" - match config.discord.anti_shiny_pokemon_encounter.ping_mode: + match context.config.discord.anti_shiny_pokemon_encounter.ping_mode: case "role": - discord_ping = f"📢 <@&{config.discord.anti_shiny_pokemon_encounter.ping_id}>" + discord_ping = f"📢 <@&{context.config.discord.anti_shiny_pokemon_encounter.ping_id}>" case "user": - discord_ping = f"📢 <@{config.discord.anti_shiny_pokemon_encounter.ping_id}>" + discord_ping = f"📢 <@{context.config.discord.anti_shiny_pokemon_encounter.ping_id}>" discord_message( - webhook_url=config.discord.anti_shiny_pokemon_encounter.webhook_url, + webhook_url=context.config.discord.anti_shiny_pokemon_encounter.webhook_url, content=f"Encountered an anti-shiny 💀 {pokemon.species.name} 💀!\n{discord_ping}", embed=True, embed_title="Anti-Shiny encountered!", @@ -280,7 +271,6 @@ def Footer() -> str: } | PhaseSummary(), embed_thumbnail=get_sprites_path() / "pokemon" / "anti-shiny" / f"{pokemon.species.safe_name}.png", - embed_footer=Footer(), embed_color="000000", ) except: @@ -288,17 +278,17 @@ def Footer() -> str: try: # Discord Pokémon matching custom filter encountered - if config.discord.custom_filter_pokemon_encounter.enable and isinstance(custom_filter_result, str): + if context.config.discord.custom_filter_pokemon_encounter.enable and isinstance(custom_filter_result, str): # Discord pings discord_ping = "" - match config.discord.custom_filter_pokemon_encounter.ping_mode: + match context.config.discord.custom_filter_pokemon_encounter.ping_mode: case "role": - discord_ping = f"📢 <@&{config.discord.custom_filter_pokemon_encounter.ping_id}>" + discord_ping = f"📢 <@&{context.config.discord.custom_filter_pokemon_encounter.ping_id}>" case "user": - discord_ping = f"📢 <@{config.discord.custom_filter_pokemon_encounter.ping_id}>" + discord_ping = f"📢 <@{context.config.discord.custom_filter_pokemon_encounter.ping_id}>" discord_message( - webhook_url=config.discord.custom_filter_pokemon_encounter.webhook_url, + webhook_url=context.config.discord.custom_filter_pokemon_encounter.webhook_url, content=f"Encountered a {pokemon.species.name} matching custom filter: `{custom_filter_result}`!\n{discord_ping}", embed=True, embed_title="Encountered Pokémon matching custom catch filter!", @@ -312,7 +302,6 @@ def Footer() -> str: } | PhaseSummary(), embed_thumbnail=get_sprites_path() / "pokemon" / "normal" / f"{pokemon.species.safe_name}.png", - embed_footer=Footer(), embed_color="6a89cc", ) except: @@ -324,13 +313,13 @@ def Footer() -> str: try: # Post the most recent OBS stream screenshot to Discord # (screenshot is taken in stats.py before phase resets) - if config.obs.discord_webhook_url and pokemon.is_shiny: + if context.config.obs.discord_webhook_url and pokemon.is_shiny: def OBSDiscordScreenshot(): time.sleep(3) # Give the screenshot some time to save to disk - images = glob.glob(f"{config.obs.replay_dir}*.png") + images = glob.glob(f"{context.config.obs.replay_dir}*.png") image = max(images, key=os.path.getctime) - discord_message(webhook_url=config.obs.discord_webhook_url, image=image) + discord_message(webhook_url=context.config.obs.discord_webhook_url, image=image) # Run in a thread to not hold up other hooks Thread(target=OBSDiscordScreenshot).start() @@ -339,12 +328,12 @@ def OBSDiscordScreenshot(): try: # Save OBS replay buffer n frames after encountering a shiny - if config.obs.replay_buffer and pokemon.is_shiny: + if context.config.obs.replay_buffer and pokemon.is_shiny: def OBSReplayBuffer(): from modules.obs import obs_hot_key - time.sleep(config.obs.replay_buffer_delay) + time.sleep(context.config.obs.replay_buffer_delay) obs_hot_key("OBS_KEY_F12", pressCtrl=True) # Run in a thread to not hold up other hooks diff --git a/profiles/discord.yml b/profiles/discord.yml index 39b75a26..345c3fdf 100644 --- a/profiles/discord.yml +++ b/profiles/discord.yml @@ -58,3 +58,12 @@ custom_filter_pokemon_encounter: ping_mode: ping_id: #webhook_url: + +# Pickup new items notification +pickup: + enable: false + interval: 10 + ping_mode: + ping_id: + #webhook_url: +