diff --git a/args/encounters.py b/args/encounters.py index 2f3d6c41..ee3d589b 100644 --- a/args/encounters.py +++ b/args/encounters.py @@ -12,11 +12,19 @@ def parse(parser): help = "Random encounters are randomized") random.add_argument("-rechu", "--random-encounters-chupon", action = "store_true", help = "All Random Encounters are replaced with Chupon (Coliseum)") + random.add_argument("-rews", "--random-encounters-world-shuffle", action = "store_true", + help = "Random encounters are shuffled by world") + random.add_argument("-rewr", "--random-encounters-world-random", + default = None, type = int, metavar = "PERCENT", choices = range(101), + help = "Random encounters are randomized with encounters from the same world") fixed = encounters.add_mutually_exclusive_group() fixed.add_argument("-fer", "--fixed-encounters-random", default = None, type = int, metavar = "PERCENT", choices = range(101), help = "Fixed encounters are randomized. Lete River, Serpent Trench, Mine Cart, Imperial Camp, ...") + fixed.add_argument("-fewr", "--fixed-encounters-world-random", + default = None, type = int, metavar = "PERCENT", choices = range(101), + help = "Fixed encounters are randomized with encounters from the same world. Lete River, Serpent Trench, Mine Cart, Imperial Camp, ...") escapable = encounters.add_mutually_exclusive_group() escapable.add_argument("-escr", "--encounters-escapable-random", @@ -24,8 +32,9 @@ def parse(parser): help = "Percent of random encounters escapable including with warp or smoke bombs") def process(args): - args.random_encounters_original = not args.random_encounters_shuffle and args.random_encounters_random is None - args.fixed_encounters_original = args.fixed_encounters_random is None + args.random_encounters_original = not args.random_encounters_shuffle and args.random_encounters_random is None \ + and not args.random_encounters_world_shuffle and args.random_encounters_world_random is None + args.fixed_encounters_original = args.fixed_encounters_random is None and args.fixed_encounters_world_random is None args.encounters_escapable_original = args.encounters_escapable_random is None def flags(args): @@ -37,9 +46,16 @@ def flags(args): flags += f" -rer {args.random_encounters_random}" elif args.random_encounters_chupon: flags += " -rechu" + elif args.random_encounters_world_shuffle: + flags += " -rews" + elif args.fixed_encounters_world_random is not None: + flags += f" -rewr {args.fixed_encounters_world_random}" if args.fixed_encounters_random is not None: flags += f" -fer {args.fixed_encounters_random}" + elif args.fixed_encounters_world_random is not None: + flags += f" -fewr {args.fixed_encounters_world_random}" + if args.encounters_escapable_random is not None: flags += f" -escr {args.encounters_escapable_random}" @@ -56,14 +72,22 @@ def options(args): random_encounters = "Random" elif args.random_encounters_chupon: random_encounters = "Chupon" + elif args.random_encounters_world_shuffle: + random_encounters = "WShuffle" + elif args.fixed_encounters_world_random is not None: + random_encounters = "WRandom" result.append(("Random Encounters", random_encounters, "random_encounters")) if args.random_encounters_random is not None: result.append(("Boss Percent", f"{args.random_encounters_random}%", "random_encounters_random")) + elif args.fixed_encounters_world_random is not None: + result.append(("Boss Percent", f"{args.fixed_encounters_world_random}%", "fixed_encounters_world_random")) fixed_encounters = "Original" if args.fixed_encounters_random is not None: fixed_encounters = "Random" + elif args.fixed_encounters_world_random is not None: + fixed_encounters = "WRandom" result.append(("Fixed Encounters", fixed_encounters, "fixed_encounters")) if args.fixed_encounters_random is not None: diff --git a/data/bosses.py b/data/bosses.py index 60f355e6..6534133f 100644 --- a/data/bosses.py +++ b/data/bosses.py @@ -159,6 +159,53 @@ 487 : "Tritoch", 505 : "Kefka (Narshe)", } + +wob_formation_name = { + 4 : "Marshal", + 79 : "Rizopas", + 387 : "Ultros 3", + 409 : "Leader", + 432 : "Whelk", + 435 : "Vargas", + 436 : "TunnelArmr", + 437 : "GhostTrain", + 438 : "Dadaluma", + 439 : "Ifrit/Shiva", + 440 : "Cranes", + 441 : "Number 024", + 442 : "Number 128", + 449 : "FlameEater", + 450 : "AtmaWeapon", + 451 : "Nerapa", + 459 : "Air Force", + 473 : "Ultros 1", + 474 : "Ultros 2", + 477 : "Ultros/Chupon", + 505 : "Kefka (Narshe)", +} + +wor_formation_name = { + 354 : "MagiMaster", + 422 : "Phunbaba 3", + 423 : "Phunbaba 4", + 444 : "Umaro", + 446 : "Guardian", + 452 : "SrBehemoth", + 454 : "Tentacles", + 455 : "Dullahan", + 456 : "Chadarnook", + 460 : "Stooges", + 462 : "Wrexsoul", + 463 : "Doom Gaze", + 464 : "Hidon", + 468 : "Doom", + 469 : "Goddess", + 470 : "Poltrgeist", + 482 : "Atma", + 484 : "Inferno", + 487 : "Tritoch", +} + normal_enemy_name = { 100 : "Marshal", 340 : "Piranha", diff --git a/data/enemies.py b/data/enemies.py index 32ae3199..cbee9025 100644 --- a/data/enemies.py +++ b/data/enemies.py @@ -275,6 +275,71 @@ def shuffle_encounters(self, maps): # NOTE: any remaining formations (due to extra_formations) are lost + def world_shuffle_encounters(self, maps): + import collections + # find all packs that are randomly encountered in zones + packs = collections.OrderedDict() + for zone in self.zones.zones: + if self.skip_shuffling_zone(maps, zone): + continue + + for x in range(zone.PACK_COUNT): + if self.skip_shuffling_pack(zone.packs[x], zone.encounter_rates[x]): + continue + + packs[self.packs.packs[zone.packs[x]]] = None + + # find all formations that are randomly encountered in packs + wob_formations = [] + wor_formations = [] + wob_packs = [] + wor_packs = [] + for pack in packs: + #check if this pack has wob or wor formations and stash the formations and pack in the apporiate lists + is_wob = self.formations.is_wob(pack.formations[0]) + if is_wob: + wob_packs.append(pack) + else: + wor_packs.append(pack) + for y in range(pack.FORMATION_COUNT): + if self.skip_shuffling_formation(pack.formations[y]): + continue + + if pack.extra_formations[y]: + # pack has extra formations (i.e. each formation is randomized with the subsequent 3 formations) + # unfortunately, this means there are more formations than packs to put them in, so some formations are lost + for x in range(4): + if is_wob: + wob_formations.append(pack.formations[y] + x) + else: + wor_formations.append(pack.formations[y] + x) + else: + if is_wob: + wob_formations.append(pack.formations[y]) + else: + wor_formations.append(pack.formations[y]) + + # shuffle the randomly encounterable formations + import random + random.shuffle(wob_formations) + random.shuffle(wor_formations) + + for pack in wob_packs: + for y in range(pack.FORMATION_COUNT): + if self.skip_shuffling_formation(pack.formations[y]): + continue + + pack.formations[y] = wob_formations.pop() + + for pack in wor_packs: + for y in range(pack.FORMATION_COUNT): + if self.skip_shuffling_formation(pack.formations[y]): + continue + + pack.formations[y] = wor_formations.pop() + + # NOTE: any remaining formations (due to extra_formations) are lost + def chupon_encounters(self, maps): # find all packs that are randomly encountered in zones packs = [] @@ -306,6 +371,28 @@ def randomize_encounters(self, maps): self.packs.randomize_packs(packs, boss_percent) + def randomize_encounters_by_world(self, maps): + # find all packs that are randomly encountered in zones + wob_packs = [] + wor_packs = [] + boss_percent = self.args.random_encounters_world_random / 100.0 + for zone in self.zones.zones: + if self.skip_shuffling_zone(maps, zone): + continue + + for x in range(zone.PACK_COUNT): + if self.skip_shuffling_pack(zone.packs[x], zone.encounter_rates[x]): + continue + pack = zone.packs[x] + is_wob = self.formations.is_wob(self.packs.packs[pack].formations[0]) + if is_wob: + wob_packs.append(pack) + else: + wor_packs.append(pack) + + self.packs.randomize_wob_packs(wob_packs, boss_percent) + self.packs.randomize_wor_packs(wor_packs, boss_percent) + def randomize_loot(self): for enemy in self.enemies: self.set_common_steal(enemy.id, self.items.get_random()) @@ -390,6 +477,10 @@ def mod(self, maps): self.shuffle_encounters(maps) elif self.args.random_encounters_chupon: self.chupon_encounters(maps) + elif self.args.random_encounters_world_shuffle: + self.world_shuffle_encounters(maps) + elif self.args.random_encounters_world_random is not None: + self.randomize_encounters_by_world(maps) elif not self.args.random_encounters_original: self.randomize_encounters(maps) diff --git a/data/enemy_formations.py b/data/enemy_formations.py index 8c672e8b..5adf7ad6 100644 --- a/data/enemy_formations.py +++ b/data/enemy_formations.py @@ -11,6 +11,8 @@ class EnemyFormations(): ENEMIES_END = 0xf83bf ENEMIES_SIZE = 15 + WOR_ENEMIES_START_INDEX = 192 + PHUNBABA3 = bosses.name_formation["Phunbaba 3"] DOOM_GAZE = bosses.name_formation["Doom Gaze"] ALL_STATUES = list(bosses.statue_formation_name) @@ -29,6 +31,8 @@ def __init__(self, rom, args, enemies): self.dragons = list(bosses.dragon_formation_name) self.bosses = list(bosses.normal_formation_name) + self.wob_bosses = list(bosses.wob_formation_name) + self.wor_bosses = list(bosses.wor_formation_name) # formations not to include in "normal" (i.e. non-boss/dragon) pool self.non_normal = [*self.dragons, *self.bosses, 4, 40, 42, 43, 59, 60, 63, 252, 335, @@ -36,15 +40,28 @@ def __init__(self, rom, args, enemies): *range(467, 472), *range(473, 476), 477, 481, 482, 484, 485, *range(487, 576)] non_normal_set = set(self.non_normal) + # the method for forming this list and the next was flawed/how it would be used wasn't understood. + # revisit and redefine these in a simpilier way. + self.wob = [*range(0, 103), *range(104, 114), *range(115, 126), 135, 136, + *range(139, 171), *range(174, 192), 210, *range(356, 362), + *range(373, 384), *range(402, 409), *range(410,412), *range(416,420), 434, 479] + self.wob_set = set(self.wob) + self.normal = [] self.formations = [] self.formation_names = [] + self.wob_normal = [] + self.wor_normal = [] for formation_index in range(len(self.flags_data)): formation = EnemyFormation(formation_index, self.flags_data[formation_index], self.enemies_data[formation_index]) self.formations.append(formation) if formation_index not in non_normal_set: self.normal.append(formation_index) + if formation_index in self.wob_set: + self.wob_normal.append(formation_index) + else: + self.wor_normal.append(formation_index) # name the formation based on enemies and their counts enemy_count = {} @@ -88,10 +105,21 @@ def has_enemy(self, formation_id, enemy_id): return True return False + def is_wob(self, formation_id): + return formation_id in self.wob_set + def get_random_normal(self): import random return random.choice(self.normal) + def get_random_wob_normal(self): + import random + return random.choice(self.wob_normal) + + def get_random_wor_normal(self): + import random + return random.choice(self.wor_normal) + def get_random_boss(self, exclude = None): import random if exclude is None: @@ -100,6 +128,22 @@ def get_random_boss(self, exclude = None): possible_bosses = [boss_id for boss_id in self.bosses if boss_id not in exclude] return random.choice(possible_bosses) + def get_random_wob_boss(self, exclude = None): + import random + if exclude is None: + return random.choice(self.wob_bosses) + + possible_bosses = [boss_id for boss_id in self.wob_bosses if boss_id not in exclude] + return random.choice(possible_bosses) + + def get_random_wor_boss(self, exclude = None): + import random + if exclude is None: + return random.choice(self.wor_bosses) + + possible_bosses = [boss_id for boss_id in self.wor_bosses if boss_id not in exclude] + return random.choice(possible_bosses) + def get_random_dragon(self): import random return random.choice(self.dragons) diff --git a/data/enemy_packs.py b/data/enemy_packs.py index dae38805..8bb76607 100644 --- a/data/enemy_packs.py +++ b/data/enemy_packs.py @@ -252,6 +252,68 @@ def randomize_packs(self, packs, boss_percent, no_phunbaba3 = False): # TODO: update get_random_normal to use more of the otherwise unused Fixed encounters self.packs[pack_id].formations[formation_index] = formation + def randomize_wob_packs(self, packs, boss_percent, no_phunbaba3 = False): + exclude_bosses = [] + if no_phunbaba3 or not self.args.shuffle_random_phunbaba3: + exclude_bosses += [self.formations.PHUNBABA3] + if not self.args.doom_gaze_no_escape: + exclude_bosses += [self.formations.DOOM_GAZE] + + # We only want statues and dragons to show up when they are intentionally + # mixed into the general boss pool + # Statues are currently seen as normal bosses in regards to scaling, + # but the long-term goal is to add their own scaling option so it + # makes most sense to begin treating these similarly to dragons. + if self.args.statue_boss_location != bosses.BossLocations.MIX: + exclude_bosses += self.formations.ALL_STATUES + + # This is more futureproofing in the event we consolidate dragons in the future + if self.args.dragon_boss_location != bosses.BossLocations.MIX: + exclude_bosses += self.formations.ALL_DRAGONS + + import random + for pack_id in packs: + if random.random() < boss_percent: + formation = self.formations.get_random_wob_boss(exclude_bosses) # outside of the below for loop, this ensures that there's no variability within fixed encounters within the same seed + for formation_index in range(self.packs[pack_id].FORMATION_COUNT): + self.packs[pack_id].formations[formation_index] = formation + else: + formation = self.formations.get_random_wob_normal() # outside of the below for loop, this ensures that there's no variability within fixed encounters within the same seed + for formation_index in range(self.packs[pack_id].FORMATION_COUNT): + # TODO: update get_random_normal to use more of the otherwise unused Fixed encounters + self.packs[pack_id].formations[formation_index] = formation + + def randomize_wor_packs(self, packs, boss_percent, no_phunbaba3 = False): + exclude_bosses = [] + if no_phunbaba3 or not self.args.shuffle_random_phunbaba3: + exclude_bosses += [self.formations.PHUNBABA3] + if not self.args.doom_gaze_no_escape: + exclude_bosses += [self.formations.DOOM_GAZE] + + # We only want statues and dragons to show up when they are intentionally + # mixed into the general boss pool + # Statues are currently seen as normal bosses in regards to scaling, + # but the long-term goal is to add their own scaling option so it + # makes most sense to begin treating these similarly to dragons. + if self.args.statue_boss_location != bosses.BossLocations.MIX: + exclude_bosses += self.formations.ALL_STATUES + + # This is more futureproofing in the event we consolidate dragons in the future + if self.args.dragon_boss_location != bosses.BossLocations.MIX: + exclude_bosses += self.formations.ALL_DRAGONS + + import random + for pack_id in packs: + if random.random() < boss_percent: + formation = self.formations.get_random_wor_boss(exclude_bosses) # outside of the below for loop, this ensures that there's no variability within fixed encounters within the same seed + for formation_index in range(self.packs[pack_id].FORMATION_COUNT): + self.packs[pack_id].formations[formation_index] = formation + else: + formation = self.formations.get_random_wor_normal() # outside of the below for loop, this ensures that there's no variability within fixed encounters within the same seed + for formation_index in range(self.packs[pack_id].FORMATION_COUNT): + # TODO: update get_random_normal to use more of the otherwise unused Fixed encounters + self.packs[pack_id].formations[formation_index] = formation + def chupon_packs(self, packs): # Replace all packs with the CHUPON formation for pack_id in packs: @@ -304,6 +366,54 @@ def randomize_fixed(self): # same issue as replacing number 128 with phunbaba3 (removed party member reappears in party after mine cart ride) self.randomize_packs(mine_cart, boss_percent, no_phunbaba3 = True) + def randomize_fixed_by_world(self): + # TODO: assign each check enough unused "packs" to eliminate variability within the same seed + lete_river = [263, 264] # nautiloid, exocite, pterodon + imperial_camp = [272, 298, 300, 269, 270] # soldier, dogs, templar/soldier, final 3 battles + doma_wob = [299] # soldier + phantom_train = [303] # ghost (siegfried [365] unrandomized for style) + serpent_trench = [275, 276, 277, 410, 411, 412, 413] # anguiform, actaneon, aspik, unused, unused, unused, unused + narshe_battle = [278, 279, 280] # brown and green soldiers, rider + opera_house = [281, 414] # sewer rat/vermin, unused + vector = [257, 285, 284] # guards, garm, commando, protoarmor, pipsqueak + mine_cart = [297, 400] # mag roaders + imperial_base = [295, 296] # soldier and magitek + sealed_cave = [405] # ninja + burning_house = [301, 287, 415] # balloon (x4, x3), unused + iaf = [382, 416] # sky armor / spit fire, unused + floating_continent_escape = [397, 398, 399] # naughty + owzer_mansion = [402, 403, 407, 404] # dahling, nightshade, souldancer, still life + moogle_defense = [261] # vomammoth + + self.fixed = lete_river + imperial_camp + doma_wob + phantom_train + serpent_trench + narshe_battle + opera_house + vector + self.fixed += mine_cart + imperial_base + sealed_cave + burning_house + iaf + floating_continent_escape + moogle_defense + + boss_percent = self.args.fixed_encounters_world_random / 100.0 + + # fixed packs which are capable of handling bababreath + wob_phunbaba3_safe = imperial_camp + doma_wob + phantom_train + vector + imperial_base + sealed_cave + wob_phunbaba3_safe += burning_house + iaf + floating_continent_escape + owzer_mansion + wor_phunbaba3_safe = owzer_mansion + self.randomize_wob_packs(wob_phunbaba3_safe, boss_percent) + self.randomize_wor_packs(wor_phunbaba3_safe, boss_percent) + + # for some reason, losing the party leader here makes the raft move very slowly after the battle + self.randomize_wob_packs(lete_river, boss_percent, no_phunbaba3 = True) + + # bababreath on party leader before the caves causes party to be invisible on entry + # it also happens if first fight phunbaba3 and then another non-phunbaba3 battle before the cave + self.randomize_wob_packs(serpent_trench, boss_percent, no_phunbaba3 = True) + + # special event instead of game over (move to save point and try again) + self.randomize_wob_packs(narshe_battle, boss_percent, no_phunbaba3 = True) + self.randomize_wob_packs(moogle_defense, boss_percent, no_phunbaba3 = True) + + # special game over event does not refresh objects/party leader + self.randomize_wob_packs(opera_house, boss_percent, no_phunbaba3 = True) + + # same issue as replacing number 128 with phunbaba3 (removed party member reappears in party after mine cart ride) + self.randomize_wob_packs(mine_cart, boss_percent, no_phunbaba3 = True) + def _update_names(self): # generate names based on formations and enemies self.pack_names = [] @@ -383,8 +493,10 @@ def mod(self): self.pad_enemy_packs() # keep this before randomized_fixed, as this pads with normal enemies, whereas that may add bosses - if not self.args.fixed_encounters_original: + if self.args.fixed_encounters_random is not None: self.randomize_fixed() + elif self.args.fixed_encounters_world_random is not None: + self.randomize_fixed_by_world() if not self.args.random_encounters_original: # if shuffled/randomized encounters, need to remove extra formations from floating continent