diff --git a/args/bosses.py b/args/bosses.py index 2854fc1c..1bb0583d 100644 --- a/args/bosses.py +++ b/args/bosses.py @@ -35,6 +35,8 @@ def parse(parser): help = "Undead status removed from bosses") bosses.add_argument("-bmkl", "--boss-marshal-keep-lobos", action = "store_true", help = "Don't replace the Marshal's Lobos with randomized enemies") + bosses.add_argument("-oops", default = None, nargs = "?", const = "random", type = str, + help = "Oops, all ! Replace all bosses with the specified boss enemy ID or name, or \"random\".") def process(args): if args.mix_bosses_dragons: @@ -47,6 +49,48 @@ def process(args): if vanilla_locations and args.statue_boss_location == BossLocations.MIX: args.statue_boss_location = BossLocations.SHUFFLE + if args.oops is not None: + import data.bosses as bosses + excluded_final_battle_ids = set(bosses.final_battle_enemy_name.keys()) + excluded_final_battle_ids.remove(298) # Keep Kefka (Final) as valid! + + # Build the set of valid boss IDs + valid_boss_ids = set() + for enemy_dict in [bosses.normal_enemy_name, bosses.dragon_enemy_name, bosses.statue_enemy_name, bosses.final_battle_enemy_name]: + valid_boss_ids.update(enemy_dict.keys()) + valid_boss_ids.difference_update(excluded_final_battle_ids) + + try: + # Try to parse as integer ID first + oops_id = int(args.oops) + if oops_id not in valid_boss_ids: + raise ValueError(f"Invalid boss ID: {oops_id}. Must be a valid boss enemy ID.") + args.oops = oops_id + except ValueError as e: + if "Invalid boss ID" in str(e): + raise e + + # If not a valid integer ID, try to parse as normalized name + def normalize(name): + return "".join(c.lower() for c in name if c.isalnum()) + + name_to_id = {} + for enemy_dict in [bosses.normal_enemy_name, bosses.dragon_enemy_name, bosses.statue_enemy_name, bosses.final_battle_enemy_name]: + for eid, name in enemy_dict.items(): + if eid not in excluded_final_battle_ids: + name_to_id[normalize(name)] = eid + + normalized_input = normalize(args.oops) + if normalized_input in name_to_id: + args.oops = name_to_id[normalized_input] + elif normalized_input == "random": + args.oops = "random" + else: + raise ValueError( + f"Invalid boss ID or name: '{args.oops}'. " + f"Please check the enemy maps in data/bosses.py for correct names and IDs." + ) + def flags(args): flags = "" @@ -73,6 +117,8 @@ def flags(args): flags += " -bnu" if args.boss_marshal_keep_lobos: flags += " -bmkl" + if args.oops is not None: + flags += f" -oops {args.oops}" return flags @@ -91,6 +137,10 @@ def options(args): if args.statue_boss_location: statue_battles = args.statue_boss_location.capitalize() + oops = args.oops + if oops == "random": + oops = "Random" + return [ ("Boss Battles", boss_battles, "boss_battles"), ("Dragons", dragon_battles, "dragon_battles"), @@ -100,6 +150,7 @@ def options(args): ("Boss Experience", args.boss_experience, "boss_experience"), ("No Undead", args.boss_no_undead, "boss_no_undead"), ("Marshal Keep Lobos", args.boss_marshal_keep_lobos, "boss_marshal_keep_lobos"), + ("Oops All Boss ID", oops, "oops"), ] def menu(args): diff --git a/data/enemy_formations.py b/data/enemy_formations.py index 8c672e8b..5bcd0587 100644 --- a/data/enemy_formations.py +++ b/data/enemy_formations.py @@ -179,6 +179,64 @@ def mod(self): if self.args.random_encounters_chupon: self.add_chupon() + if self.args.oops is not None: + self.oops_mod() + + def oops_mod(self): + boss_enemy_ids = set() + import data.bosses as bosses + boss_enemy_ids.update(bosses.normal_enemy_name.keys()) + boss_enemy_ids.update(bosses.dragon_enemy_name.keys()) + boss_enemy_ids.update(bosses.statue_enemy_name.keys()) + boss_enemy_ids.update(bosses.final_battle_enemy_name.keys()) + + # The user-selected target boss ID + target_boss_id = self.args.oops + + if target_boss_id == "random": + import random + excluded_final_battle_ids = set(bosses.final_battle_enemy_name.keys()) + excluded_final_battle_ids.remove(298) # Keep Kefka (Final) as valid! + valid_boss_ids = [eid for eid in bosses.enemy_name.keys() if eid not in excluded_final_battle_ids and eid not in bosses.removed_enemy_name] + target_boss_id = random.choice(valid_boss_ids) + self.args.oops = target_boss_id + + boss_name = bosses.enemy_name.get(target_boss_id) + + if self.args.spoiler_log: + from log import section + section("Oops All Bosses", [f" Boss: {boss_name} (ID {target_boss_id})"], []) + + # Find the native mold of the chosen boss from any formation containing it + target_mold = None + for formation in self.formations: + if target_boss_id in formation.enemies(): + target_mold = formation.mold + break + + for formation in self.formations: + has_boss = False + for enemy_index in range(formation.ENEMY_CAPACITY): + if formation.enemy_slots & (1 << enemy_index): + if formation.enemy_ids[enemy_index] in boss_enemy_ids: + has_boss = True + break + + if has_boss: + # Set only the first slot (slot 0) as active to prevent multi-boss VRAM overlays + formation.enemy_slots = 1 + + # Set slot 0's enemy ID to the target boss ID + formation.enemy_ids[0] = target_boss_id + + # Center the single boss perfectly on screen (y = 5, x = 6) + formation.enemy_y_positions[0] = 5 + formation.enemy_x_positions[0] = 6 + + # Apply the native mold for correct VRAM layout & rendering + if target_mold is not None: + formation.mold = target_mold + def print_scripts(self): for formation_index, formation in enumerate(self.formations): if formation.enable_event_script: