diff --git a/args/chests.py b/args/chests.py index a366c74e..719f9010 100644 --- a/args/chests.py +++ b/args/chests.py @@ -14,6 +14,9 @@ def parse(parser): help = "Chest contents randomized by tier. Probability of higher tiers begins low and increases as more chests are opened") chests_contents.add_argument("-cce", "--chest-contents-empty", action = "store_true", help = "Chest contents empty") + chests_contents.add_argument("-ccswr", "--chest-contents-shuffle-by-world-random", default = None, type = int, + metavar = "PERCENT", choices = range(101), + help = "Chest contents shuffled within each world and given percent randomized") chests.add_argument("-chrm", "--chest-random-monsters", default = [0, 0], type = int, nargs = 2, metavar = ("ENEMY", "BOSS"), choices = range(101), @@ -29,6 +32,10 @@ def process(args): if args.chest_random_monsters: args.chest_random_monsters_enemy = args.chest_random_monsters[0] args.chest_random_monsters_boss = args.chest_random_monsters[1] + if args.chest_contents_shuffle_by_world_random is not None: + args.chest_contents_shuffle_random_percent = args.chest_contents_shuffle_by_world_random + args.chest_contents_shuffle_by_world_random = True + def flags(args): flags = "" @@ -41,6 +48,8 @@ def flags(args): flags += " -ccrs" elif args.chest_contents_empty: flags += " -cce" + elif args.chest_contents_shuffle_by_world_random: + flags += f" -ccswr {args.chest_contents_shuffle_random_percent}" if args.chest_random_monsters: flags += f" -chrm {args.chest_random_monsters_enemy} {args.chest_random_monsters_boss}" @@ -62,11 +71,15 @@ def options(args): contents_value = "Random Scaled" elif args.chest_contents_empty: contents_value = "Empty" + elif args.chest_contents_shuffle_by_world_random: + contents_value = "Shuffle by World + Random" result.append(("Contents", contents_value, "contents_value")) if args.chest_contents_shuffle_random: result.append(("Random Percent", f"{args.chest_contents_shuffle_random_percent}%", "chest_contents_shuffle_random_percent")) - + elif args.chest_contents_shuffle_by_world_random: + result.append(("Random Percent", f"{args.chest_contents_shuffle_random_percent}%", "chest_contents_shuffle_random_percent")) + if args.chest_random_monsters: result.append(("MIAB Percent", f"{args.chest_random_monsters_enemy}%", "chest_random_monsters_enemy")) result.append((" Boss Percent", f"{args.chest_random_monsters_boss}%", "chest_random_monsters_boss")) @@ -81,6 +94,9 @@ def menu(args): if args.chest_contents_shuffle_random: entries[0] = ("Shuffle + Random", entries[1][1]) # put percent on same line del entries[1] # delete random percent line + elif args.chest_contents_shuffle_by_world_random: + entries[0] = ("WShuffle + Random", entries[1][1]) # put percent on same line + del entries[1] # delete random percent line else: entries[0] = (entries[0][1], "") diff --git a/args/shops.py b/args/shops.py index 779a1e49..ce755134 100644 --- a/args/shops.py +++ b/args/shops.py @@ -12,6 +12,9 @@ def parse(parser): help = "Shop inventories randomized based on type and tier. All weapon shops randomized, all armor shops, etc...") shops_inventory.add_argument("-sie", "--shop-inventory-empty", action = "store_true", help = "Shop inventories empty") + shops_inventory.add_argument("-siswr", "--shop-inventory-shuffle-world-random", + default = None, type = int, metavar = "PERCENT", choices = range(101), + help = "Shop inventories randomized based on type by world. All weapon shops randomized, all armor shops, etc...") shops_prices = shops.add_mutually_exclusive_group() shops_prices.add_argument("-sprv", "--shop-prices-random-value", default = None, type = int, @@ -58,6 +61,10 @@ def process(args): args.shop_inventory_shuffle_random_percent = args.shop_inventory_shuffle_random args.shop_inventory_shuffle_random = True + if args.shop_inventory_shuffle_world_random is not None: + args.shop_inventory_shuffle_random_percent = args.shop_inventory_shuffle_world_random + args.shop_inventory_shuffle_world_random = True + args._process_min_max("shop_prices_random_value") args._process_min_max("shop_prices_random_percent") @@ -70,6 +77,8 @@ def flags(args): flags += " -sirt" elif args.shop_inventory_empty: flags += " -sie" + elif args.shop_inventory_shuffle_world_random: + flags += f" -siswr {args.shop_inventory_shuffle_random_percent}" if args.shop_prices_random_value: flags += f" -sprv {args.shop_prices_random_value_min} {args.shop_prices_random_value_max}" @@ -116,6 +125,8 @@ def options(args): inventory = "Random Tiered" elif args.shop_inventory_empty: inventory = "Empty" + elif args.shop_inventory_shuffle_world_random: + inventory = "Shuffle by World + Random" price = "Original" if args.shop_prices_random_value: @@ -146,6 +157,8 @@ def options(args): result = [("Inventory", inventory, "shops_inventory")] if args.shop_inventory_shuffle_random: result.append(("Random Percent", f"{args.shop_inventory_shuffle_random_percent}%", "shops_random_percent")) + elif args.shop_inventory_shuffle_world_random: + result.append(("Random Percent", f"{args.shop_inventory_shuffle_random_percent}%", "shops_random_percent")) result.extend([ ("Price", price, "price"), @@ -167,6 +180,9 @@ def menu(args): if args.shop_inventory_shuffle_random: entries[0] = ("Shuffle + Random", entries[1][1]) # put percent on same line del entries[1] # delete random percent line + elif args.shop_inventory_shuffle_world_random: + entries[0] = ("WShuffle + Random", entries[1][1]) # put percent on same line + del entries[1] # delete random percent line else: entries[0] = (entries[0][1], "") diff --git a/data/chests.py b/data/chests.py index ba1a8bdb..5ad268d4 100644 --- a/data/chests.py +++ b/data/chests.py @@ -92,6 +92,9 @@ def shuffle_random(self): # first shuffle the chests to mix up empty/item/gold positions self.shuffle(randomizable_types) + self.random_chests(randomizable_types) + + def random_chests(self, randomizable_types): if self.args.chest_contents_shuffle_random_percent == 0: return @@ -194,6 +197,91 @@ def random_scaled(self): chests_asm.scale_gold(gold_bits, self.gold_contents) + def shuffle_indices(self, types, indices): + import copy + chests_shuffle = list() + for index in indices: + chest = copy.deepcopy(self.all_chests[index]) + if chest.type in types: + chests_shuffle.append(chest) + random.shuffle(chests_shuffle) + + shuffle_index = 0 + for index in indices: + chest = self.all_chests[index] + if chest.type in types: + shuffled_chest = chests_shuffle[shuffle_index] + shuffle_index += 1 + + chest.type = shuffled_chest.type + chest.contents = shuffled_chest.contents + + def shuffle_by_world(self, types): + from data.area_chests import area_chests + + # shuffle WoB and shared chests + wob_chests = list(area_chests["Narshe School"]) + wob_chests += list(area_chests["Narshe Inside WOB"]) + wob_chests += list(area_chests["Narshe Mines WOB"]) + wob_chests += list(area_chests["Figaro Castle"]) + wob_chests += list(area_chests["South Figaro Cave WOB"]) + wob_chests += list(area_chests["South Figaro Outside WOB"]) + wob_chests += list(area_chests["South Figaro Inside/Basement"]) + wob_chests += list(area_chests["Duncan's House WOB"]) + wob_chests += list(area_chests["Mt. Kolts"]) + wob_chests += list(area_chests["Returner's Hideout"]) + wob_chests += list(area_chests["Imperial Camp"]) + wob_chests += list(area_chests["Doma"]) + wob_chests += list(area_chests["Phantom Train"]) + wob_chests += list(area_chests["Mobliz Inside"]) + wob_chests += list(area_chests["Serpent Trench"]) + wob_chests += list(area_chests["Nikeah"]) + wob_chests += list(area_chests["Kohlingen"]) + wob_chests += list(area_chests["Coliseum Owner's House WOB"]) + wob_chests += list(area_chests["Zozo"]) + wob_chests += list(area_chests["Owzer's Mansion"]) + wob_chests += list(area_chests["Albrook Outside"]) + wob_chests += list(area_chests["Albrook Inside"]) + wob_chests += list(area_chests["Albrook Dock"]) + wob_chests += list(area_chests["Maranda"]) + wob_chests += list(area_chests["Magitek Factory"]) + wob_chests += list(area_chests["Thamasa Outside"]) + wob_chests += list(area_chests["Thamasa Strago's House"]) + wob_chests += list(area_chests["Thamasa Burning House"]) + wob_chests += list(area_chests["Esper Mountain"]) + wob_chests += list(area_chests["Imperial Base"]) + wob_chests += list(area_chests["Cave To Sealed Gate"]) + wob_chests += list(area_chests["Floating Continent"]) + self.shuffle_indices(types, wob_chests) + + # shuffle WoR + wor_chests = list(area_chests["Narshe Mines WOR"]) + wor_chests += list(area_chests["Figaro Castle Basement"]) + wor_chests += list(area_chests["South Figaro Cave WOR"]) + wor_chests += list(area_chests["South Figaro Outside WOR"]) + wor_chests += list(area_chests["Cyan's Dream Phantom Train"]) + wor_chests += list(area_chests["Mobliz Bookshelf Room WOR"]) + wor_chests += list(area_chests["Mobliz Outside WOR"]) + wor_chests += list(area_chests["Mt. Zozo"]) + wor_chests += list(area_chests["Owzer's Basement"]) + wor_chests += list(area_chests["Tzen Collapsing House"]) + wor_chests += list(area_chests["Daryl's Tomb"]) + wor_chests += list(area_chests["Veldt Cave WOR"]) + wor_chests += list(area_chests["Ancient Cave"]) + wor_chests += list(area_chests["Phoenix Cave"]) + wor_chests += list(area_chests["Fanatic's Tower"]) + wor_chests += list(area_chests["Zone Eater"]) + wor_chests += list(area_chests["Umaro's Cave"]) + wor_chests += list(area_chests["Kefka's Tower"]) + self.shuffle_indices(types, wor_chests) + + def shuffle_by_world_random(self): + randomizable_types = [Chest.EMPTY, Chest.ITEM, Chest.GOLD] + + # first shuffle the chests to mix up empty/item/gold positions + self.shuffle_by_world(randomizable_types) + self.random_chests(randomizable_types) + def chest_random_monsters(self, enemy_percent, boss_percent): from data.enemy_battle_groups import event_battle_groups_to_avoid, boss_event_battle_groups, event_battle_group_name, dragon_event_battle_groups, name_event_battle_group MIAB_noboss = [a for a in range(256) if a not in event_battle_groups_to_avoid.keys() and a not in event_battle_group_name.keys()] @@ -303,11 +391,17 @@ def mod(self): self.random_scaled() elif self.args.chest_contents_empty: self.clear_contents() + elif self.args.chest_contents_shuffle_by_world_random: + self.shuffle_by_world_random() + self.remove_excluded_items() else: self.remove_excluded_items() if self.args.chest_monsters_shuffle: - self.shuffle([Chest.MONSTER]) + if self.args.chest_contents_shuffle_by_world_random: + self.shuffle_by_world([Chest.MONSTER]) + else: + self.shuffle([Chest.MONSTER]) # add randomized MIABs after other contents randomization/shuffle is complete if self.args.chest_random_monsters_enemy > 0: diff --git a/data/shops.py b/data/shops.py index a78d4c0b..a2279a71 100644 --- a/data/shops.py +++ b/data/shops.py @@ -52,7 +52,9 @@ def shuffle(self): Shop.RELIC : self.type_shops[Shop.RELIC], } type_items[Shop.ITEM].extend(type_items[Shop.VENDOR]) + self.shuffle_by_type(type_items, type_shops) + def shuffle_by_type(self, type_items, type_shops): import random import collections for shop_type in range(1, Shop.SHOP_TYPE_COUNT - 1): # skip EMPTY and VENDOR shop types @@ -114,6 +116,9 @@ def get_item(item_type, exclude = None): def shuffle_random(self): self.shuffle() + self.random() + + def random(self): if self.args.shop_inventory_shuffle_random_percent == 0: return @@ -138,6 +143,45 @@ def shuffle_random(self): return total_index += 1 + def shuffle_world_random(self): + self.shuffle_world() + self.random() + + def shuffle_world(self): + from itertools import chain + wob_shop_indicies = chain(range(5,39), range(40,48), [83], [85]) + wor_shop_indicies = chain(range(48,68), range(72,82), [84]) + + self.shuffle_indices(wob_shop_indicies) + self.shuffle_indices(wor_shop_indicies) + + def shuffle_indices(self, indices): + # shuffle shops at the specified indices (except empty ones) + # keeps weapons in weapon shops, armors in armor shops, items in item shops, etc... + + # to prevent duplicates, get list of items for each shop type and sort it by their frequency + # picking least frequent last prevents ending up with multiple of same item and only one shop to distribute them to + # randomly pick shops of the given type until find one without the item and add it + # once the shop has as many items as its shuffled count remove it from the available pool + shops_to_shuffle = list() + for shop_index in indices: + shops_to_shuffle.append(self.all_shops[shop_index]) + + type_items = {Shop.WEAPON : [], Shop.ARMOR : [], Shop.ITEM : [], Shop.RELIC : [], Shop.VENDOR : []} + for shop in shops_to_shuffle: + for item_index in range(shop.item_count): + type_items[shop.type].append(shop.items[item_index]) + + # shuffle vendor shops with item shops + # add vendor shops to list of item shops and vendor shop inventories to list of items in item shops + type_shops = {Shop.WEAPON : [], Shop.ARMOR : [], Shop.ITEM : [], Shop.RELIC : [], Shop.VENDOR : []} + for shop in shops_to_shuffle: + # exclude shops that are inaccesible from shops and type_shops lists + if shop.type != Shop.EMPTY and shop.accessible(): + type_shops[shop.type].append(shop) + type_items[Shop.ITEM].extend(type_items[Shop.VENDOR]) + self.shuffle_by_type(type_items, type_shops) + def clear_inventories(self): for shop in self.shops: shop.clear() @@ -267,6 +311,8 @@ def mod(self): self.random_tiered() elif self.args.shop_inventory_empty: self.clear_inventories() + elif self.args.shop_inventory_shuffle_world_random: + self.shuffle_world_random() self.assign_dried_meats() self.remove_excluded_items()