Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion args/chests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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 = ""
Expand All @@ -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}"
Expand All @@ -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"))
Expand All @@ -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], "")

Expand Down
16 changes: 16 additions & 0 deletions args/shops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")

Expand All @@ -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}"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"),
Expand All @@ -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], "")

Expand Down
96 changes: 95 additions & 1 deletion data/chests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Comment on lines +200 to +206

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The indices list contains raw chest IDs from area_chests, which includes special chests (such as the Lone Wolf chest and the Gem Box chest) and duplicate chests that are explicitly excluded from the randomizer pool (self.chests). Shuffling these can break game logic or duplicate/lose items. We should filter indices to only include chest IDs present in self.chests.

Suggested change
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)
def shuffle_indices(self, types, indices):
import copy
valid_chest_ids = {chest.id for chest in self.chests}
indices = [index for index in indices if index in valid_chest_ids]
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()]
Expand Down Expand Up @@ -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:
Expand Down
46 changes: 46 additions & 0 deletions data/shops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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)
Comment on lines +166 to +183

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The indices list contains raw shop indices, some of which might be empty or inaccessible. If we collect items from all shops in indices (including empty/inaccessible ones) but only collect the shops themselves into type_shops if they are accessible and non-empty, we will have a mismatch between the number of items and the total capacity of the shops. This will cause shuffle_by_type to raise an IndexError when shop_indices becomes empty before all items are distributed, or a KeyError if shop.type is Shop.EMPTY. We should filter shops_to_shuffle to only include accessible and non-empty shops right at the start.

Suggested change
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)
shops_to_shuffle = list()
for shop_index in indices:
shop = self.all_shops[shop_index]
if shop.type != Shop.EMPTY and shop.accessible():
shops_to_shuffle.append(shop)
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:
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()
Expand Down Expand Up @@ -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()
Expand Down