From f5a8b563d9510f0777a890c0aad10018b845d55e Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 7 Jan 2025 14:45:02 -0700 Subject: [PATCH 01/17] fix: key logic issue with decoupled doors --- CHANGELOG.md | 17 +++++++++++++++++ KeyDoorShuffle.py | 7 ++++++- Main.py | 2 +- RELEASENOTES.md | 19 ++----------------- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1bd83c8..3c9f2520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ Changelog archive +* 1.4.7.2 + - Fixed an issue with shuffle_ganon/fix_gtower_exit causing a generation failure + - More HMG fixes by Muffins +* 1.4.7.1 + - Fixed an issue with the repaired "beemizer" setting not being backwards compatible +* 1.4.7 + - Fixed generation error with Big Key in starting inventory (thanks Cody!) + - HMG/NL logic fixes by Muffins + - Enemizer: Disabled Walking Zora in the UW due to crash with Swamola (they ignore a lot of collison anyway) + - Enemizer: Fixed an issue with enemizer bush sprites + - Enemizer: Banned new Mimics from being the randomized bush sprite due to crash + - "Beatable" or "accessibility: none" can now use randomized trap doors to seal off entire parts of dungeons (was intended, bug prevented the logic skip) + - Logic error with enemizer and standard should use new enemy logic rules + - Fixed a bug with the inconsistent treatment of the beemizer setting + - Fixed an issue with returning Blacksmith in Simple shuffle (when blacksmith is at Link's House) + - Fixed an issue with dark sanctuary spawn at tavern north door (thanks Codemann!) + - Various enemy bans for the last few months * 1.4.6 - Restores original Sanc & Quit behavior, if Aga1 is not dead, then no quick pyramid warp - Fixed problem with Lite/Lean shuffling some fairy caves unnecessarily diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 5331ad4c..0508e35d 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -814,7 +814,12 @@ def key_wasted(new_door, old_door, old_counter, new_counter, key_layout, world, def find_next_counter(new_door, old_counter, key_layout, prize_flag=None): - proposed_doors = {**old_counter.open_doors, **dict.fromkeys([new_door, new_door.dest])} + prop_doors = next((item_or_tuple for item_or_tuple in key_layout.proposal + if new_door == item_or_tuple or (isinstance(item_or_tuple, tuple) and new_door in item_or_tuple)), None) + if prop_doors: + proposed_doors = {**old_counter.open_doors, **dict.fromkeys([prop_doors])} + else: + proposed_doors = {**old_counter.open_doors} bk_open = old_counter.big_key_opened or new_door.bigKey prize_flag = prize_flag if prize_flag else old_counter.prize_doors_opened return find_counter(proposed_doors, bk_open, key_layout, prize_flag) diff --git a/Main.py b/Main.py index a36d34e7..3614c00b 100644 --- a/Main.py +++ b/Main.py @@ -38,7 +38,7 @@ from source.enemizer.DamageTables import DamageTable from source.enemizer.Enemizer import randomize_enemies from source.rom.DataTables import init_data_tables -version_number = '1.4.7.2' +version_number = '1.4.8' version_branch = '-u' __version__ = f'{version_number}{version_branch}' diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 602fb94d..b800687c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,19 +1,4 @@ # Patch Notes -* 1.4.7.2 - - Fixed an issue with shuffle_ganon/fix_gtower_exit causing a generation failure - - More HMG fixes by Muffins -* 1.4.7.1 - - Fixed an issue with the repaired "beemizer" setting not being backwards compatible -* 1.4.7 - - Fixed generation error with Big Key in starting inventory (thanks Cody!) - - HMG/NL logic fixes by Muffins - - Enemizer: Disabled Walking Zora in the UW due to crash with Swamola (they ignore a lot of collison anyway) - - Enemizer: Fixed an issue with enemizer bush sprites - - Enemizer: Banned new Mimics from being the randomized bush sprite due to crash - - "Beatable" or "accessibility: none" can now use randomized trap doors to seal off entire parts of dungeons (was intended, bug prevented the logic skip) - - Logic error with enemizer and standard should use new enemy logic rules - - Fixed a bug with the inconsistent treatment of the beemizer setting - - Fixed an issue with returning Blacksmith in Simple shuffle (when blacksmith is at Link's House) - - Fixed an issue with dark sanctuary spawn at tavern north door (thanks Codemann!) - - Various enemy bans for the last few months +* 1.4.8 + - 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) From d558e140655b8f10345a048991db0762a2df51ee Mon Sep 17 00:00:00 2001 From: Telethar Date: Sat, 28 Dec 2024 17:44:42 -0800 Subject: [PATCH 02/17] Add Mirror Scroll as an option instead of baking it into door shuffle. --- .gitignore | 1 + BaseClasses.py | 8 ++++++-- CLI.py | 3 ++- Main.py | 1 + README.md | 6 ++++++ Rom.py | 6 +++++- resources/app/cli/args.json | 4 ++++ resources/app/cli/lang/en.json | 1 + resources/app/gui/lang/en.json | 1 + resources/app/gui/randomize/item/widgets.json | 1 + source/classes/CustomSettings.py | 2 ++ source/classes/constants.py | 1 + source/tools/MysteryUtils.py | 1 + 13 files changed, 32 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index bc4845cf..a332f0ff 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ weights/ /Players/ /QUsb2Snes/ /output/ +/enemizer/ base2current.json diff --git a/BaseClasses.py b/BaseClasses.py index 9ac4a9cb..50a48039 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -143,6 +143,7 @@ class World(object): set_player_attr('potshuffle', False) set_player_attr('pot_contents', None) set_player_attr('pseudoboots', False) + set_player_attr('mirrorscroll', False) set_player_attr('collection_rate', False) set_player_attr('colorizepots', True) set_player_attr('pot_pool', {}) @@ -2588,6 +2589,7 @@ class Spoiler(object): 'potshuffle': self.world.potshuffle, 'shopsanity': self.world.shopsanity, 'pseudoboots': self.world.pseudoboots, + 'mirrorscroll': self.world.mirrorscroll, 'triforcegoal': self.world.treasure_hunt_count, 'triforcepool': self.world.treasure_hunt_total, 'race': self.world.settings.world_rep['meta']['race'], @@ -2766,6 +2768,7 @@ class Spoiler(object): outfile.write(f"Bow Mode: {self.metadata['bow_mode'][player]}\n") outfile.write(f"Bombbag: {yn(self.metadata['bombbag'][player])}\n") outfile.write(f"Pseudoboots: {yn(self.metadata['pseudoboots'][player])}\n") + outfile.write(f"Mirror Scroll: {yn(self.metadata['mirrorscroll'][player])}\n") outfile.write('\n') # Item Pool Settings @@ -3120,7 +3123,7 @@ take_any_mode = {'none': 0, 'random': 1, 'fixed': 2} bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} # additions -# byte 12: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo) +# byte 12: POOT TKKK (mirrorscroll, pseudoboots, overworld_map, trap_door_mode, key_logic_algo) overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2} trap_door_mode = {'vanilla': 0, 'optional': 1, 'boss': 2, 'oneway': 3} key_logic_algo = {'dangerous': 0, 'partial': 1, 'strict': 2} @@ -3176,7 +3179,7 @@ class Settings(object): (flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4 | take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]), - ((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 5 + ((0xF0 if w.mirrorscroll[p] else 0) | (0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 5 | trap_door_mode[w.trap_door_mode[p]] << 3 | key_logic_algo[w.key_logic_algorithm[p]]), (skullwoods_mode[w.skullwoods[p]] << 6 | linked_drops_mode[w.linked_drops[p]] << 4), @@ -3249,6 +3252,7 @@ class Settings(object): args.take_any[p] = r(take_any_mode)[(settings[11] & 0xC) >> 2] args.keyshuffle[p] = r(keyshuffle_mode)[settings[11] & 0x3] if len(settings) > 12: + args.mirrorscroll[p] = True if settings[12] & 0xF0 else False args.pseudoboots[p] = True if settings[12] & 0x80 else False args.overworld_map[p] = r(overworld_map_mode)[(settings[12] & 0x60) >> 5] args.trap_door_mode[p] = r(trap_door_mode)[(settings[12] & 0x18) >> 3] diff --git a/CLI.py b/CLI.py index 4efc85b6..b2cc20e3 100644 --- a/CLI.py +++ b/CLI.py @@ -136,7 +136,7 @@ def parse_cli(argv, no_defaults=False): 'triforce_max_difference', 'triforce_pool_min', 'triforce_pool_max', 'triforce_goal_min', 'triforce_goal_max', 'triforce_min_difference', 'triforce_goal', 'triforce_pool', 'shufflelinks', 'shuffletavern', 'skullwoods', 'linked_drops', - 'pseudoboots', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', + 'pseudoboots', 'mirrorscroll', 'retro', 'accessibility', 'hints', 'beemizer', 'experimental', 'dungeon_counters', 'shufflebosses', 'shuffleenemies', 'enemy_health', 'enemy_damage', 'shufflepots', 'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'heartbeep', 'remote_items', 'shopsanity', 'dropshuffle', 'pottery', 'keydropshuffle', @@ -197,6 +197,7 @@ def parse_settings(): "overworld_map": "default", 'take_any': 'none', "pseudoboots": False, + "mirrorscroll": False, "shuffleenemies": "none", "shufflebosses": "none", diff --git a/Main.py b/Main.py index 3614c00b..ea6820ec 100644 --- a/Main.py +++ b/Main.py @@ -138,6 +138,7 @@ def main(args, seed=None, fish=None): world.skullwoods = args.skullwoods.copy() world.linked_drops = args.linked_drops.copy() world.pseudoboots = args.pseudoboots.copy() + world.mirrorscroll = args.mirrorscroll.copy() world.overworld_map = args.overworld_map.copy() world.take_any = args.take_any.copy() world.restrict_boss_items = args.restrict_boss_items.copy() diff --git a/README.md b/README.md index 20d75b00..4e4f98af 100644 --- a/README.md +++ b/README.md @@ -300,6 +300,12 @@ Dashing is allowed without the boots item however doors and certain rocks remain CLI `--pseudoboots` +#### Mirror Scroll + +Mirror is usable inside dungeons. Locations that require the mirror are still unattainable. + +CLI `--mirrorscroll` + #### Flute Mode Normal mode for flute means you need to activate it at the village statue after finding it like usual. Activated flute mode mean you can use it immediately upon finding it. The flute SFX plays to let you know this is the case. diff --git a/Rom.py b/Rom.py index a3654c8c..72403c5e 100644 --- a/Rom.py +++ b/Rom.py @@ -545,7 +545,11 @@ def patch_rom(world, rom, player, team, is_mystery=False): patch_shuffled_dark_sanc(world, rom, player) # setup dr option flags based on experimental, etc. - dr_flags = DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] == 'vanilla' else DROptions.Town_Portal + dr_flags = DROptions.NoOptions + if world.mirrorscroll[player]: + dr_flags = DROptions.Town_Portal + if world.doorShuffle[player] == 'vanilla': + dr_flags |= DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] not in ['vanilla', 'basic']: dr_flags |= DROptions.Map_Info if ((world.collection_rate[player] or world.goal[player] == 'completionist') diff --git a/resources/app/cli/args.json b/resources/app/cli/args.json index d855857c..53c04ae4 100644 --- a/resources/app/cli/args.json +++ b/resources/app/cli/args.json @@ -465,6 +465,10 @@ "action": "store_true", "type": "bool" }, + "mirrorscroll": { + "action": "store_true", + "type": "bool" + }, "calc_playthrough": { "action": "store_false", "type": "bool" diff --git a/resources/app/cli/lang/en.json b/resources/app/cli/lang/en.json index 113b5efe..7c70461d 100644 --- a/resources/app/cli/lang/en.json +++ b/resources/app/cli/lang/en.json @@ -348,6 +348,7 @@ "Fixed: Take any caves will replace certain location. See documentation for full list" ], "pseudoboots": [ " Players starts with pseudo boots that allow dashing but no item checks (default: %(default)s"], + "mirrorscroll": [ " Players starts with mirror scroll that allows mirror in dungeons but not overworld (default: %(default)s"], "bombbag": ["Start with 0 bomb capacity. Two capacity upgrades (+10) are added to the pool (default: %(default)s)" ], "any_enemy_logic": [ "How to handle potential traversal between dungeon in Crossed door shuffle", diff --git a/resources/app/gui/lang/en.json b/resources/app/gui/lang/en.json index e841d2fa..92318d72 100644 --- a/resources/app/gui/lang/en.json +++ b/resources/app/gui/lang/en.json @@ -241,6 +241,7 @@ "randomizer.item.race": "Generate \"Race\" ROM", "randomizer.item.retro": "Retro mode", "randomizer.item.pseudoboots": "Pseudoboots", + "randomizer.item.mirrorscroll": "Mirror Scroll", "randomizer.item.worldstate": "World State", "randomizer.item.worldstate.standard": "Standard", diff --git a/resources/app/gui/randomize/item/widgets.json b/resources/app/gui/randomize/item/widgets.json index 0a895d3c..b0b5551f 100644 --- a/resources/app/gui/randomize/item/widgets.json +++ b/resources/app/gui/randomize/item/widgets.json @@ -2,6 +2,7 @@ "checkboxes": { "hints": { "type": "checkbox" }, "pseudoboots": { "type": "checkbox" }, + "mirrorscroll": { "type": "checkbox" }, "race": { "type": "checkbox" } }, "leftItemFrame": { diff --git a/source/classes/CustomSettings.py b/source/classes/CustomSettings.py index 37bb0206..26771c60 100644 --- a/source/classes/CustomSettings.py +++ b/source/classes/CustomSettings.py @@ -150,6 +150,7 @@ class CustomSettings(object): args.restrict_boss_items[p] = get_setting(settings['restrict_boss_items'], args.restrict_boss_items[p]) args.overworld_map[p] = get_setting(settings['overworld_map'], args.overworld_map[p]) args.pseudoboots[p] = get_setting(settings['pseudoboots'], args.pseudoboots[p]) + args.mirrorscroll[p] = get_setting(settings['mirrorscroll'], args.mirrorscroll[p]) args.triforce_goal[p] = get_setting(settings['triforce_goal'], args.triforce_goal[p]) args.triforce_pool[p] = get_setting(settings['triforce_pool'], args.triforce_pool[p]) args.triforce_goal_min[p] = get_setting(settings['triforce_goal_min'], args.triforce_goal_min[p]) @@ -287,6 +288,7 @@ class CustomSettings(object): settings_dict[p]['linked_drops'] = world.linked_drops[p] settings_dict[p]['overworld_map'] = world.overworld_map[p] settings_dict[p]['pseudoboots'] = world.pseudoboots[p] + settings_dict[p]['mirrorscroll'] = world.mirrorscroll[p] settings_dict[p]['triforce_goal'] = world.treasure_hunt_count[p] settings_dict[p]['triforce_pool'] = world.treasure_hunt_total[p] settings_dict[p]['beemizer'] = world.beemizer[p] diff --git a/source/classes/constants.py b/source/classes/constants.py index 7c213ff9..54f4f2fb 100644 --- a/source/classes/constants.py +++ b/source/classes/constants.py @@ -57,6 +57,7 @@ SETTINGSTOPROCESS = { "item": { "hints": "hints", "pseudoboots": "pseudoboots", + "mirrorscroll": "mirrorscroll", "race": "race", "worldstate": "mode", diff --git a/source/tools/MysteryUtils.py b/source/tools/MysteryUtils.py index aa0d9127..0d9bd8b7 100644 --- a/source/tools/MysteryUtils.py +++ b/source/tools/MysteryUtils.py @@ -134,6 +134,7 @@ def roll_settings(weights): ret.skullwoods = get_choice('skullwoods') ret.linked_drops = get_choice('linked_drops') ret.pseudoboots = get_choice_bool('pseudoboots') + ret.mirrorscroll = get_choice_bool('mirrorscroll') ret.shopsanity = get_choice_bool('shopsanity') keydropshuffle = get_choice_bool('keydropshuffle') ret.dropshuffle = get_choice('dropshuffle') if 'dropshuffle' in weights else 'none' From c3f6317e848a231eb545323e078839c40c56b1df Mon Sep 17 00:00:00 2001 From: Telethar Date: Sat, 28 Dec 2024 17:49:44 -0800 Subject: [PATCH 03/17] Change to |= --- Rom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rom.py b/Rom.py index 72403c5e..220dcc2d 100644 --- a/Rom.py +++ b/Rom.py @@ -547,7 +547,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): # setup dr option flags based on experimental, etc. dr_flags = DROptions.NoOptions if world.mirrorscroll[player]: - dr_flags = DROptions.Town_Portal + dr_flags |= DROptions.Town_Portal if world.doorShuffle[player] == 'vanilla': dr_flags |= DROptions.Eternal_Mini_Bosses if world.doorShuffle[player] not in ['vanilla', 'basic']: From ed5627d5bc4185c82d53613cf4d1a7acd33be9c0 Mon Sep 17 00:00:00 2001 From: aerinon Date: Tue, 7 Jan 2025 15:56:33 -0700 Subject: [PATCH 04/17] feat: mirror scroll forced on in DR modes, fix settings code bit for it --- BaseClasses.py | 11 ++++++----- RELEASENOTES.md | 1 + Rom.py | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 50a48039..ab332eb0 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -3123,12 +3123,12 @@ take_any_mode = {'none': 0, 'random': 1, 'fixed': 2} bow_mode = {'progressive': 0, 'silvers': 1, 'retro': 2, 'retro_silvers': 3} # additions -# byte 12: POOT TKKK (mirrorscroll, pseudoboots, overworld_map, trap_door_mode, key_logic_algo) +# byte 12: POOT TKKK (pseudoboots, overworld_map, trap_door_mode, key_logic_algo) overworld_map_mode = {'default': 0, 'compass': 1, 'map': 2} trap_door_mode = {'vanilla': 0, 'optional': 1, 'boss': 2, 'oneway': 3} key_logic_algo = {'dangerous': 0, 'partial': 1, 'strict': 2} -# byte 13: SSDD ???? (skullwoods, linked_drops, 4 free bytes) +# byte 13: SSDD M??? (skullwoods, linked_drops, mirrorscroll, ??? = 3 free bytes) skullwoods_mode = {'original': 0, 'restricted': 1, 'loose': 2, 'followlinked': 3} linked_drops_mode = {'unset': 0, 'linked': 1, 'independent': 2} @@ -3179,10 +3179,11 @@ class Settings(object): (flute_mode[w.flute_mode[p]] << 7 | bow_mode[w.bow_mode[p]] << 4 | take_any_mode[w.take_any[p]] << 2 | keyshuffle_mode[w.keyshuffle[p]]), - ((0xF0 if w.mirrorscroll[p] else 0) | (0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 5 + ((0x80 if w.pseudoboots[p] else 0) | overworld_map_mode[w.overworld_map[p]] << 5 | trap_door_mode[w.trap_door_mode[p]] << 3 | key_logic_algo[w.key_logic_algorithm[p]]), - (skullwoods_mode[w.skullwoods[p]] << 6 | linked_drops_mode[w.linked_drops[p]] << 4), + (skullwoods_mode[w.skullwoods[p]] << 6 | linked_drops_mode[w.linked_drops[p]] << 4 + | (0x8 if w.mirrorscroll[p] else 0)), ]) return base64.b64encode(code, "+-".encode()).decode() @@ -3252,7 +3253,6 @@ class Settings(object): args.take_any[p] = r(take_any_mode)[(settings[11] & 0xC) >> 2] args.keyshuffle[p] = r(keyshuffle_mode)[settings[11] & 0x3] if len(settings) > 12: - args.mirrorscroll[p] = True if settings[12] & 0xF0 else False args.pseudoboots[p] = True if settings[12] & 0x80 else False args.overworld_map[p] = r(overworld_map_mode)[(settings[12] & 0x60) >> 5] args.trap_door_mode[p] = r(trap_door_mode)[(settings[12] & 0x18) >> 3] @@ -3260,6 +3260,7 @@ class Settings(object): if len(settings) > 13: args.skullwoods[p] = r(skullwoods_mode)[(settings[13] & 0xc0) >> 6] args.linked_drops[p] = r(linked_drops_mode)[(settings[13] & 0x30) >> 4] + args.mirrorscroll[p] = True if settings[13] & 0x8 else False class KeyRuleType(FastEnum): diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b800687c..5f5c6836 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,4 +1,5 @@ # Patch Notes * 1.4.8 + - New option: Mirror Scroll - to add the item to the starting inventory in non-doors modes - 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) diff --git a/Rom.py b/Rom.py index 220dcc2d..df032a83 100644 --- a/Rom.py +++ b/Rom.py @@ -546,7 +546,7 @@ def patch_rom(world, rom, player, team, is_mystery=False): # setup dr option flags based on experimental, etc. dr_flags = DROptions.NoOptions - if world.mirrorscroll[player]: + if world.mirrorscroll[player] or world.doorShuffle[player] != 'vanilla': dr_flags |= DROptions.Town_Portal if world.doorShuffle[player] == 'vanilla': dr_flags |= DROptions.Eternal_Mini_Bosses From 3a9b1601affef37068c802f1696a6dad8b518b17 Mon Sep 17 00:00:00 2001 From: codemann8 Date: Wed, 25 Dec 2024 23:37:38 -0600 Subject: [PATCH 05/17] Documentation typo --- docs/Customizer.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Customizer.md b/docs/Customizer.md index 11cea9fc..9ee0571b 100644 --- a/docs/Customizer.md +++ b/docs/Customizer.md @@ -77,17 +77,17 @@ You may list each location for a player and the item you wish to place there. A This must be defined by player. Each player number should be listed with the appropriate section. Each section is a list of placement rules. Each placement rule has a specific type. -Supported Types: PlacementGroup, NotPlacmentGroup, PreferredLocationGroup +Supported Types: LocationGroup, NotLocationGroup, PreferredLocationGroup -#### PlacementGroup +#### LocationGroup You may define an item, and a list of locations. The locations may be weighted if desired. The item will be placed at one of the listed locations - this currently ignores logic. The item will be placed there. The special location 'Random' indicates that the item should be placed randomly, without any other consideration. This may be repeated for placement of multiple items like multiple bows or swords. -#### NotPlacementGroup +#### NotLocationGroup You may define an item and a list of locations that an item should not be placed at. This will apply to all items of that type. The logic is considered for this. If it is otherwise impossible, the item will be considered for the listed locations. This is important for small key layouts mostly, but it will try other locations first. -#### PreferredPlacementGroup +#### PreferredLocationGroup You may define a list of items and a list of locations. Those items will be considered first for placements and the logic will attempt to place those items in those locations first. If there are more item than locations or vice versa, the leftover items or location will be treated normally. (Although, the leftover items will be placed earlier by the algorithm than those not listed) From 9a8529100e7d66b9cdc04f22325f7f1781317ddc Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 8 Jan 2025 08:00:00 -0700 Subject: [PATCH 06/17] doc: doc fix --- RELEASENOTES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5f5c6836..3fe217ac 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,6 @@ # Patch Notes * 1.4.8 - - New option: Mirror Scroll - to add the item to the starting inventory in non-doors modes + - New option: Mirror Scroll - to add the item to the starting inventory in non-doors modes (Thanks Telethar!) - 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) + - Minor documentation fixes (thanks Codemann8!) From fe520626e9f8e59fae9efc6b4bb7deba6c258bc9 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 8 Jan 2025 09:19:38 -0700 Subject: [PATCH 07/17] fix: rain state + activated flute --- InitialSram.py | 17 +++++++++++++++++ RELEASENOTES.md | 3 ++- Rom.py | 2 +- data/base2current.bps | Bin 117954 -> 117954 bytes 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/InitialSram.py b/InitialSram.py index c5c6a814..742e4fe3 100644 --- a/InitialSram.py +++ b/InitialSram.py @@ -53,6 +53,9 @@ class InitialSram: def pre_open_pyramid_hole(self): self._or_value(OVERWORLD_DATA+0x5B, 0x20) + def pre_set_overworld_flag(self, owid, bitmask): + self._or_value(OVERWORLD_DATA+owid, bitmask) + def pre_open_tr_bomb_doors(self): self._or_value(ROOM_DATA+0x47, 0x80) self._or_value(ROOM_DATA+0x01AB, 0x80) @@ -111,6 +114,20 @@ class InitialSram: equip[0x37B] = 1 starting_magic = 0x80 + if world.mode[player] == 'standard' and world.logic[player] not in ['noglitches', 'minorglitches']: + if startingstate.has('Ocarina (Activated)', player): + self.pre_set_overworld_flag(0x18, 0x20) + + if startingstate.has('Return Old Man', player): + self._initial_sram_bytes[0x410] |= 0x01 + + if startingstate.has('Beat Agahnim 1', player): + self.pre_open_lumberjack() + if world.mode[player] == 'standard': + self.set_progress_indicator(0x80) # todo: probably missing some code rom side for this + else: + self.set_progress_indicator(0x03) + for item in world.precollected_items: if item.player != player: continue diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3fe217ac..414346ab 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,4 +3,5 @@ * 1.4.8 - New option: Mirror Scroll - to add the item to the starting inventory in non-doors modes (Thanks Telethar!) - 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) - - Minor documentation fixes (thanks Codemann8!) + - Fixed an issue with flute activation in rain mode. (thanks Codemann!) + - Minor documentation fixes (thanks Codemann!) diff --git a/Rom.py b/Rom.py index df032a83..2d666de2 100644 --- a/Rom.py +++ b/Rom.py @@ -42,7 +42,7 @@ from source.enemizer.Enemizer import write_enemy_shuffle_settings JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '1cbe62592c0e9a7d224232c62292ecc9' +RANDOMIZERBASEHASH = '403d349584246fd845c3a9c78a55b3d4' class JsonRom(object): diff --git a/data/base2current.bps b/data/base2current.bps index a54460176d1154a5ed35259a748b73b4324c5c25..0292d6f7bd5188d22f912d6816b65e3388782b64 100644 GIT binary patch delta 37 vcmV+=0NVe;nFqp|2e1MI1VI140J8=IYwH7;@_w^)?nd4SG~}{G)uMC{FoF=` delta 37 vcmV+=0NVe;nFqp|2e1MI1Ox#4|FZ@IYwH6Ng0Qo6?nd4S2Is*;YiTD)9a;}c From 1bb7345a01496ffa7b36c0405897e18e22365d4d Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 8 Jan 2025 09:47:44 -0700 Subject: [PATCH 08/17] fix: bonk fairy used as mandatory connector in standard fix: hmg multi-player issue --- RELEASENOTES.md | 2 ++ UnderworldGlitchRules.py | 2 +- source/overworld/EntranceShuffle2.py | 21 +++++++++++---------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 414346ab..dfef7c0b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,5 +3,7 @@ * 1.4.8 - New option: Mirror Scroll - to add the item to the starting inventory in non-doors modes (Thanks Telethar!) - 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!) + - Fixed an issue with multi-player HMG - Minor documentation fixes (thanks Codemann!) diff --git a/UnderworldGlitchRules.py b/UnderworldGlitchRules.py index 623a1dcc..0ae3cee8 100644 --- a/UnderworldGlitchRules.py +++ b/UnderworldGlitchRules.py @@ -68,7 +68,7 @@ def connect_hmg_entrances_regions(world, player): connection.connect(target) # Add the new Ice path (back of bomb drop to front) to the world and model it properly - ip_clip_entrance = world.get_entrance('Ice Bomb Drop Clip', 1) + ip_clip_entrance = world.get_entrance('Ice Bomb Drop Clip', player) clip_door = Door(player, "Ice Bomb Drop Clip", DoorType.Logical, ip_clip_entrance) world.doors += [clip_door] world.initialize_doors([clip_door]) diff --git a/source/overworld/EntranceShuffle2.py b/source/overworld/EntranceShuffle2.py index a033a112..9426d6ba 100644 --- a/source/overworld/EntranceShuffle2.py +++ b/source/overworld/EntranceShuffle2.py @@ -260,7 +260,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def): rem_entrances.update(lw_entrances) rem_entrances.update(dw_entrances) else: - # cross world mandantory + # cross world mandatory entrance_list = list(entrances) if avail.swapped: ban_list = Forbidden_Swap_Entrances_Inv if avail.inverted else Forbidden_Swap_Entrances @@ -314,13 +314,11 @@ def do_main_shuffle(entrances, exits, avail, mode_def): avail.decoupled_exits.remove(bomb_shop) rem_exits.remove(bomb_shop) - def bonk_fairy_exception(x): # (Bonk Fairy not eligible in standard) - return not avail.is_standard() or x != 'Bonk Fairy (Light)' if not cross_world: # OM Cave entrance in lw/dw if cross_world off if 'Old Man Cave Exit (West)' in rem_exits: world_limiter = DW_Entrances if avail.inverted else LW_Entrances - om_cave_options = sorted([x for x in rem_entrances if x in world_limiter and bonk_fairy_exception(x)]) + om_cave_options = sorted([x for x in rem_entrances if x in world_limiter and bonk_fairy_exception(avail, x)]) om_cave_choice = random.choice(om_cave_options) if not avail.coupled: connect_exit('Old Man Cave Exit (West)', om_cave_choice, avail) @@ -334,7 +332,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def): if not avail.inverted: # we don't really care where this ends up in inverted? for ext in om_house: if ext in rem_exits: - om_house_options = [x for x in rem_entrances if x in LW_Entrances and bonk_fairy_exception(x)] + om_house_options = [x for x in rem_entrances if x in LW_Entrances and bonk_fairy_exception(avail, x)] om_house_choice = random.choice(om_house_options) if not avail.coupled: connect_exit(ext, om_house_choice, avail) @@ -351,7 +349,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def): lw_entrances, dw_entrances = [], [] left = sorted(rem_entrances) for x in left: - if bonk_fairy_exception(x): + if bonk_fairy_exception(avail, x): lw_entrances.append(x) if x in LW_Entrances else dw_entrances.append(x) do_same_world_connectors(lw_entrances, dw_entrances, multi_exit_caves, avail) if avail.world.doorShuffle[avail.player] != 'vanilla': @@ -361,7 +359,7 @@ def do_main_shuffle(entrances, exits, avail, mode_def): unused_entrances.update(lw_entrances) unused_entrances.update(dw_entrances) else: - entrance_list = sorted([x for x in rem_entrances if bonk_fairy_exception(x)]) + entrance_list = sorted([x for x in rem_entrances if bonk_fairy_exception(avail, x)]) do_cross_world_connectors(entrance_list, multi_exit_caves, avail) unused_entrances.update(entrance_list) @@ -1081,6 +1079,8 @@ def do_vanilla_connect(pool_def, avail): avail.entrances.remove(entrance) avail.exits.remove(target) +def bonk_fairy_exception(avail, x): # (Bonk Fairy not eligible in standard) + return not avail.is_standard() or x != 'Bonk Fairy (Light)' def do_mandatory_connections(avail, entrances, cave_options, must_exit): if len(must_exit) == 0: @@ -1171,7 +1171,8 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): if len(cave) == 2: entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] and e not in invalid_cave_connections[tuple(cave)] and e not in must_exit - and (not avail.swapped or rnd_cave[0] != avail.combine_map[e])) + and (not avail.swapped or rnd_cave[0] != avail.combine_map[e]) + and bonk_fairy_exception(avail, e)) entrances.remove(entrance) connect_two_way(entrance, rnd_cave[0], avail) if avail.swapped and avail.combine_map[entrance] != rnd_cave[0]: @@ -1191,7 +1192,7 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): cave_entrances.append(entrance) else: entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit] and e not in must_exit - and (not avail.swapped or cave_exit != avail.combine_map[e])) + and (not avail.swapped or cave_exit != avail.combine_map[e]) and bonk_fairy_exception(avail, e)) cave_entrances.append(entrance) entrances.remove(entrance) connect_two_way(entrance, cave_exit, avail) @@ -1222,7 +1223,7 @@ def do_mandatory_connections(avail, entrances, cave_options, must_exit): continue else: entrance = next(e for e in entrances[::-1] if e not in invalid_cave_connections[tuple(cave)] - and (not avail.swapped or cave_exit != avail.combine_map[e])) + and (not avail.swapped or cave_exit != avail.combine_map[e]) and bonk_fairy_exception(avail, e)) invalid_cave_connections[tuple(cave)] = set() entrances.remove(entrance) connect_two_way(entrance, cave_exit, avail) From f83b6d03daecfc4b46bc40e3c67334eed6e64fae Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 8 Jan 2025 11:54:29 -0700 Subject: [PATCH 09/17] fix: remove some restrictions from gui's custom item pool --- ItemList.py | 30 ------------------------------ RELEASENOTES.md | 1 + 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/ItemList.py b/ItemList.py index 3a74d106..56b394b1 100644 --- a/ItemList.py +++ b/ItemList.py @@ -1121,32 +1121,11 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer assert loc not in placed_items placed_items[loc] = item - # Correct for insanely oversized item counts and take initial steps to handle undersized pools. - # Bow to Silver Arrows Upgrade, including Generic Keys & Rupoors - for x in [*range(0, 66 + 1), 68, 69]: - key = CONST.CUSTOMITEMS[x] - if customitemarray[key] > total_items_to_place: - customitemarray[key] = total_items_to_place - - # Triforce - if customitemarray["triforce"] > total_items_to_place: - customitemarray["triforce"] = total_items_to_place - # Triforce Pieces if goal in ['triforcehunt', 'trinity', 'ganonhunt']: g, t = set_default_triforce(goal, customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"]) customitemarray["triforcepiecesgoal"], customitemarray["triforcepieces"] = g, t - itemtotal = 0 - # Bow to Silver Arrows Upgrade, including Generic Keys & Rupoors - for x in [*range(0, 66 + 1), 68, 69]: - key = CONST.CUSTOMITEMS[x] - itemtotal = itemtotal + customitemarray[key] - # Triforce - itemtotal = itemtotal + customitemarray["triforce"] - # Generic Keys - itemtotal = itemtotal + customitemarray["generickeys"] - customitems = [ "Bow", "Silver Arrows", "Blue Boomerang", "Red Boomerang", "Hookshot", "Mushroom", "Magic Powder", "Fire Rod", "Ice Rod", "Bombos", "Ether", "Quake", "Lamp", "Hammer", "Shovel", "Ocarina", "Bug Catching Net", "Book of Mudora", "Cane of Somaria", "Cane of Byrna", "Cape", "Pegasus Boots", "Power Glove", "Titans Mitts", "Progressive Glove", "Flippers", "Piece of Heart", "Boss Heart Container", "Sanctuary Heart Container", "Master Sword", "Tempered Sword", "Golden Sword", "Blue Shield", "Red Shield", "Mirror Shield", "Progressive Shield", "Blue Mail", "Red Mail", "Progressive Armor", "Magic Upgrade (1/2)", "Magic Upgrade (1/4)", "Bomb Upgrade (+5)", "Bomb Upgrade (+10)", "Arrow Upgrade (+5)", "Arrow Upgrade (+10)", "Single Arrow", "Arrows (10)", "Single Bomb", "Bombs (3)", "Rupee (1)", "Rupees (5)", "Rupees (20)", "Rupees (50)", "Rupees (100)", "Rupees (300)", "Rupoor", "Blue Clock", "Green Clock", "Red Clock", "Progressive Bow", "Bombs (10)", "Triforce Piece", "Triforce" ] @@ -1175,7 +1154,6 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer and (goal in ['triforcehunt', 'trinity', 'ganonhunt']) and (customitemarray["triforce"] == 0)): extrapieces = treasure_hunt_count - customitemarray["triforcepieces"] pool.extend(['Triforce Piece'] * extrapieces) - itemtotal = itemtotal + extrapieces if timer in ['display', 'timed', 'timed-countdown']: clock_mode = 'countdown' if timer == 'timed-countdown' else 'stopwatch' @@ -1186,7 +1164,6 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer if goal in ['pedestal', 'trinity']: place_item('Master Sword Pedestal', 'Triforce') - itemtotal = itemtotal + 1 if mode == 'standard': if world.keyshuffle[player] == 'universal': @@ -1203,13 +1180,6 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer pool.extend(['Magic Mirror'] * customitemarray["mirror"]) pool.extend(['Moon Pearl'] * customitemarray["pearl"]) - if world.keyshuffle[player] == 'universal': - itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode - if itemtotal < total_items_to_place: - nothings = total_items_to_place - itemtotal -# print("Placing " + str(nothings) + " Nothings") - pool.extend(['Nothing'] * nothings) - start_inventory = [x for x in world.precollected_items if x.player == player] if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic'] and all(x.name != 'Pegasus Boots' for x in start_inventory): precollected_items.append('Pegasus Boots') diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dfef7c0b..1d41d539 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,4 +6,5 @@ - 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 multi-player HMG + - Fixed an issue limiting number of items specified in the item pool on the GUI - Minor documentation fixes (thanks Codemann!) From 07ed662309afe1393ae575046f1036d6cbf1adad Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 8 Jan 2025 12:06:52 -0700 Subject: [PATCH 10/17] fix: key logic bug with recent fix --- KeyDoorShuffle.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 0508e35d..92c820ff 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -817,7 +817,8 @@ def find_next_counter(new_door, old_counter, key_layout, prize_flag=None): prop_doors = next((item_or_tuple for item_or_tuple in key_layout.proposal if new_door == item_or_tuple or (isinstance(item_or_tuple, tuple) and new_door in item_or_tuple)), None) if prop_doors: - proposed_doors = {**old_counter.open_doors, **dict.fromkeys([prop_doors])} + prop_doors = list(prop_doors) if isinstance(prop_doors, tuple) else [prop_doors] + proposed_doors = {**old_counter.open_doors, **dict.fromkeys(prop_doors)} else: proposed_doors = {**old_counter.open_doors} bk_open = old_counter.big_key_opened or new_door.bigKey From ecbd30389e58307ab035a6e3c5bb70c30e446dc7 Mon Sep 17 00:00:00 2001 From: Kris Davie Date: Sun, 5 Jan 2025 09:31:42 +0100 Subject: [PATCH 11/17] Make sure we can actually reach clips --- UnderworldGlitchRules.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/UnderworldGlitchRules.py b/UnderworldGlitchRules.py index 0ae3cee8..fc928732 100644 --- a/UnderworldGlitchRules.py +++ b/UnderworldGlitchRules.py @@ -174,20 +174,23 @@ def dungeon_reentry_rules( def underworld_glitches_rules(world, player): def mire_clip(state): torches = world.get_region("Mire Torches Top", player) - return state.can_dash_clip(torches, player) or ( - state.can_bomb_clip(torches, player) and state.has_fire_source(player) + return state.can_reach(torches, player) and ( + state.can_dash_clip(torches, player) + or (state.can_bomb_clip(torches, player) and state.has_fire_source(player)) ) def hera_clip(state): hera = world.get_region("Hera 4F", player) - return state.can_bomb_clip(hera, player) or state.can_dash_clip(hera, player) + return state.can_reach(hera) and ( + state.can_bomb_clip(hera, player) or state.can_dash_clip(hera, player) + ) # We use these plus functool.partial because lambdas don't work in loops properly. def bomb_clip(state, region, player): - return state.can_bomb_clip(region, player) + return state.can_reach(region, player) and state.can_bomb_clip(region, player) def dash_clip(state, region, player): - return state.can_dash_clip(region, player) + return state.can_reach(region, player) and state.can_dash_clip(region, player) # Bomb clips for clip in ( kikiskip_spots From 7da70470fa4397a42cd64994ad68822089b7d750 Mon Sep 17 00:00:00 2001 From: Kris Davie Date: Sun, 5 Jan 2025 12:40:18 +0100 Subject: [PATCH 12/17] Extra logic changes and early exits @codemann8 --- UnderworldGlitchRules.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/UnderworldGlitchRules.py b/UnderworldGlitchRules.py index fc928732..235dcce8 100644 --- a/UnderworldGlitchRules.py +++ b/UnderworldGlitchRules.py @@ -48,7 +48,6 @@ def create_hmg_entrances_regions(world, player): ip_clip_entrance = Entrance(player, "Ice Bomb Drop Clip", ip_bomb_top_reg) ip_bomb_top_reg.exits.append(ip_clip_entrance) - def connect_hmg_entrances_regions(world, player): for spots in [ kikiskip_spots, @@ -174,23 +173,21 @@ def dungeon_reentry_rules( def underworld_glitches_rules(world, player): def mire_clip(state): torches = world.get_region("Mire Torches Top", player) - return state.can_reach(torches, player) and ( - state.can_dash_clip(torches, player) + return (state.can_dash_clip(torches, player) or (state.can_bomb_clip(torches, player) and state.has_fire_source(player)) - ) + ) and state.can_reach(torches, player) def hera_clip(state): hera = world.get_region("Hera 4F", player) - return state.can_reach(hera) and ( - state.can_bomb_clip(hera, player) or state.can_dash_clip(hera, player) - ) + return (state.can_bomb_clip(hera, player) or state.can_dash_clip(hera, player)) \ + and state.has("Flippers", player) and state.can_reach(hera) and mire_clip(state) # We use these plus functool.partial because lambdas don't work in loops properly. def bomb_clip(state, region, player): - return state.can_reach(region, player) and state.can_bomb_clip(region, player) + return state.can_bomb_clip(region, player) and state.can_reach(region, player) def dash_clip(state, region, player): - return state.can_reach(region, player) and state.can_dash_clip(region, player) + return state.can_dash_clip(region, player) and state.can_reach(region, player) # Bomb clips for clip in ( kikiskip_spots @@ -237,18 +234,24 @@ def underworld_glitches_rules(world, player): # Allow mire big key to be used in Hera Rules.add_rule( world.get_entrance("Hera Startile Corner NW", player), - lambda state: mire_clip(state) and state.has("Big Key (Misery Mire)", player), + lambda state: state.has("Big Key (Misery Mire)", player) and mire_clip(state), combine="or", ) Rules.add_rule( world.get_location("Tower of Hera - Big Chest", player), - lambda state: mire_clip(state) and state.has("Big Key (Misery Mire)", player), + lambda state: state.has("Big Key (Misery Mire)", player) and mire_clip(state), combine="or", ) # This uses the mire clip because it's always expected to come from mire Rules.set_rule( world.get_entrance("Hera to Swamp Clip", player), - lambda state: mire_clip(state) and state.has("Flippers", player), + lambda state: state.has("Flippers", player) and mire_clip(state), + ) + Rules.add_rule( + world.get_location("Swamp Palace - Big Chest", player), + lambda state: (state.has("Big Key (Misery Mire)", player) or state.has("Big Key (Tower of Hera)", player)) \ + and state.has("Flippers", player) and mire_clip(state), + combine="or", ) # We need to set _all_ swamp doors to be openable with mire keys, otherwise the small key can't be behind them - 6 keys because of Pots # Flippers required for all of these doors to prevent locks when flooding @@ -267,9 +270,9 @@ def underworld_glitches_rules(world, player): ]: Rules.add_rule( world.get_entrance(door, player), - lambda state: mire_clip(state) + lambda state: state.has("Flippers", player) and state.has("Small Key (Misery Mire)", player, count=6) - and state.has("Flippers", player), + and mire_clip(state), combine="or", ) @@ -297,16 +300,16 @@ def underworld_glitches_rules(world, player): ) ), } - inverted = world.mode[player] == "inverted" + inverted_dm = world.mode[player] == "inverted" def hera_rule(state): - return (state.has("Moon Pearl", player) or not inverted) and rule_map.get( + return (state.has("Moon Pearl", player) or not inverted_dm) and rule_map.get( world.get_entrance("Tower of Hera", player).connected_region.name, lambda state: False, )(state) def gt_rule(state): - return (state.has("Moon Pearl", player) or inverted) and rule_map.get( + return (state.has("Moon Pearl", player) or inverted_dm) and rule_map.get( world.get_entrance(("Ganons Tower"), player).connected_region.name, lambda state: False, )(state) @@ -315,8 +318,8 @@ def underworld_glitches_rules(world, player): return ( state.can_reach("Old Man S&Q", "Entrance", player) and state.has("Flippers", player) - and mire_clip(state) and (hera_rule(state) or gt_rule(state)) + and mire_clip(state) ) Rules.add_rule( From 6268a7e5038f78c9b554ef8c3a5aa75450b21dd8 Mon Sep 17 00:00:00 2001 From: Kris Davie Date: Wed, 8 Jan 2025 19:18:40 +0100 Subject: [PATCH 13/17] Fixed issue with 2 extra SP keys in pool for HMG (codemann) --- Fill.py | 31 +++++++++++++++++++++++++++++++ ItemList.py | 10 ---------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/Fill.py b/Fill.py index feacb10b..83f921f4 100644 --- a/Fill.py +++ b/Fill.py @@ -57,6 +57,37 @@ def fill_dungeons_restrictive(world, shuffled_locations): fill_restrictive(world, base_state, shuffled_locations, items, key_pool, True) all_state_base = world.get_all_state() + for player in range(1, world.players + 1): + if world.logic[player] == 'hybridglitches' and world.keyshuffle[i.player] in ['none', 'nearby'] \ + and world.pottery[player] not in ['none', 'cave']: + # remove 2 keys from main pool + count_to_remove = 2 + to_remove = [] + for wix, wi in enumerate(smalls): + if wi.name == 'Small Key (Swamp Palace)' and wi.player == player: + to_remove.append(wix) + if count_to_remove == len(to_remove): + break + for wix in reversed(to_remove): + del smalls[wix] + # remove 2 swamp locations from pool + hybrid_locations = [] + to_remove = [] + for i, loc in enumerate(shuffled_locations): + if loc.name in ['Swamp Palace - Trench 1 Pot Key', 'Swamp Palace - Pot Row Pot Key'] and loc.player == player: + to_remove.append(i) + hybrid_locations.append(loc) + if count_to_remove == len(to_remove): + break + for i in reversed(to_remove): + shuffled_locations.pop(i) + # place 2 HMG keys + hybrid_state_base = all_state_base.copy() + for x in bigs + smalls + prizes + others: + hybrid_state_base.collect(x, True) + hybrid_smalls = [ItemFactory('Small Key (Swamp Palace)', player)] * 2 + fill(hybrid_state_base, hybrid_smalls, hybrid_locations, unplaced_smalls) + big_state_base = all_state_base.copy() for x in smalls + others: big_state_base.collect(x, True) diff --git a/ItemList.py b/ItemList.py index 56b394b1..add69174 100644 --- a/ItemList.py +++ b/ItemList.py @@ -368,16 +368,6 @@ def generate_itempool(world, player): or (item.map and world.mapshuffle[player]) or (item.compass and world.compassshuffle[player]))]) - if world.logic[player] == 'hybridglitches' and world.pottery[player] not in ['none', 'cave']: - keys_to_remove = 2 - to_remove = [] - for wix, wi in enumerate(world.itempool): - if wi.name == 'Small Key (Swamp Palace)' and wi.player == player: - to_remove.append(wix) - if keys_to_remove == len(to_remove): - break - for wix in reversed(to_remove): - del world.itempool[wix] # logic has some branches where having 4 hearts is one possible requirement (of several alternatives) # rather than making all hearts/heart pieces progression items (which slows down generation considerably) From 5664526e0cb953f9d3f1a86bbfe2728199d45913 Mon Sep 17 00:00:00 2001 From: Kris Davie Date: Sun, 12 Jan 2025 22:20:54 +0100 Subject: [PATCH 14/17] Restore native dungeon item behavior for glitched modes. baserom handles dungeon IDs --- Rom.py | 6 +++--- source/dungeon/EnemyList.py | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Rom.py b/Rom.py index 2d666de2..4fd43122 100644 --- a/Rom.py +++ b/Rom.py @@ -434,7 +434,9 @@ def patch_rom(world, rom, player, team, is_mystery=False): if location.item.name in valid_pot_items and location.item.player == player: location.pot.item = valid_pot_items[location.item.name] else: - code = handle_native_dungeon(location, itemid) + code = itemid + if world.pottery[player] == 'none' or location.locked: + code = handle_native_dungeon(location, itemid) standing_item_flag = 0x80 if location.item.player != player: standing_item_flag |= 0x40 @@ -452,8 +454,6 @@ def patch_rom(world, rom, player, team, is_mystery=False): if not location.crystal: if location.item is not None: - # Keys in their native dungeon should use the original item code for keys - itemid = handle_native_dungeon(location, itemid) if world.remote_items[player]: itemid = list(location_table.keys()).index(location.name) + 1 assert itemid < 0x100 diff --git a/source/dungeon/EnemyList.py b/source/dungeon/EnemyList.py index 8f168a8d..a23bf525 100644 --- a/source/dungeon/EnemyList.py +++ b/source/dungeon/EnemyList.py @@ -557,7 +557,9 @@ class Sprite(object): item_id = self.location.item.code if self.location.item is not None else 0x5A code = 0xF9 if self.location.item.player != self.location.player else 0xF8 if code == 0xF8: - item_id = handle_native_dungeon(self.location, item_id) + world = self.location.parent_region.world + if world.dropshuffle[self.location.player] == 'none' or self.location.locked: + item_id = handle_native_dungeon(self.location, item_id) data.append(item_id) data.append(0 if code == 0xF8 else self.location.item.player) data.append(code) From 996462d62325f62718093b57053ed09649043dd4 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 15 Jan 2025 08:51:41 -0700 Subject: [PATCH 15/17] fix: enemy drops in TR Dark Ride need to require Somaria --- RELEASENOTES.md | 2 ++ source/dungeon/EnemyList.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1d41d539..6792d595 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,6 +5,8 @@ - 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!) + - Fixed an issue with enemies in TR Dark Ride room not requiring Somaria + - More HMG fixes by Muffins - Fixed an issue with multi-player HMG - Fixed an issue limiting number of items specified in the item pool on the GUI - Minor documentation fixes (thanks Codemann!) diff --git a/source/dungeon/EnemyList.py b/source/dungeon/EnemyList.py index a23bf525..2ce45a26 100644 --- a/source/dungeon/EnemyList.py +++ b/source/dungeon/EnemyList.py @@ -1653,9 +1653,9 @@ def init_vanilla_sprites(): create_sprite(0x00b3, EnemySprite.Beamos, 0x00, 0, 0x06, 0x18, 'Mire Spikes') create_sprite(0x00b3, EnemySprite.FourWayShooter, 0x00, 0, 0x0a, 0x1a, 'Mire Spikes') create_sprite(0x00b3, EnemySprite.Stalfos, 0x00, 0, 0x07, 0x1c, 'Mire Spikes') - create_sprite(0x00b5, EnemySprite.FirebarCW, 0x00, 0, 0x16, 0x0a, 'TR Dark Ride') - create_sprite(0x00b5, EnemySprite.FirebarCW, 0x00, 0, 0x09, 0x0f, 'TR Dark Ride') - create_sprite(0x00b5, EnemySprite.FirebarCW, 0x00, 0, 0x16, 0x16, 'TR Dark Ride') + create_sprite(0x00b5, EnemySprite.FirebarCW, 0x00, 0, 0x16, 0x0a, 'TR Dark Ride Ledges') + create_sprite(0x00b5, EnemySprite.FirebarCW, 0x00, 0, 0x09, 0x0f, 'TR Dark Ride Ledges') + create_sprite(0x00b5, EnemySprite.FirebarCW, 0x00, 0, 0x16, 0x16, 'TR Dark Ride Ledges') create_sprite(0x00b6, EnemySprite.Chainchomp, 0x00, 0, 0x06, 0x07, 'TR Chain Chomps Top') create_sprite(0x00b6, EnemySprite.Chainchomp, 0x00, 0, 0x0a, 0x07, 'TR Chain Chomps Top') create_sprite(0x00b6, EnemySprite.CrystalSwitch, 0x00, 0, 0x03, 0x04) From 9f55e90b7eaf8c6f10b0e3f086c7b3da48c33370 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 15 Jan 2025 10:37:02 -0700 Subject: [PATCH 16/17] refactor: TR Dark Ride model --- DoorShuffle.py | 6 ++++-- Dungeons.py | 3 ++- RELEASENOTES.md | 2 +- Regions.py | 5 +++-- Rules.py | 20 +++++++++++--------- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/DoorShuffle.py b/DoorShuffle.py index e22a6822..0369ff5c 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -3862,8 +3862,10 @@ logical_connections = [ ('TR Crystaroller Chest to Middle Barrier - Blue', 'TR Crystaroller Middle'), ('TR Crystaroller Middle Ranged Crystal Exit', 'TR Crystaroller Middle'), ('TR Crystaroller Bottom Ranged Crystal Exit', 'TR Crystaroller Bottom'), - ('TR Dark Ride Path', 'TR Dark Ride Ledges'), - ('TR Dark Ride Ledges Path', 'TR Dark Ride'), + ('TR Dark Ride Normal Path', 'TR Dark Ride South Platform'), + ('TR Dark Ride Backward Path', 'TR Dark Ride North Platform'), + ('TR Dark Ride Ledge Path', 'TR Dark Ride Ledges'), + ('TR Dark Ride Return Path', 'TR Dark Ride South Platform'), ('TR Crystal Maze Start to Interior Barrier - Blue', 'TR Crystal Maze Interior'), ('TR Crystal Maze Start to Crystal', 'TR Crystal Maze Start - Crystal'), ('TR Crystal Maze Start Crystal Exit', 'TR Crystal Maze Start'), diff --git a/Dungeons.py b/Dungeons.py index 3284617e..183aaea4 100644 --- a/Dungeons.py +++ b/Dungeons.py @@ -180,7 +180,8 @@ tr_regions = [ 'TR Big View', 'TR Big Chest', 'TR Big Chest Entrance', 'TR Lazy Eyes', 'TR Dash Room', 'TR Tongue Pull', 'TR Rupees', 'TR Crystaroller Bottom', 'TR Crystaroller Middle', 'TR Crystaroller Top', 'TR Crystaroller Top - Crystal', 'TR Crystaroller Chest', 'TR Crystaroller Middle - Ranged Crystal', - 'TR Crystaroller Bottom - Ranged Crystal', 'TR Dark Ride', 'TR Dark Ride Ledges', 'TR Dash Bridge', 'TR Eye Bridge', + 'TR Crystaroller Bottom - Ranged Crystal', 'TR Dark Ride North Platform', 'TR Dark Ride South Platform', + 'TR Dark Ride Ledges', 'TR Dash Bridge', 'TR Eye Bridge', 'TR Crystal Maze Start', 'TR Crystal Maze Start - Crystal', 'TR Crystal Maze Interior', 'TR Crystal Maze End', 'TR Crystal Maze End - Ranged Crystal', 'TR Final Abyss Balcony', 'TR Final Abyss Ledge', 'TR Boss', 'Turtle Rock Main Portal', 'Turtle Rock Lazy Eyes Portal', 'Turtle Rock Chest Portal', diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6792d595..ae498523 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -5,7 +5,7 @@ - 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!) - - Fixed an issue with enemies in TR Dark Ride room not requiring Somaria + - Fixed an issue with enemies in TR Dark Ride room not requiring Somaria. (Refactored the room for decoupled logic better) - More HMG fixes by Muffins - Fixed an issue with multi-player HMG - Fixed an issue limiting number of items specified in the item pool on the GUI diff --git a/Regions.py b/Regions.py index 6cb44847..69440afa 100644 --- a/Regions.py +++ b/Regions.py @@ -889,8 +889,9 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'TR Crystaroller Chest', 'Turtle Rock', ['Turtle Rock - Crystaroller Room'], ['TR Crystaroller Chest to Middle Barrier - Blue']), create_dungeon_region(player, 'TR Crystaroller Middle - Ranged Crystal', 'Turtle Rock', None, ['TR Crystaroller Middle Ranged Crystal Exit']), create_dungeon_region(player, 'TR Crystaroller Bottom - Ranged Crystal', 'Turtle Rock', None, ['TR Crystaroller Bottom Ranged Crystal Exit']), - create_dungeon_region(player, 'TR Dark Ride', 'Turtle Rock', None, ['TR Dark Ride Up Stairs', 'TR Dark Ride SW', 'TR Dark Ride Path']), - create_dungeon_region(player, 'TR Dark Ride Ledges', 'Turtle Rock', None, ['TR Dark Ride Ledges Path']), + create_dungeon_region(player, 'TR Dark Ride North Platform', 'Turtle Rock', None, ['TR Dark Ride Up Stairs', 'TR Dark Ride Normal Path', 'TR Dark Ride Ledge Path']), + create_dungeon_region(player, 'TR Dark Ride South Platform', 'Turtle Rock', None, ['TR Dark Ride SW', 'TR Dark Ride Backward Path']), + create_dungeon_region(player, 'TR Dark Ride Ledges', 'Turtle Rock', None, ['TR Dark Ride Return Path']), create_dungeon_region(player, 'TR Dash Bridge', 'Turtle Rock', None, ['TR Dash Bridge NW', 'TR Dash Bridge SW', 'TR Dash Bridge WS']), create_dungeon_region(player, 'TR Eye Bridge', 'Turtle Rock', ['Turtle Rock - Eye Bridge - Bottom Left', 'Turtle Rock - Eye Bridge - Bottom Right', 'Turtle Rock - Eye Bridge - Top Left', 'Turtle Rock - Eye Bridge - Top Right'], diff --git a/Rules.py b/Rules.py index fcd475fa..3600f11c 100644 --- a/Rules.py +++ b/Rules.py @@ -561,10 +561,11 @@ def global_rules(world, player): lambda state: state.has('Cane of Somaria', player) and state.has('Fire Rod', player)) set_rule(world.get_entrance('TR Big Chest Entrance Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has('Hookshot', player)) set_rule(world.get_entrance('TR Big Chest Gap', player), lambda state: state.has('Cane of Somaria', player) or state.has_Boots(player)) - set_rule(world.get_entrance('TR Dark Ride Up Stairs', player), lambda state: state.has('Cane of Somaria', player)) - set_rule(world.get_entrance('TR Dark Ride SW', player), lambda state: state.has('Cane of Somaria', player)) - set_rule(world.get_entrance('TR Dark Ride Path', player), lambda state: state.has('Cane of Somaria', player)) - set_rule(world.get_entrance('TR Dark Ride Ledges Path', player), lambda state: state.has('Cane of Somaria', player)) + set_rule(world.get_entrance('TR Dark Ride SW', player), lambda state: state.has('Cane of Somaria', player)) # due to needing the switch + set_rule(world.get_entrance('TR Dark Ride Normal Path', player), lambda state: state.has('Cane of Somaria', player)) + set_rule(world.get_entrance('TR Dark Ride Backward Path', player), lambda state: state.has('Cane of Somaria', player)) + set_rule(world.get_entrance('TR Dark Ride Return Path', player), lambda state: state.has('Cane of Somaria', player)) + set_rule(world.get_entrance('TR Dark Ride Ledge Path', player), lambda state: state.has('Cane of Somaria', player)) for location in world.get_region('TR Dark Ride Ledges', player).locations: set_rule(location, lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('TR Final Abyss Balcony Path', player), lambda state: state.has('Cane of Somaria', player)) @@ -1305,8 +1306,9 @@ def add_conditional_lamps(world, player): add_lamp_requirement(spot, player) dark_rooms = { - 'TR Dark Ride': {'sewer': False, 'entrances': ['TR Dark Ride Up Stairs', 'TR Dark Ride SW', 'TR Dark Ride Path'], 'locations': []}, - 'TR Dark Ride Ledges': {'sewer': False, 'entrances': ['TR Dark Ride Ledges Path'], 'locations': []}, + 'TR Dark Ride North Platform': {'sewer': False, 'entrances': ['TR Dark Ride Up Stairs', 'TR Dark Ride Normal Path', 'TR Dark Ride Ledge Path'], 'locations': []}, + 'TR Dark Ride South Platform': {'sewer': False, 'entrances': ['TR Dark Ride SW', 'TR Dark Ride Backward Path'], 'locations': []}, + 'TR Dark Ride Ledges': {'sewer': False, 'entrances': ['TR Dark Ride Return Path'], 'locations': []}, 'Mire Dark Shooters': {'sewer': False, 'entrances': ['Mire Dark Shooters Up Stairs', 'Mire Dark Shooters SW', 'Mire Dark Shooters SE'], 'locations': []}, 'Mire Key Rupees': {'sewer': False, 'entrances': ['Mire Key Rupees NE'], 'locations': []}, 'Mire Block X': {'sewer': False, 'entrances': ['Mire Block X NW', 'Mire Block X WS'], 'locations': []}, @@ -2230,7 +2232,7 @@ bunny_revivable_entrances = { "Ice Many Pots", "Mire South Fish", "Mire Right Bridge", "Mire Left Bridge", "TR Boss", "Eastern Hint Tile Blocked Path", "Thieves Spike Switch", "Thieves Boss", "Mire Spike Barrier", "Mire Cross", "Mire Hidden Shooters", - "Mire Spikes", "TR Final Abyss Balcony", "TR Dark Ride", "TR Pokey 1", "TR Tile Room", + "Mire Spikes", "TR Final Abyss Balcony", "TR Dark Ride South Platform", "TR Pokey 1", "TR Tile Room", "TR Roller Room", "Eastern Cannonball", "Thieves Hallway", "Ice Switch Room", "Mire Tile Room", "Mire Conveyor Crystal", "Mire Hub", "TR Dash Bridge", "TR Hub", "Eastern Boss", "Eastern Lobby", "Thieves Ambush", @@ -2298,8 +2300,8 @@ bunny_impassible_doors = { 'TR Lobby Ledge Gap', 'TR Hub SW', 'TR Hub SE', 'TR Hub ES', 'TR Hub EN', 'TR Hub NW', 'TR Hub NE', 'TR Hub Path', 'TR Hub Ledges Path', 'TR Torches NW', 'TR Pokey 2 Bottom to Top Barrier - Blue', 'TR Pokey 2 Top to Bottom Barrier - Blue', 'TR Twin Pokeys SW', 'TR Twin Pokeys EN', 'TR Big Chest Gap', - 'TR Big Chest Entrance Gap', 'TR Lazy Eyes ES', 'TR Tongue Pull WS', 'TR Tongue Pull NE', 'TR Dark Ride Up Stairs', - 'TR Dark Ride SW', 'TR Dark Ride Path', 'TR Dark Ride Ledges Path', + 'TR Big Chest Entrance Gap', 'TR Lazy Eyes ES', 'TR Tongue Pull WS', 'TR Tongue Pull NE', 'TR Dark Ride SW', # due to needing the switch + 'TR Dark Ride Normal Path', 'TR Dark Ride Ledge Path', 'TR Dark Ride Backward Path', 'TR Dark Ride Return Path', 'TR Crystal Maze Start to Interior Barrier - Blue', 'TR Crystal Maze End to Interior Barrier - Blue', 'TR Final Abyss Balcony Path', 'TR Final Abyss Ledge Path', 'GT Hope Room EN', 'GT Blocked Stairs Block Path', 'GT Bob\'s Room Hole', 'GT Speed Torch SE', 'GT Speed Torch South Path', 'GT Speed Torch North Path', From c7a57d63fa0cd2e27f342508c50d6ca0d8dc0863 Mon Sep 17 00:00:00 2001 From: aerinon Date: Wed, 15 Jan 2025 14:32:07 -0700 Subject: [PATCH 17/17] 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 @@ class World(object): 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 parse_cli(argv, no_defaults=False): '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 balance_money_progression(world): 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 balance_money_progression(world): 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 @@ class CustomSettings(object): 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 @@ class CustomSettings(object): 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 @@ class CustomSettings(object): 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 @@ class CustomSettings(object): 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 @@ class CustomSettings(object): 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]