Skip to content

Commit

Permalink
feat: customizer options for prices in shops and money balance tuning
Browse files Browse the repository at this point in the history
  • Loading branch information
aerinon committed Jan 15, 2025
1 parent 9f55e90 commit c7a57d6
Show file tree
Hide file tree
Showing 8 changed files with 79 additions and 10 deletions.
1 change: 1 addition & 0 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
4 changes: 3 additions & 1 deletion CLI.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 7 additions & 4 deletions Fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
27 changes: 22 additions & 5 deletions ItemList.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -733,15 +736,26 @@ 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)
balance_prices(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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!)
Expand Down
1 change: 1 addition & 0 deletions resources/app/cli/args.json
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,7 @@
"action": "store_false",
"type": "bool"
},
"money_balance": {},
"saveonexit": {
"choices": [
"ask",
Expand Down
42 changes: 42 additions & 0 deletions source/classes/CustomSettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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']
Expand Down Expand Up @@ -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 = {}, {}
Expand Down Expand Up @@ -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]
Expand Down

0 comments on commit c7a57d6

Please sign in to comment.