From c7a57d63fa0cd2e27f342508c50d6ca0d8dc0863 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 15 Jan 2025 14:32:07 -0700 Subject: [PATCH] feat: customizer options for prices in shops and money balance tuning --- BaseClasses.py | 1 + CLI.py | 4 ++- Fill.py | 11 ++++++--- ItemList.py | 27 ++++++++++++++++---- Main.py | 2 ++ RELEASENOTES.md | 1 + resources/app/cli/args.json | 1 + source/classes/CustomSettings.py | 42 ++++++++++++++++++++++++++++++++ 8 files changed, 79 insertions(+), 10 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index ab332eb0..529eca39 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -153,6 +153,7 @@ def set_player_attr(attr, val): set_player_attr('trap_door_mode', 'optional') set_player_attr('key_logic_algorithm', 'partial') set_player_attr('aga_randomness', True) + set_player_attr('money_balance', 100) set_player_attr('shopsanity', False) set_player_attr('mixed_travel', 'prevent') diff --git a/CLI.py b/CLI.py index b2cc20e3..b3a5bdcf 100644 --- a/CLI.py +++ b/CLI.py @@ -142,7 +142,8 @@ def defval(value): 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', 'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', - 'trap_door_mode', 'key_logic_algorithm', 'door_self_loops', 'any_enemy_logic', 'aga_randomness']: + 'trap_door_mode', 'key_logic_algorithm', 'door_self_loops', 'any_enemy_logic', 'aga_randomness', + 'money_balance']: value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) if player == 1: setattr(ret, name, {1: value}) @@ -228,6 +229,7 @@ def parse_settings(): 'mixed_travel': 'prevent', 'standardize_palettes': 'standardize', 'aga_randomness': True, + 'money_balance': 100, "triforce_pool": 0, "triforce_goal": 0, diff --git a/Fill.py b/Fill.py index 83f921f4..79af1403 100644 --- a/Fill.py +++ b/Fill.py @@ -1020,14 +1020,16 @@ def kiki_required(state, location): solvent = set() insolvent = set() for player in range(1, world.players+1): - if wallet[player] >= sphere_costs[player] >= 0: + modifier = world.money_balance[player]/100 + if wallet[player] >= sphere_costs[player] * modifier >= 0: solvent.add(player) - if sphere_costs[player] > 0 and sphere_costs[player] > wallet[player]: + if sphere_costs[player] > 0 and sphere_costs[player] * modifier > wallet[player]: insolvent.add(player) if len([p for p in solvent if len(locked_by_money[p]) > 0]) == 0: if len(insolvent) > 0: target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p]) - difference = sphere_costs[target_player]-wallet[target_player] + target_modifier = world.money_balance[target_player]/100 + difference = sphere_costs[target_player] * target_modifier - wallet[target_player] logger.debug(f'Money balancing needed: Player {target_player} short {difference}') else: difference = 0 @@ -1066,7 +1068,8 @@ def kiki_required(state, location): solvent.add(target_player) # apply solvency for player in solvent: - wallet[player] -= sphere_costs[player] + modifier = world.money_balance[player]/100 + wallet[player] -= sphere_costs[player] * modifier for location in locked_by_money[player]: if isinstance(location, str) and location == 'Kiki': kiki_paid[player] = True diff --git a/ItemList.py b/ItemList.py index add69174..aa2f1053 100644 --- a/ItemList.py +++ b/ItemList.py @@ -704,7 +704,8 @@ def customize_shops(world, player): if retro_bow and item.name == 'Single Arrow': price = 80 # randomize price - shop.add_inventory(idx, item.name, randomize_price(price), max_repeat, player=item.player) + price = final_price(loc, price, world, player) + shop.add_inventory(idx, item.name, price, max_repeat, player=item.player) if item.name in cap_replacements and shop_name not in retro_shops and item.player == player: possible_replacements.append((shop, idx, location, item)) # randomize shopkeeper @@ -721,8 +722,10 @@ def customize_shops(world, player): if len(choices) > 0: shop, idx, loc, item = random.choice(choices) upgrade = ItemFactory('Bomb Upgrade (+5)', player) - shop.add_inventory(idx, upgrade.name, randomize_price(upgrade.price), 6, - item.name, randomize_price(item.price), player=item.player) + up_price = final_price(loc, upgrade.price, world, player) + rep_price = final_price(loc, item.price, world, player) + shop.add_inventory(idx, upgrade.name, up_price, 6, + item.name, rep_price, player=item.player) loc.item = upgrade upgrade.location = loc if not found_arrow_upgrade and len(possible_replacements) > 0: @@ -733,8 +736,10 @@ def customize_shops(world, player): if len(choices) > 0: shop, idx, loc, item = random.choice(choices) upgrade = ItemFactory('Arrow Upgrade (+5)', player) - shop.add_inventory(idx, upgrade.name, randomize_price(upgrade.price), 6, - item.name, randomize_price(item.price), player=item.player) + up_price = final_price(loc, upgrade.price, world, player) + rep_price = final_price(loc, item.price, world, player) + shop.add_inventory(idx, upgrade.name, up_price, 6, + item.name, rep_price, player=item.player) loc.item = upgrade upgrade.location = loc change_shop_items_to_rupees(world, player, shops_to_customize) @@ -742,6 +747,15 @@ def customize_shops(world, player): check_hints(world, player) +def final_price(location, price, world, player): + if world.customizer and world.customizer.get_prices(player): + custom_prices = world.customizer.get_prices(player) + if location in custom_prices: + # todo: validate valid price + return custom_prices[location] + return randomize_price(price) + + def randomize_price(price): half_price = price // 2 max_price = price - half_price @@ -781,6 +795,9 @@ def balance_prices(world, player): shop_locations = [] for shop, loc_list in shop_to_location_table.items(): for loc in loc_list: + if world.customizer and world.customizer.get_prices(player) and loc in world.customizer.get_prices(player): + needed_money += world.customizer.get_prices(player)[loc] + continue # considered a fixed price and shouldn't be altered loc = world.get_location(loc, player) shop_locations.append(loc) slot = shop_to_location_table[loc.parent_region.name].index(loc.name) diff --git a/Main.py b/Main.py index ea6820ec..bbaa96f3 100644 --- a/Main.py +++ b/Main.py @@ -145,6 +145,7 @@ def main(args, seed=None, fish=None): world.collection_rate = args.collection_rate.copy() world.colorizepots = args.colorizepots.copy() world.aga_randomness = args.aga_randomness.copy() + world.money_balance = args.money_balance.copy() world.treasure_hunt_count = {} world.treasure_hunt_total = {} @@ -513,6 +514,7 @@ def copy_world(world): ret.trap_door_mode = world.trap_door_mode.copy() ret.key_logic_algorithm = world.key_logic_algorithm.copy() ret.aga_randomness = world.aga_randomness.copy() + ret.money_balance = world.money_balance.copy() ret.experimental = world.experimental.copy() ret.shopsanity = world.shopsanity.copy() ret.dropshuffle = world.dropshuffle.copy() diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ae498523..30cf51ad 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,7 @@ * 1.4.8 - New option: Mirror Scroll - to add the item to the starting inventory in non-doors modes (Thanks Telethar!) + - Customizer: Ability to customize shop prices and control money balancing. `money_balance` is a percentage betwen 0 and 100 that attempts to ensure you have that much percentage of money available for purchases. (100 is default, 0 essentially ignores money considerations) - Fixed a key logic bug with decoupled doors when a big key door leads to a small key door (the small key door was missing appropriate logic) - Fixed an ER bug where Bonk Fairy could be used for a mandatory connector in standard mode (boots could allow escape to be skipped) - Fixed an issue with flute activation in rain mode. (thanks Codemann!) diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index 53c04ae4..12275c65 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -539,6 +539,7 @@ "action": "store_false", "type": "bool" }, + "money_balance": {}, "saveonexit": { "choices": [ "ask", diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 26771c60..a86e782b 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -19,11 +19,20 @@ def __init__(self): self.relative_dir = None self.world_rep = {} self.player_range = None + self.player_map = {} # player number to name def load_yaml(self, file): self.file_source = load_yaml(file) head, filename = os.path.split(file) self.relative_dir = head + if 'version' in self.file_source and self.file_source['version'].startswith('2'): + player_number = 1 + for key in self.file_source.keys(): + if key in ['meta', 'version']: + continue + else: + self.player_map[player_number] = key + player_number += 1 def determine_seed(self, default_seed): if 'meta' in self.file_source: @@ -161,6 +170,7 @@ def get_setting(value: Any, default): args.triforce_max_difference[p] = get_setting(settings['triforce_max_difference'], args.triforce_max_difference[p]) args.beemizer[p] = get_setting(settings['beemizer'], args.beemizer[p]) args.aga_randomness[p] = get_setting(settings['aga_randomness'], args.aga_randomness[p]) + args.money_balance[p] = get_setting(settings['money_balance'], args.money_balance[p]) # mystery usage args.usestartinventory[p] = get_setting(settings['usestartinventory'], args.usestartinventory[p]) @@ -189,6 +199,9 @@ def get_placements(self): return self.file_source['placements'] return None + def get_prices(self, player): + return self.get_attribute_by_player_composite('prices', player) + def get_advanced_placements(self): if 'advanced_placements' in self.file_source: return self.file_source['advanced_placements'] @@ -229,6 +242,34 @@ def get_enemies(self): return self.file_source['enemies'] return None + + def get_attribute_by_player_composite(self, attribute, player): + attempt = self.get_attribute_by_player_new(attribute, player) + if attempt is not None: + return attempt + attempt = self.get_attribute_by_player(attribute, player) + return attempt + + def get_attribute_by_player(self, attribute, player): + if attribute in self.file_source: + if player in self.file_source[attribute]: + return self.file_source[attribute][player] + return None + + def get_attribute_by_player_new(self, attribute, player): + player_id = self.get_player_id(player) + if player_id is not None: + if attribute in self.file_source[player_id]: + return self.file_source[player_id][attribute] + return None + + def get_player_id(self, player): + if player in self.file_source: + return player + if player in self.player_map and self.player_map[player] in self.file_source: + return self.player_map[player] + return None + def create_from_world(self, world, settings): self.player_range = range(1, world.players + 1) settings_dict, meta_dict = {}, {} @@ -293,6 +334,7 @@ def create_from_world(self, world, settings): settings_dict[p]['triforce_pool'] = world.treasure_hunt_total[p] settings_dict[p]['beemizer'] = world.beemizer[p] settings_dict[p]['aga_randomness'] = world.aga_randomness[p] + settings_dict[p]['money_balance'] = world.money_balance[p] # rom adjust stuff # settings_dict[p]['sprite'] = world.sprite[p]