feat: customizer options for prices in shops and money balance tuning

This commit is contained in:
aerinon
2025-01-15 14:32:07 -07:00
parent 9f55e90b7e
commit c7a57d63fa
8 changed files with 79 additions and 10 deletions

View File

@@ -153,6 +153,7 @@ class World(object):
set_player_attr('trap_door_mode', 'optional') set_player_attr('trap_door_mode', 'optional')
set_player_attr('key_logic_algorithm', 'partial') set_player_attr('key_logic_algorithm', 'partial')
set_player_attr('aga_randomness', True) set_player_attr('aga_randomness', True)
set_player_attr('money_balance', 100)
set_player_attr('shopsanity', False) set_player_attr('shopsanity', False)
set_player_attr('mixed_travel', 'prevent') set_player_attr('mixed_travel', 'prevent')

4
CLI.py
View File

@@ -142,7 +142,8 @@ def parse_cli(argv, no_defaults=False):
'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle',
'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx', 'mixed_travel', 'standardize_palettes', 'code', 'reduce_flashing', 'shuffle_sfx',
'msu_resume', 'collection_rate', 'colorizepots', 'decoupledoors', 'door_type_mode', '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) value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1: if player == 1:
setattr(ret, name, {1: value}) setattr(ret, name, {1: value})
@@ -228,6 +229,7 @@ def parse_settings():
'mixed_travel': 'prevent', 'mixed_travel': 'prevent',
'standardize_palettes': 'standardize', 'standardize_palettes': 'standardize',
'aga_randomness': True, 'aga_randomness': True,
'money_balance': 100,
"triforce_pool": 0, "triforce_pool": 0,
"triforce_goal": 0, "triforce_goal": 0,

11
Fill.py
View File

@@ -1020,14 +1020,16 @@ def balance_money_progression(world):
solvent = set() solvent = set()
insolvent = set() insolvent = set()
for player in range(1, world.players+1): 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) 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) insolvent.add(player)
if len([p for p in solvent if len(locked_by_money[p]) > 0]) == 0: if len([p for p in solvent if len(locked_by_money[p]) > 0]) == 0:
if len(insolvent) > 0: if len(insolvent) > 0:
target_player = min(insolvent, key=lambda p: sphere_costs[p]-wallet[p]) 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}') logger.debug(f'Money balancing needed: Player {target_player} short {difference}')
else: else:
difference = 0 difference = 0
@@ -1066,7 +1068,8 @@ def balance_money_progression(world):
solvent.add(target_player) solvent.add(target_player)
# apply solvency # apply solvency
for player in solvent: 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]: for location in locked_by_money[player]:
if isinstance(location, str) and location == 'Kiki': if isinstance(location, str) and location == 'Kiki':
kiki_paid[player] = True kiki_paid[player] = True

View File

@@ -704,7 +704,8 @@ def customize_shops(world, player):
if retro_bow and item.name == 'Single Arrow': if retro_bow and item.name == 'Single Arrow':
price = 80 price = 80
# randomize price # 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: if item.name in cap_replacements and shop_name not in retro_shops and item.player == player:
possible_replacements.append((shop, idx, location, item)) possible_replacements.append((shop, idx, location, item))
# randomize shopkeeper # randomize shopkeeper
@@ -721,8 +722,10 @@ def customize_shops(world, player):
if len(choices) > 0: if len(choices) > 0:
shop, idx, loc, item = random.choice(choices) shop, idx, loc, item = random.choice(choices)
upgrade = ItemFactory('Bomb Upgrade (+5)', player) upgrade = ItemFactory('Bomb Upgrade (+5)', player)
shop.add_inventory(idx, upgrade.name, randomize_price(upgrade.price), 6, up_price = final_price(loc, upgrade.price, world, player)
item.name, randomize_price(item.price), player=item.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 loc.item = upgrade
upgrade.location = loc upgrade.location = loc
if not found_arrow_upgrade and len(possible_replacements) > 0: if not found_arrow_upgrade and len(possible_replacements) > 0:
@@ -733,8 +736,10 @@ def customize_shops(world, player):
if len(choices) > 0: if len(choices) > 0:
shop, idx, loc, item = random.choice(choices) shop, idx, loc, item = random.choice(choices)
upgrade = ItemFactory('Arrow Upgrade (+5)', player) upgrade = ItemFactory('Arrow Upgrade (+5)', player)
shop.add_inventory(idx, upgrade.name, randomize_price(upgrade.price), 6, up_price = final_price(loc, upgrade.price, world, player)
item.name, randomize_price(item.price), player=item.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 loc.item = upgrade
upgrade.location = loc upgrade.location = loc
change_shop_items_to_rupees(world, player, shops_to_customize) change_shop_items_to_rupees(world, player, shops_to_customize)
@@ -742,6 +747,15 @@ def customize_shops(world, player):
check_hints(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): def randomize_price(price):
half_price = price // 2 half_price = price // 2
max_price = price - half_price max_price = price - half_price
@@ -781,6 +795,9 @@ def balance_prices(world, player):
shop_locations = [] shop_locations = []
for shop, loc_list in shop_to_location_table.items(): for shop, loc_list in shop_to_location_table.items():
for loc in loc_list: 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) loc = world.get_location(loc, player)
shop_locations.append(loc) shop_locations.append(loc)
slot = shop_to_location_table[loc.parent_region.name].index(loc.name) slot = shop_to_location_table[loc.parent_region.name].index(loc.name)

View File

@@ -145,6 +145,7 @@ def main(args, seed=None, fish=None):
world.collection_rate = args.collection_rate.copy() world.collection_rate = args.collection_rate.copy()
world.colorizepots = args.colorizepots.copy() world.colorizepots = args.colorizepots.copy()
world.aga_randomness = args.aga_randomness.copy() world.aga_randomness = args.aga_randomness.copy()
world.money_balance = args.money_balance.copy()
world.treasure_hunt_count = {} world.treasure_hunt_count = {}
world.treasure_hunt_total = {} world.treasure_hunt_total = {}
@@ -513,6 +514,7 @@ def copy_world(world):
ret.trap_door_mode = world.trap_door_mode.copy() ret.trap_door_mode = world.trap_door_mode.copy()
ret.key_logic_algorithm = world.key_logic_algorithm.copy() ret.key_logic_algorithm = world.key_logic_algorithm.copy()
ret.aga_randomness = world.aga_randomness.copy() ret.aga_randomness = world.aga_randomness.copy()
ret.money_balance = world.money_balance.copy()
ret.experimental = world.experimental.copy() ret.experimental = world.experimental.copy()
ret.shopsanity = world.shopsanity.copy() ret.shopsanity = world.shopsanity.copy()
ret.dropshuffle = world.dropshuffle.copy() ret.dropshuffle = world.dropshuffle.copy()

View File

@@ -2,6 +2,7 @@
* 1.4.8 * 1.4.8
- New option: Mirror Scroll - to add the item to the starting inventory in non-doors modes (Thanks Telethar!) - 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 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 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!) - Fixed an issue with flute activation in rain mode. (thanks Codemann!)

View File

@@ -539,6 +539,7 @@
"action": "store_false", "action": "store_false",
"type": "bool" "type": "bool"
}, },
"money_balance": {},
"saveonexit": { "saveonexit": {
"choices": [ "choices": [
"ask", "ask",

View File

@@ -19,11 +19,20 @@ class CustomSettings(object):
self.relative_dir = None self.relative_dir = None
self.world_rep = {} self.world_rep = {}
self.player_range = None self.player_range = None
self.player_map = {} # player number to name
def load_yaml(self, file): def load_yaml(self, file):
self.file_source = load_yaml(file) self.file_source = load_yaml(file)
head, filename = os.path.split(file) head, filename = os.path.split(file)
self.relative_dir = head 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): def determine_seed(self, default_seed):
if 'meta' in self.file_source: if 'meta' in self.file_source:
@@ -161,6 +170,7 @@ class CustomSettings(object):
args.triforce_max_difference[p] = get_setting(settings['triforce_max_difference'], args.triforce_max_difference[p]) 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.beemizer[p] = get_setting(settings['beemizer'], args.beemizer[p])
args.aga_randomness[p] = get_setting(settings['aga_randomness'], args.aga_randomness[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 # mystery usage
args.usestartinventory[p] = get_setting(settings['usestartinventory'], args.usestartinventory[p]) args.usestartinventory[p] = get_setting(settings['usestartinventory'], args.usestartinventory[p])
@@ -189,6 +199,9 @@ class CustomSettings(object):
return self.file_source['placements'] return self.file_source['placements']
return None return None
def get_prices(self, player):
return self.get_attribute_by_player_composite('prices', player)
def get_advanced_placements(self): def get_advanced_placements(self):
if 'advanced_placements' in self.file_source: if 'advanced_placements' in self.file_source:
return self.file_source['advanced_placements'] return self.file_source['advanced_placements']
@@ -229,6 +242,34 @@ class CustomSettings(object):
return self.file_source['enemies'] return self.file_source['enemies']
return None 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): def create_from_world(self, world, settings):
self.player_range = range(1, world.players + 1) self.player_range = range(1, world.players + 1)
settings_dict, meta_dict = {}, {} settings_dict, meta_dict = {}, {}
@@ -293,6 +334,7 @@ class CustomSettings(object):
settings_dict[p]['triforce_pool'] = world.treasure_hunt_total[p] settings_dict[p]['triforce_pool'] = world.treasure_hunt_total[p]
settings_dict[p]['beemizer'] = world.beemizer[p] settings_dict[p]['beemizer'] = world.beemizer[p]
settings_dict[p]['aga_randomness'] = world.aga_randomness[p] settings_dict[p]['aga_randomness'] = world.aga_randomness[p]
settings_dict[p]['money_balance'] = world.money_balance[p]
# rom adjust stuff # rom adjust stuff
# settings_dict[p]['sprite'] = world.sprite[p] # settings_dict[p]['sprite'] = world.sprite[p]