From f895db558ca7b778d46cc463194559817893da68 Mon Sep 17 00:00:00 2001 From: UmedMuzl Date: Mon, 9 Feb 2026 20:34:41 -0600 Subject: [PATCH 1/4] Price Tweaks --- __init__.py | 273 +---------------------------------------- archipelago/Prices.py | 217 ++++++++++++++++++++++++++++++++ archipelago/Regions.py | 7 +- 3 files changed, 224 insertions(+), 273 deletions(-) create mode 100644 archipelago/Prices.py diff --git a/__init__.py b/__init__.py index e1247c6f8..4de91e0a0 100644 --- a/__init__.py +++ b/__init__.py @@ -184,6 +184,7 @@ def copy_dependencies(zip_path, file): from worlds.LauncherComponents import Component, SuffixIdentifier, components, Type, icon_paths import randomizer.ShuffleExits as ShuffleExits from archipelago.FillSettings import fillsettings + from archipelago.Prices import PriceGenerator from Utils import open_filename import shutil import zlib @@ -687,273 +688,6 @@ def stage_assert_generate(cls, multiworld: MultiWorld): raise FileNotFoundError("Invalid DK64 ROM file, please make sure your ROM is a vanilla DK64 file in big endian.") check_version() - def _get_slot_data(self): - """Get the slot data.""" - return { - # "death_link": self.options.death_link.value, - } - - def _get_shared_shop_vendors(self, Kongs, Types): - """Identify vendor/level combinations that have shared shops.""" - from randomizer.Lists.Location import SharedShopLocations - - shared_shop_vendors = set() - - if not self.options.enable_shared_shops.value: - if not hasattr(self.spoiler.settings, "selected_shared_shops"): - self.spoiler.settings.selected_shared_shops = set() - return shared_shop_vendors, set() - - # Get or create the set of available shared shops - if hasattr(self.spoiler.settings, "selected_shared_shops") and self.spoiler.settings.selected_shared_shops: - available_shared_shops = self.spoiler.settings.selected_shared_shops - else: - all_shared_shops = list(SharedShopLocations) - self.random.shuffle(all_shared_shops) - available_shared_shops = set(all_shared_shops[:10]) - self.spoiler.settings.selected_shared_shops = available_shared_shops - - # Build set of vendor/level combinations - for location_id, location in self.spoiler.LocationList.items(): - if location.type == Types.Shop and location.kong == Kongs.any: - if location_id in available_shared_shops: - shared_shop_vendors.add((location.level, location.vendor)) - - return shared_shop_vendors, available_shared_shops - - def _categorize_shop_locations(self, shared_shop_vendors, available_shared_shops, Kongs, Types): - """Categorize shops into included and excluded based on settings.""" - shop_locations = [] - excluded_shop_locations = [] - shops_per_kong = {kong: 0 for kong in Kongs} - - for location_id, location in self.spoiler.LocationList.items(): - if location.type != Types.Shop: - continue - - # Check if shop is excluded by smaller_shops setting - if hasattr(location, "smallerShopsInaccessible") and location.smallerShopsInaccessible and self.options.smaller_shops.value: - excluded_shop_locations.append(location_id) - continue - - # Check if shared shop is excluded - if location.kong == Kongs.any: - if not self.options.enable_shared_shops.value or location_id not in available_shared_shops: - excluded_shop_locations.append(location_id) - continue - - # Check if kong shop is blocked by a shared shop at same vendor/level - if location.kong != Kongs.any and self.options.enable_shared_shops.value: - if (location.level, location.vendor) in shared_shop_vendors: - excluded_shop_locations.append(location_id) - continue - - # Shop is included - shop_locations.append(location_id) - if location.kong != Kongs.any: - shops_per_kong[location.kong] += 1 - - return shop_locations, excluded_shop_locations, shops_per_kong - - def _calculate_kong_averages(self, shops_per_kong, max_coins, percentage, min_max_coins, Kongs): - """Calculate average price per shop for each kong.""" - avg_prices_per_kong = {} - - for kong in Kongs: - if kong == Kongs.any or shops_per_kong[kong] == 0: - continue - - # Calculate budget based on max coins available to this kong - kong_budget = max_coins[kong] * percentage - - # Use higher safety margins: 80% for easy/medium, 95% for hard mode - safety_margin = 0.80 if percentage < 0.85 else 0.95 - target = max(1, kong_budget * safety_margin) - avg_prices_per_kong[kong] = target / shops_per_kong[kong] - - return avg_prices_per_kong - - def _generate_individual_prices(self, shop_locations, avg_prices_per_kong, progressive_avg_price, progressive_stddev, shopprices, DK64RItems, Kongs): - """Generate random individual prices for shops and progressive items.""" - individual_prices = {} - - # Generate shop prices - for location_id in shop_locations: - location = self.spoiler.LocationList[location_id] - - if shopprices == 0: - individual_prices[location_id] = 0 - continue - - # Determine average and stddev for this shop - if location.kong == Kongs.any: - kong_avg = progressive_avg_price - kong_stddev = progressive_stddev - else: - kong_avg = avg_prices_per_kong.get(location.kong, progressive_avg_price) - kong_stddev = kong_avg * 0.3 - - price = round(self.random.normalvariate(kong_avg, kong_stddev)) - price = max(1, min(price, int(kong_avg * 2))) - individual_prices[location_id] = price - - progressive_moves = { - DK64RItems.ProgressiveSlam: 3, - DK64RItems.ProgressiveAmmoBelt: 2, - DK64RItems.ProgressiveInstrumentUpgrade: 3, - } - - for item, count in progressive_moves.items(): - individual_prices[item] = [] - for _ in range(count): - if shopprices == 0: - individual_prices[item].append(0) - else: - price = round(self.random.normalvariate(progressive_avg_price, progressive_stddev)) - price = max(1, min(price, int(progressive_avg_price * 2))) - individual_prices[item].append(price) - - return individual_prices - - def _convert_to_cumulative_prices(self, individual_prices, shop_locations, max_cumulative_per_kong, Kongs): - """Convert individual prices to cumulative running totals per kong. - - This mimics the logic from determineFinalPriceAssortment: - - Progressive items add to all kongs' totals, price stored is average of all totals - - Kong-specific shops add to that kong's total only - - Cumulative prices are capped at max_cumulative_per_kong to ensure accessibility - """ - price_assignment = [] - - # Build list of price assignments - for key, value in individual_prices.items(): - if isinstance(value, list): - # Progressive move - add multiple entries - for price in value: - price_assignment.append({"is_prog": True, "cost": price, "item": key, "kong": Kongs.any}) - elif key in shop_locations: - # Shop location - location = self.spoiler.LocationList[key] - price_assignment.append({"is_prog": False, "cost": value, "item": key, "kong": location.kong}) - - # Shuffle and calculate cumulative prices - self.random.shuffle(price_assignment) - total_cost = [0] * 5 - cumulative_prices = {} - - for assignment in price_assignment: - kong = assignment["kong"] - written_price = assignment["cost"] - - if kong == Kongs.any: - # Progressive item - add to all kongs, price is average of current totals - current_kong_total = 0 - for kong_index in range(5): - current_kong_total += total_cost[kong_index] - total_cost[kong_index] += written_price - # Cap at maximum affordable amount - total_cost[kong_index] = min(total_cost[kong_index], max_cumulative_per_kong[kong_index]) - written_price = int(current_kong_total / 5) - else: - # Kong-specific shop - add to that kong's total - total_cost[kong] += written_price - # Cap at maximum affordable amount - total_cost[kong] = min(total_cost[kong], max_cumulative_per_kong[kong]) - written_price = total_cost[kong] - - # Store cumulative price - key = assignment["item"] - if assignment["is_prog"]: - if key not in cumulative_prices: - cumulative_prices[key] = [] - cumulative_prices[key].append(written_price) - else: - cumulative_prices[key] = written_price - - return cumulative_prices - - def _generate_archipelago_prices(self): - """Generate custom shop prices for Archipelago. - - Generates individual prices for each shop and progressive item, then converts them - to cumulative prices (running totals) per kong. Excluded shops are set to 0 cost. - """ - from randomizer.Enums.Items import Items as DK64RItems - from randomizer.Lists.Item import ItemList as DK64RItemList - from randomizer.Enums.Kongs import Kongs - - # Constants - MAX_COINS = { - Kongs.donkey: 179, - Kongs.diddy: 183, - Kongs.lanky: 190, - Kongs.tiny: 198, - Kongs.chunky: 224, - } - - PRICE_PERCENTAGES = { - 0: 0.0, # free - 1: 0.35, # easy - 2: 0.55, # medium - 3: 0.85, # hard - } - - # Get settings - shopprices = self.options.shop_prices.value - percentage = PRICE_PERCENTAGES[shopprices] - min_max_coins = min(MAX_COINS.values()) - # Cap cumulative prices at actual max coins (rainbow coins are tracked separately as collectibles) - max_cumulative_per_kong = {kong: MAX_COINS[kong] for kong in MAX_COINS} - - # Categorize shops - shared_shop_vendors, available_shared_shops = self._get_shared_shop_vendors(Kongs, Types) - shop_locations, excluded_shop_locations, shops_per_kong = self._categorize_shop_locations(shared_shop_vendors, available_shared_shops, Kongs, Types) - - # Calculate pricing averages - avg_prices_per_kong = self._calculate_kong_averages(shops_per_kong, MAX_COINS, percentage, min_max_coins, Kongs) - # Progressive items use 25% of budget, divided among 8 total progressive items - progressive_avg_price = (min_max_coins * percentage * 0.25) / 8 if shopprices > 0 else 0 - progressive_stddev = progressive_avg_price * 0.3 - - # Generate individual prices - individual_prices = self._generate_individual_prices(shop_locations, avg_prices_per_kong, progressive_avg_price, progressive_stddev, shopprices, DK64RItems, Kongs) - - # Add 0 prices for non-shop items and excluded shops - for item_id in DK64RItemList.keys(): - if item_id not in individual_prices: - individual_prices[item_id] = 0 - - for location_id in excluded_shop_locations: - individual_prices[location_id] = 0 - - # Store and finalize prices - self.spoiler.settings.original_prices = individual_prices.copy() - - if shopprices > 0: - # Convert to cumulative prices - cumulative_prices = self._convert_to_cumulative_prices(individual_prices, shop_locations, max_cumulative_per_kong, Kongs) - - # Add 0 prices for items not in shops - for item_id in DK64RItemList.keys(): - if item_id not in cumulative_prices: - cumulative_prices[item_id] = 0 - - # Add 0 prices for all location IDs not already priced - for location_id in self.spoiler.LocationList.keys(): - if location_id not in cumulative_prices: - cumulative_prices[location_id] = 0 - - for location_id in excluded_shop_locations: - cumulative_prices[location_id] = 0 - - self.spoiler.settings.prices = cumulative_prices - else: - # Free prices - ensure all locations exist with 0 cost - for location_id in self.spoiler.LocationList.keys(): - if location_id not in individual_prices: - individual_prices[location_id] = 0 - self.spoiler.settings.prices = individual_prices.copy() - def generate_early(self): """Generate the world.""" # Check host setting for minimal logic and force glitchless if disabled @@ -1224,7 +958,10 @@ def generate_early(self): else: self.spoiler.settings.selected_shared_shops = set() - self._generate_archipelago_prices() + # Generate custom shop prices for Archipelago + price_generator = PriceGenerator(self.spoiler, self.options, self.random) + price_generator.generate_prices() + # Handle Loading Zones - this will handle LO and (someday?) LZR appropriately if self.spoiler.settings.shuffle_loading_zones != ShuffleLoadingZones.none: if self.spoiler.settings.level_randomization != LevelRandomization.loadingzone: diff --git a/archipelago/Prices.py b/archipelago/Prices.py new file mode 100644 index 000000000..2daaf6145 --- /dev/null +++ b/archipelago/Prices.py @@ -0,0 +1,217 @@ +"""Shop price generation functionality for Archipelago DK64.""" + +from randomizer.Enums.Items import Items as DK64RItems +from randomizer.Lists.Item import ItemList as DK64RItemList +from randomizer.Enums.Kongs import Kongs +from randomizer.Enums.Types import Types +from randomizer.Lists.Location import SharedShopLocations + +class PriceGenerator: + """Generates custom shop prices for Archipelago DK64.""" + + # Progressive moves configuration (item: count) + PROGRESSIVE_MOVES = { + "ProgressiveSlam": 3, + "ProgressiveAmmoBelt": 2, + "ProgressiveInstrumentUpgrade": 3, + } + + def __init__(self, spoiler, options, random): + """Initialize the price generator.""" + self.spoiler = spoiler + self.options = options + self.random = random + + def _get_price_weights(self, shop_prices_value): + """Get the parameters for the price distribution.""" + match shop_prices_value: + case 3: # Hard + return (6.5, 3, 12) + case 2: # Medium + return (4.5, 2, 9) + case 1: # Easy + return (2.5, 1, 6) + case _: # Free + return (0, 0, 0) + + def _generate_random_price(self, avg, stddev, upper_limit): + """Generate a random price using normal distribution.""" + if avg == 0: # Free prices + return 0 + + price = round(self.random.normalvariate(avg, stddev)) + return max(1, min(price, upper_limit)) + + def _get_shared_shop_vendors(self, Kongs, Types): + """Identify vendor/level combinations that have shared shops.""" + + shared_shop_vendors = set() + + if not self.options.enable_shared_shops.value: + if not hasattr(self.spoiler.settings, "selected_shared_shops"): + self.spoiler.settings.selected_shared_shops = set() + return shared_shop_vendors, set() + + # Get or create the set of available shared shops + if hasattr(self.spoiler.settings, "selected_shared_shops") and self.spoiler.settings.selected_shared_shops: + available_shared_shops = self.spoiler.settings.selected_shared_shops + else: + all_shared_shops = list(SharedShopLocations) + self.random.shuffle(all_shared_shops) + available_shared_shops = set(all_shared_shops[:10]) + self.spoiler.settings.selected_shared_shops = available_shared_shops + + # Build set of vendor/level combinations + for location_id, location in self.spoiler.LocationList.items(): + if location.type == Types.Shop and location.kong == Kongs.any: + if location_id in available_shared_shops: + shared_shop_vendors.add((location.level, location.vendor)) + + return shared_shop_vendors, available_shared_shops + + def _categorize_shop_locations(self, shared_shop_vendors, available_shared_shops, Kongs, Types): + """Categorize shops into included and excluded based on settings.""" + shop_locations = [] + excluded_shop_locations = [] + shops_per_kong = {kong: 0 for kong in Kongs} + + for location_id, location in self.spoiler.LocationList.items(): + if location.type != Types.Shop: + continue + + # Check if shop is excluded by smaller_shops setting + if hasattr(location, "smallerShopsInaccessible") and location.smallerShopsInaccessible and self.options.smaller_shops.value: + excluded_shop_locations.append(location_id) + continue + + # Check if shared shop is excluded + if location.kong == Kongs.any: + if not self.options.enable_shared_shops.value or location_id not in available_shared_shops: + excluded_shop_locations.append(location_id) + continue + + # Check if kong shop is blocked by a shared shop at same vendor/level + if location.kong != Kongs.any and self.options.enable_shared_shops.value: + if (location.level, location.vendor) in shared_shop_vendors: + excluded_shop_locations.append(location_id) + continue + + # Shop is included + shop_locations.append(location_id) + if location.kong != Kongs.any: + shops_per_kong[location.kong] += 1 + + return shop_locations, excluded_shop_locations, shops_per_kong + + def _generate_individual_prices(self, shop_locations, avg, stddev, upper_limit, DK64RItems): + """Generate random individual prices for shops and progressive items.""" + individual_prices = {} + + # Generate shop prices - simple random price per shop + for location_id in shop_locations: + individual_prices[location_id] = self._generate_random_price(avg, stddev, upper_limit) + + # Progressive items get their own price list + for item_name, count in self.PROGRESSIVE_MOVES.items(): + item_enum = getattr(DK64RItems, item_name) + individual_prices[item_enum] = [] + for _ in range(count): + individual_prices[item_enum].append(self._generate_random_price(avg, stddev, upper_limit)) + + return individual_prices + + def _convert_to_cumulative_prices(self, individual_prices, shop_locations, Kongs): + """Convert individual prices to cumulative running totals per kong.""" + price_assignment = [] + + # Build list of price assignments + for key, value in individual_prices.items(): + if isinstance(value, list): + # Progressive move - add multiple entries + for price in value: + price_assignment.append({"is_prog": True, "cost": price, "item": key, "kong": Kongs.any}) + elif key in shop_locations: + # Shop location + location = self.spoiler.LocationList[key] + price_assignment.append({"is_prog": False, "cost": value, "item": key, "kong": location.kong}) + + # Shuffle and calculate cumulative prices + self.random.shuffle(price_assignment) + total_cost = [0] * 5 + cumulative_prices = {} + + for assignment in price_assignment: + kong = assignment["kong"] + written_price = assignment["cost"] + + if kong == Kongs.any: + # Progressive item - add to all kongs, price is average of current totals + current_kong_total = 0 + for kong_index in range(5): + current_kong_total += total_cost[kong_index] + total_cost[kong_index] += written_price + written_price = int(current_kong_total / 5) + else: + # Kong-specific shop - add to that kong's total + total_cost[kong] += written_price + written_price = total_cost[kong] + + # Store cumulative price + key = assignment["item"] + if assignment["is_prog"]: + if key not in cumulative_prices: + cumulative_prices[key] = [] + cumulative_prices[key].append(written_price) + else: + cumulative_prices[key] = written_price + + return cumulative_prices + + def generate_prices(self): + """Generate custom shop prices for Archipelago.""" + # Get price distribution parameters (matches standalone) + shopprices = self.options.shop_prices.value + avg, stddev, upper_limit = self._get_price_weights(shopprices) + + # Categorize shops + shared_shop_vendors, available_shared_shops = self._get_shared_shop_vendors(Kongs, Types) + shop_locations, excluded_shop_locations, shops_per_kong = self._categorize_shop_locations(shared_shop_vendors, available_shared_shops, Kongs, Types) + + # Generate individual prices using standalone algorithm + individual_prices = self._generate_individual_prices(shop_locations, avg, stddev, upper_limit, DK64RItems) + + # Add 0 prices for non-shop items and excluded shops + for item_id in DK64RItemList.keys(): + if item_id not in individual_prices: + individual_prices[item_id] = 0 + + for location_id in excluded_shop_locations: + individual_prices[location_id] = 0 + + # Store original prices + self.spoiler.settings.original_prices = individual_prices.copy() + + if shopprices > 0: + # Convert to cumulative prices + cumulative_prices = self._convert_to_cumulative_prices(individual_prices, shop_locations, Kongs) + + # Add 0 prices for items not in shops + for item_id in DK64RItemList.keys(): + if item_id not in cumulative_prices: + cumulative_prices[item_id] = 0 + + # Add 0 prices for all location IDs not already priced + for location_id in self.spoiler.LocationList.keys(): + if location_id not in cumulative_prices: + cumulative_prices[location_id] = 0 + + for location_id in excluded_shop_locations: + cumulative_prices[location_id] = 0 + + self.spoiler.settings.prices = cumulative_prices + else: + # Free prices - ensure all locations exist with 0 cost + for location_id in self.spoiler.LocationList.keys(): + if location_id not in individual_prices: + individual_prices[location_id] = 0 + self.spoiler.settings.prices = individual_prices.copy() diff --git a/archipelago/Regions.py b/archipelago/Regions.py index 24f9790e0..c77c461b3 100644 --- a/archipelago/Regions.py +++ b/archipelago/Regions.py @@ -323,15 +323,12 @@ def create_region( token_location.place_locked_item(DK64Item("Bonus Completed", ItemClassification.progression_skip_balancing, None, player)) new_region.locations.append(token_location) # Item placement limitations! These only apply to items in your own world, as other worlds' items will be AP items, and those can be anywhere. - # Fairy locations cannot have your own world's blueprints on them for technical reasons. - if location_obj.type == Types.Fairy: - add_item_rule(location, lambda item: not (item.player == player and "Blueprint" in item.name)) # Bosses and Crowns cannot have Junk due to technical reasons if location_obj.type in (Types.Key, Types.Crown): add_item_rule(location, lambda item: not (item.player == player and "Junk" in item.name)) - # Shops cannot have shopkeepers for the time being due to funny haha display bug + # Shops cannot have shopkeepers or Rainbow Coins due to technical issues if location_obj.type == Types.Shop: - add_item_rule(location, lambda item: not (item.player == player and item.name in ["Cranky", "Funky", "Candy", "Snide"])) + add_item_rule(location, lambda item: not (item.player == player and item.name in ["Cranky", "Funky", "Candy", "Snide", "Rainbow"])) if location_obj.type == Types.Key and logic_holder.settings.win_condition_item in (WinConditionComplex.req_bosses, WinConditionComplex.krools_challenge): token_location = DK64Location(player, location_obj.name + " Token", None, new_region) set_rule(token_location, lambda state, player=player, location_logic=location_logic: hasDK64RLocation(state, player, location_logic)) From b5a7becdcbdbbd5b670c13022321c29887efefd0 Mon Sep 17 00:00:00 2001 From: UmedMuzl Date: Mon, 9 Feb 2026 20:35:17 -0600 Subject: [PATCH 2/4] lint --- archipelago.json | 2 +- archipelago/Prices.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/archipelago.json b/archipelago.json index dba8d57d5..c4e9d7ef9 100644 --- a/archipelago.json +++ b/archipelago.json @@ -1,6 +1,6 @@ { "minimum_ap_version": "0.6.5", - "world_version": "1.4.44", + "world_version": "1.4.45", "authors": ["2dos", "AlmostSeagull", "Ballaam", "Green Bean", "Killklli", "Lrauq", "PoryGone", "Umed"], "version": 7, "compatible_version": 7, diff --git a/archipelago/Prices.py b/archipelago/Prices.py index 2daaf6145..a1d747789 100644 --- a/archipelago/Prices.py +++ b/archipelago/Prices.py @@ -6,6 +6,7 @@ from randomizer.Enums.Types import Types from randomizer.Lists.Location import SharedShopLocations + class PriceGenerator: """Generates custom shop prices for Archipelago DK64.""" From beed0fd70d07a2a95d904dfc687356cce9e57c66 Mon Sep 17 00:00:00 2001 From: UmedMuzl Date: Mon, 9 Feb 2026 20:50:18 -0600 Subject: [PATCH 3/4] Update Prices.py --- archipelago/Prices.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/archipelago/Prices.py b/archipelago/Prices.py index a1d747789..260a8a8fb 100644 --- a/archipelago/Prices.py +++ b/archipelago/Prices.py @@ -5,6 +5,7 @@ from randomizer.Enums.Kongs import Kongs from randomizer.Enums.Types import Types from randomizer.Lists.Location import SharedShopLocations +from archipelago.Options import ShopPrices class PriceGenerator: @@ -25,15 +26,14 @@ def __init__(self, spoiler, options, random): def _get_price_weights(self, shop_prices_value): """Get the parameters for the price distribution.""" - match shop_prices_value: - case 3: # Hard - return (6.5, 3, 12) - case 2: # Medium - return (4.5, 2, 9) - case 1: # Easy - return (2.5, 1, 6) - case _: # Free - return (0, 0, 0) + if shop_prices_value == ShopPrices.option_high: + return (6.5, 3, 12) + elif shop_prices_value == ShopPrices.option_medium: + return (4.5, 2, 9) + elif shop_prices_value == ShopPrices.option_low: + return (2.5, 1, 6) + elif shop_prices_value == ShopPrices.option_free: + return (0, 0, 0) def _generate_random_price(self, avg, stddev, upper_limit): """Generate a random price using normal distribution.""" @@ -45,7 +45,6 @@ def _generate_random_price(self, avg, stddev, upper_limit): def _get_shared_shop_vendors(self, Kongs, Types): """Identify vendor/level combinations that have shared shops.""" - shared_shop_vendors = set() if not self.options.enable_shared_shops.value: From 2c9d63dcd75bb55fb932e0cc0224115f46f8eafd Mon Sep 17 00:00:00 2001 From: UmedMuzl <124306572+UmedMuzl@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:18:48 -0600 Subject: [PATCH 4/4] Update Regions.py --- archipelago/Regions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/archipelago/Regions.py b/archipelago/Regions.py index c77c461b3..4ef7e0f78 100644 --- a/archipelago/Regions.py +++ b/archipelago/Regions.py @@ -326,6 +326,9 @@ def create_region( # Bosses and Crowns cannot have Junk due to technical reasons if location_obj.type in (Types.Key, Types.Crown): add_item_rule(location, lambda item: not (item.player == player and "Junk" in item.name)) + # Fairies cannot have blueprints due to crashes + if location_obj.type == Types.Fairy: + add_item_rule(location, lambda item: not (item.player == player and "Blueprint" in item.name)) # Shops cannot have shopkeepers or Rainbow Coins due to technical issues if location_obj.type == Types.Shop: add_item_rule(location, lambda item: not (item.player == player and item.name in ["Cranky", "Funky", "Candy", "Snide", "Rainbow"]))