diff --git a/BaseClasses.py b/BaseClasses.py index 532daced..edef87bd 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2672,6 +2672,9 @@ class Item(object): def __unicode__(self): return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})' + def __eq__(self, other): + return self.name == other.name and self.player == other.player + # have 6 address that need to be filled class Crystal(Item): diff --git a/DoorShuffle.py b/DoorShuffle.py index 3ad19a54..b2a2df6c 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -2108,7 +2108,7 @@ def find_trappable_candidates(builder, world, player): if ext.door and ext.door.type in [DoorType.Interior, DoorType.Normal]] for d in filtered_doors: # I only support the first 3 due to the trapFlag right now - if 0 <= d.doorListPos < 3 and not d.entranceFlag: + if 0 <= d.doorListPos < 3 and not d.entranceFlag and d.name != 'Skull Small Hall WS': room = world.get_room(d.roomIndex, player) kind = room.kind(d) if d.type == DoorType.Interior: @@ -4250,7 +4250,7 @@ default_door_connections = [ ('Eastern Map Valley SW', 'Eastern Dark Square NW'), ('Eastern Attic Start WS', 'Eastern False Switches ES'), ('Eastern Cannonball Hell WS', 'Eastern Single Eyegore ES'), - ('Desert Compass NW', 'Desert Cannonball S'), + ('Desert Compass NE', 'Desert Cannonball S'), ('Desert Beamos Hall NE', 'Desert Tiles 2 SE'), ('PoD Middle Cage N', 'PoD Pit Room S'), ('PoD Pit Room NW', 'PoD Arena Main SW'), diff --git a/Doors.py b/Doors.py index 6627a345..7c9e5ca6 100644 --- a/Doors.py +++ b/Doors.py @@ -207,7 +207,7 @@ def create_doors(world, player): create_door(player, 'Desert East Wing ES', Intr).dir(Ea, 0x85, Bot, High).pos(3), create_door(player, 'Desert East Wing Key Door EN', Intr).dir(Ea, 0x85, Top, High).small_key().pos(1), create_door(player, 'Desert Compass Key Door WN', Intr).dir(We, 0x85, Top, High).small_key().pos(1), - create_door(player, 'Desert Compass NW', Nrml).dir(No, 0x85, Right, High).trap(0x4).pos(0), + create_door(player, 'Desert Compass NE', Nrml).dir(No, 0x85, Right, High).trap(0x4).pos(0), create_door(player, 'Desert Cannonball S', Nrml).dir(So, 0x75, Right, High).pos(1).portal(X, 0x02), create_door(player, 'Desert Arrow Pot Corner S Edge', Open).dir(So, 0x75, None, High).edge(6, Z, 0x20), create_door(player, 'Desert Arrow Pot Corner W Edge', Open).dir(We, 0x75, None, High).edge(2, Z, 0x20), diff --git a/ItemList.py b/ItemList.py index e7a21160..a07d2b2b 100644 --- a/ItemList.py +++ b/ItemList.py @@ -306,6 +306,7 @@ def generate_itempool(world, player): for _ in range(0, amt): pool.append('Rupees (20)') + start_inventory = list(world.precollected_items) for item in precollected_items: world.push_precollected(ItemFactory(item, player)) @@ -459,6 +460,22 @@ def generate_itempool(world, player): create_dynamic_bonkdrop_locations(world, player) add_bonkdrop_contents(world, player) + # modfiy based on start inventory, if any + modify_pool_for_start_inventory(start_inventory, world, player) + + # increase pool if not enough items + ttl_locations = sum(1 for x in world.get_unfilled_locations(player) if '- Prize' not in x.name) + pool_size = count_player_dungeon_item_pool(world, player) + pool_size += sum(1 for x in world.itempool if x.player == player) + + if pool_size < ttl_locations: + retro_bow = world.bow_mode[player].startswith('retro') + amount_to_add = ttl_locations - pool_size + filler_additions = random.choices(list(filler_items.keys()), filler_items.values(), k=amount_to_add) + for item in filler_additions: + item_name = 'Rupees (5)' if retro_bow and item == 'Arrows (10)' else item + world.itempool.append(ItemFactory(item_name, player)) + take_any_locations = [ 'Snitch Lady (East)', 'Snitch Lady (West)', 'Bush Covered House', 'Light World Bomb Hut', @@ -1123,14 +1140,60 @@ def get_pool_core(world, player, progressive, shuffle, difficulty, treasure_hunt pool.extend(['Small Key (Universal)']) else: pool.extend(['Small Key (Universal)']) - modify_pool_for_start_inventory(pool, world, player) return (pool, placed_items, precollected_items, clock_mode, lamps_needed_for_dark_rooms) -def modify_pool_for_start_inventory(pool, world, player): - for item in world.precollected_items: +item_alternates = { + # Bows + 'Progressive Bow (Alt)': ('Progressive Bow', 1), + 'Bow': ('Progressive Bow', 1), + 'Silver Arrows': ('Progressive Bow', 2), + # Gloves + 'Power Glove': ('Progressive Glove', 1), + 'Titans Mitts': ('Progressive Glove', 2), + # Swords + 'Sword and Shield': ('Progressive Sword', 1), # could find a way to also remove a shield, but mostly not impactful + 'Fighter Sword': ('Progressive Sword', 1), + 'Master Sword': ('Progressive Sword', 2), + 'Tempered Sword': ('Progressive Sword', 3), + 'Golden Sword': ('Progressive Sword', 4), + # Shields + 'Blue Shield': ('Progressive Shield', 1), + 'Red Shield': ('Progressive Shield', 2), + 'Mirror Shield': ('Progressive Shield', 3), + # Armors + 'Blue Mail': ('Progressive Armor', 1), + 'Red Mail': ('Progressive Armor', 2), + + 'Magic Upgrade (1/4)': ('Magic Upgrade (1/2)', 2), + 'Ocarina': ('Ocarina (Activated)', 1), + 'Ocarina (Activated)': ('Ocarina', 1), + 'Boss Heart Container': ('Sanctuary Heart Container', 1), + 'Sanctuary Heart Container': ('Boss Heart Container', 1), + 'Power Star': ('Triforce Piece', 1) +} + + +def modify_pool_for_start_inventory(start_inventory, world, player): + # skips custom item pools - these shouldn't be adjusted + if (world.customizer and world.customizer.get_item_pool()) or world.custom: + return + for item in start_inventory: if item.player == player: - pool.remove(item.name) + if item in world.itempool: + world.itempool.remove(item) + elif item.name in item_alternates: + alt = item_alternates[item.name] + i = alt[1] + while i > 0: + alt_item = ItemFactory([alt[0]], player)[0] + if alt_item in world.itempool: + world.itempool.remove(alt_item) + i = i-1 + elif 'Bottle' in item.name: + bottle_item = next((x for x in world.itempool if 'Bottle' in item.name and x.player == player), None) + if bottle_item is not None: + world.itempool.remove(bottle_item) if item.dungeon: d = world.get_dungeon(item.dungeon, item.player) match = next((i for i in d.all_items if i.name == item.name), None) @@ -1245,6 +1308,22 @@ def make_custom_item_pool(world, player, progressive, shuffle, difficulty, timer # print("Placing " + str(nothings) + " Nothings") pool.extend(['Nothing'] * nothings) + start_inventory = [x for x in world.precollected_items if x.player == player] + if not start_inventory: + if world.logic[player] in ['owglitches', 'nologic']: + precollected_items.append('Pegasus Boots') + if 'Pegasus Boots' in pool: + pool.remove('Pegasus Boots') + pool.append('Rupees (20)') + if world.swords[player] == 'assured': + precollected_items.append('Progressive Sword') + if 'Progressive Sword' in pool: + pool.remove('Progressive Sword') + pool.append('Rupees (50)') + elif 'Fighter Sword' in pool: + pool.remove('Fighter Sword') + pool.append('Rupees (50)') + return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_total, treasure_hunt_icon, lamps_needed_for_dark_rooms) def set_default_triforce(goal, custom_goal, custom_total): @@ -1378,12 +1457,20 @@ def make_customizer_pool(world, player): if pieces < t: pool.extend(['Triforce Piece'] * (t - pieces)) - ttl_locations = sum(1 for x in world.get_unfilled_locations(player) if '- Prize' not in x.name) - pool_size = len(get_player_dungeon_item_pool(world, player)) + len(pool) - - if pool_size < ttl_locations: - amount_to_add = ttl_locations - pool_size - pool.extend(random.choices(list(filler_items.keys()), filler_items.values(), k=amount_to_add)) + if not world.customizer.get_start_inventory(): + if world.logic[player] in ['owglitches', 'nologic']: + precollected_items.append('Pegasus Boots') + if 'Pegasus Boots' in pool: + pool.remove('Pegasus Boots') + pool.append('Rupees (20)') + if world.swords[player] == 'assured': + precollected_items.append('Progressive Sword') + if 'Progressive Sword' in pool: + pool.remove('Progressive Sword') + pool.append('Rupees (50)') + elif 'Fighter Sword' in pool: + pool.remove('Fighter Sword') + pool.append('Rupees (50)') return pool, placed_items, precollected_items, clock_mode, 1 @@ -1399,9 +1486,9 @@ filler_items = { } -def get_player_dungeon_item_pool(world, player): - return [item for dungeon in world.dungeons for item in dungeon.all_items - if dungeon.player == player and item.location is None] +def count_player_dungeon_item_pool(world, player): + return sum(1 for dungeon in world.dungeons for item in dungeon.all_items + if dungeon.player == player and item.location is None and is_dungeon_item(item.name, world, player)) # location pool doesn't support larger values at this time diff --git a/Main.py b/Main.py index f584b791..b7af0523 100644 --- a/Main.py +++ b/Main.py @@ -35,7 +35,7 @@ from source.overworld.EntranceShuffle2 import link_entrances_new from source.tools.BPS import create_bps_from_data from source.classes.CustomSettings import CustomSettings -__version__ = '1.2.0.3-u' +__version__ = '1.2.0.4-u' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 21f4a884..54b2edb0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -108,7 +108,11 @@ These are now independent of retro mode and have three options: None, Random, an * Bonk Fairy (Dark) # Bug Fixes and Notes - +* 1.2.0.4-u + * Starting inventory fixes if item not present in the item pool. + * Support for Assured sword setting and OWG Boots when using a custom item pool. (Customizer or GUI) + * Logic fix for the skull woods star tile that lets you into the X pot room. Now accounts for small key or big key door there blocking the way from the star tile. A trap door is not allowed there. + * Standard logic improvement that requires a path from Zelda to the start so that you cannot get softlocked by rescuing Zelda. Standard mirror scroll change may need to be reverted if impossible seed are still generated. * 1.2.0.3-u * Starting inventory taken into account with default item pool. (Custom pools must do this themselves) * Fast ROM update diff --git a/Regions.py b/Regions.py index 261b0113..5d3ff3d6 100644 --- a/Regions.py +++ b/Regions.py @@ -475,7 +475,7 @@ def create_dungeon_regions(world, player): create_dungeon_region(player, 'Desert Dead End', 'Desert Palace', None, ['Desert Dead End Edge']), create_dungeon_region(player, 'Desert East Lobby', 'Desert Palace', None, ['Desert East Lobby WS', 'Desert East Lobby S']), create_dungeon_region(player, 'Desert East Wing', 'Desert Palace', None, ['Desert East Wing ES', 'Desert East Wing Key Door EN', 'Desert East Wing W Edge', 'Desert East Wing N Edge']), - create_dungeon_region(player, 'Desert Compass Room', 'Desert Palace', ['Desert Palace - Compass Chest'], ['Desert Compass Key Door WN', 'Desert Compass NW']), + create_dungeon_region(player, 'Desert Compass Room', 'Desert Palace', ['Desert Palace - Compass Chest'], ['Desert Compass Key Door WN', 'Desert Compass NE']), create_dungeon_region(player, 'Desert Cannonball', 'Desert Palace', ['Desert Palace - Big Key Chest'], ['Desert Cannonball S']), create_dungeon_region(player, 'Desert Arrow Pot Corner', 'Desert Palace', None, ['Desert Arrow Pot Corner S Edge', 'Desert Arrow Pot Corner W Edge', 'Desert Arrow Pot Corner NW']), create_dungeon_region(player, 'Desert Trap Room', 'Desert Palace', None, ['Desert Trap Room SW']), diff --git a/Rom.py b/Rom.py index cf54c71d..03d10639 100644 --- a/Rom.py +++ b/Rom.py @@ -2338,7 +2338,7 @@ def write_strings(rom, world, player, team): hint_candidates = [] for name, district in world.districts[player].items(): hint_type = 'foolish' - choice_set = set() + choices = [] item_count, item_type = 0, 'useful' for loc_name in district.locations: location_item = world.get_location(loc_name, player).item @@ -2348,34 +2348,32 @@ def write_strings(rom, world, player, team): itm_type = 'useful' if useful_item_for_hint(location_item, world) else 'vital' hint_type = 'path' if item_type == itm_type: - choice_set.add(location_item) + choices.append(location_item) item_count += 1 elif itm_type == 'vital': item_type = 'vital' item_count = 1 - choice_set.clear() - choice_set.add(location_item) + choices.clear() + choices.append(location_item) if hint_type == 'foolish': if district.dungeons and world.shuffle[player] != 'vanilla': - choice_set.update(district.dungeons) + choices.extend(district.dungeons) hint_type = 'dungeon_path' elif district.access_points and world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull']: - choice_set.update([x.hint_text for x in district.access_points]) + choices.extend([x.hint_text for x in district.access_points]) hint_type = 'connector' if hint_type == 'foolish': hint_candidates.append((hint_type, f'{name} is a foolish choice')) elif hint_type == 'dungeon_path': - choices = sorted(list(choice_set)) dungeon_choice = random.choice(choices) # prefer required dungeons... hint_candidates.append((hint_type, f'{name} is on the path to {dungeon_choice}')) elif hint_type == 'connector': - choices = sorted(list(choice_set)) access_point = random.choice(choices) # prefer required access... hint_candidates.append((hint_type, f'{name} can reach {access_point}')) elif hint_type == 'path': if item_count == 1: - the_item = text_for_item(next(iter(choice_set)), world, player, team) + the_item = text_for_item(next(iter(choices)), world, player, team) hint_candidates.append((hint_type, f'{name} conceals only {the_item}')) else: hint_candidates.append((hint_type, f'{name} conceals {item_count} {item_type} items')) diff --git a/Rules.py b/Rules.py index 69b7fed2..3fe7e43e 100644 --- a/Rules.py +++ b/Rules.py @@ -147,6 +147,10 @@ def or_rule(rule1, rule2): return lambda state: rule1(state) or rule2(state) +def and_rule(rule1, rule2): + return lambda state: rule1(state) and rule2(state) + + def add_lamp_requirement(spot, player): add_rule(spot, lambda state: state.has('Lamp', player, state.world.lamps_needed_for_dark_rooms)) @@ -318,8 +322,22 @@ def global_rules(world, player): set_rule(world.get_entrance('Skull Big Chest Hookpath', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Skull Torch Room WN', player), lambda state: state.has('Fire Rod', player)) set_rule(world.get_entrance('Skull Vines NW', player), lambda state: state.has_sword(player)) - set_rule(world.get_entrance('Skull 2 West Lobby Pits', player), lambda state: state.has_Boots(player) or state.has('Hidden Pits', player)) - set_rule(world.get_entrance('Skull 2 West Lobby Ledge Pits', player), lambda state: state.has('Hidden Pits', player)) + + hidden_pits_door = world.get_door('Skull Small Hall WS', player) + + def hidden_pits_rule(state): + return state.has('Hidden Pits', player) + + if hidden_pits_door.bigKey: + key_logic = world.key_logic[player][hidden_pits_door.entrance.parent_region.dungeon.name] + hidden_pits_rule = and_rule(hidden_pits_rule, create_rule(key_logic.bk_name, player)) + elif hidden_pits_door.smallKey: + d_name = hidden_pits_door.entrance.parent_region.dungeon.name + hidden_pits_rule = and_rule(hidden_pits_rule, eval_small_key_door('Skull Small Hall WS', d_name, player)) + + set_rule(world.get_entrance('Skull 2 West Lobby Pits', player), lambda state: state.has_Boots(player) + or hidden_pits_rule(state)) + set_rule(world.get_entrance('Skull 2 West Lobby Ledge Pits', player), hidden_pits_rule) set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Boss', player)) set_defeat_dungeon_boss_rule(world.get_location('Skull Woods - Prize', player)) @@ -696,7 +714,7 @@ def bomb_rules(world, player): ('Hyrule Dungeon Armory S', True), # One green guard ('Hyrule Dungeon Armory ES', True), # One green guard ('Hyrule Dungeon Armory Boomerang WS', True), # One blue guard - ('Desert Compass NW', True), # Three popos + ('Desert Compass NE', True), # Three popos ('Desert Four Statues NW', True), # Four popos ('Desert Four Statues ES', True), # Four popos ('Hera Beetles WS', False), # Three blue beetles and only two pots, and bombs don't work. @@ -1390,7 +1408,7 @@ std_kill_rooms = { 'Hyrule Dungeon Armory Main': ['Hyrule Dungeon Armory S', 'Hyrule Dungeon Armory ES'], # One green guard 'Hyrule Dungeon Armory Boomerang': ['Hyrule Dungeon Armory Boomerang WS'], # One blue guard 'Eastern Stalfos Spawn': ['Eastern Stalfos Spawn ES', 'Eastern Stalfos Spawn NW'], # Can use pots - 'Desert Compass Room': ['Desert Compass NW'], # Three popos + 'Desert Compass Room': ['Desert Compass NE'], # Three popos 'Desert Four Statues': ['Desert Four Statues NW', 'Desert Four Statues ES'], # Four popos 'Hera Beetles': ['Hera Beetles WS'], # Three blue beetles and only two pots, and bombs don't work. 'Tower Gold Knights': ['Tower Gold Knights SW', 'Tower Gold Knights EN'], # Two ball and chain @@ -1735,7 +1753,7 @@ bunny_impassible_doors = { 'Eastern Map Balcony Hook Path', 'Eastern Stalfos Spawn ES', 'Eastern Stalfos Spawn NW', 'Eastern Darkness S', 'Eastern Darkness NE', 'Eastern Darkness Up Stairs', 'Eastern Attic Start WS', 'Eastern Single Eyegore NE', 'Eastern Duo Eyegores NE', 'Desert Main Lobby Left Path', - 'Desert Main Lobby Right Path', 'Desert Left Alcove Path', 'Desert Right Alcove Path', 'Desert Compass NW', + 'Desert Main Lobby Right Path', 'Desert Left Alcove Path', 'Desert Right Alcove Path', 'Desert Compass NE', 'Desert West Lobby NW', 'Desert Back Lobby NW', 'Desert Four Statues NW', 'Desert Four Statues ES', 'Desert Beamos Hall WS', 'Desert Beamos Hall NE', 'Desert Wall Slide NW', 'Hera Lobby to Front Barrier - Blue', 'Hera Front to Lobby Barrier - Blue', 'Hera Front to Down Stairs Barrier - Blue', diff --git a/docs/presets/async_doors_league/S3_BombBag.yaml b/docs/presets/async_doors_league/S3_BombBag.yaml index e1f831f6..9edc1803 100644 --- a/docs/presets/async_doors_league/S3_BombBag.yaml +++ b/docs/presets/async_doors_league/S3_BombBag.yaml @@ -2,22 +2,23 @@ meta: players: 1 race: true settings: - shopsanity: true - pseudoboots: true - goal: crystals - crystals_gt: random - bombbag: true - shuffle: crossed - shufflelinks: true - keysanity: true - door_shuffle: crossed - intensity: 3 - door_type_mode: big - pottery: keys - dropshuffle: true - experimental: true - dungeon_counters: 'on' - hints: true - msu_resume: true - collection_rate: true - quickswap: true + 1: + shopsanity: true + pseudoboots: true + goal: crystals + crystals_gt: random + bombbag: true + shuffle: crossed + shufflelinks: true + keysanity: true + door_shuffle: crossed + intensity: 3 + door_type_mode: big + pottery: keys + dropshuffle: true + experimental: true + dungeon_counters: 'on' + hints: true + msu_resume: true + collection_rate: true + quickswap: true diff --git a/docs/presets/async_doors_league/S3_Main.yaml b/docs/presets/async_doors_league/S3_Main.yaml index e8d8f282..564d19d4 100644 --- a/docs/presets/async_doors_league/S3_Main.yaml +++ b/docs/presets/async_doors_league/S3_Main.yaml @@ -2,21 +2,22 @@ meta: players: 1 race: true settings: - shopsanity: true - pseudoboots: true - goal: crystals - crystals_gt: random - shuffle: crossed - shufflelinks: true - keysanity: true - door_shuffle: crossed - intensity: 3 - door_type_mode: big - pottery: keys - dropshuffle: true - experimental: true - dungeon_counters: 'on' - hints: true - msu_resume: true - collection_rate: true - quickswap: true + 1: + shopsanity: true + pseudoboots: true + goal: crystals + crystals_gt: random + shuffle: crossed + shufflelinks: true + keysanity: true + door_shuffle: crossed + intensity: 3 + door_type_mode: big + pottery: keys + dropshuffle: true + experimental: true + dungeon_counters: 'on' + hints: true + msu_resume: true + collection_rate: true + quickswap: true diff --git a/docs/presets/async_doors_league/S3_PotteryLottery.yaml b/docs/presets/async_doors_league/S3_PotteryLottery.yaml index d65e2387..baae256d 100644 --- a/docs/presets/async_doors_league/S3_PotteryLottery.yaml +++ b/docs/presets/async_doors_league/S3_PotteryLottery.yaml @@ -2,21 +2,22 @@ meta: players: 1 race: true settings: - shopsanity: true - pseudoboots: true - goal: crystals - crystals_gt: random - shuffle: crossed - shufflelinks: true - keysanity: true - door_shuffle: crossed - intensity: 3 - door_type_mode: big - pottery: lottery - dropshuffle: true - experimental: true - dungeon_counters: 'on' - hints: true - msu_resume: true - collection_rate: true - quickswap: true + 1: + shopsanity: true + pseudoboots: true + goal: crystals + crystals_gt: random + shuffle: crossed + shufflelinks: true + keysanity: true + door_shuffle: crossed + intensity: 3 + door_type_mode: big + pottery: lottery + dropshuffle: true + experimental: true + dungeon_counters: 'on' + hints: true + msu_resume: true + collection_rate: true + quickswap: true diff --git a/source/dungeon/DungeonStitcher.py b/source/dungeon/DungeonStitcher.py index 205941b5..d0b21d89 100644 --- a/source/dungeon/DungeonStitcher.py +++ b/source/dungeon/DungeonStitcher.py @@ -319,6 +319,8 @@ def determine_paths_for_dungeon(world, player, all_regions, name): if world.mode[player] == 'standard' and name == 'Hyrule Castle Dungeon': paths.append('Hyrule Dungeon Cellblock') paths.append(('Hyrule Dungeon Cellblock', 'Hyrule Castle Throne Room')) + entrance = next(x for x in world.dungeon_portals[player] if x.name == 'Hyrule Castle South') + paths.append(('Hyrule Dungeon Cellblock', entrance.door.entrance.parent_region.name)) if world.doorShuffle[player] in ['basic'] and name == 'Thieves Town': paths.append('Thieves Attic Window') elif 'Thieves Attic Window' in all_r_names: diff --git a/test/customizer/test_stuff.yaml b/test/customizer/test_stuff.yaml new file mode 100644 index 00000000..552d5490 --- /dev/null +++ b/test/customizer/test_stuff.yaml @@ -0,0 +1,28 @@ +meta: + players: 1 + race: true +settings: + 1: + shopsanity: true + pseudoboots: true + goal: crystals + crystals_gt: random + keysanity: true + door_shuffle: crossed + intensity: 3 + door_type_mode: big + pottery: keys + dropshuffle: true + experimental: true + dungeon_counters: 'on' + hints: true + msu_resume: true + collection_rate: true + quickswap: true +start_inventory: + 1: + - Pegasus Boots + - Ocarina (Activated) + - Magic Mirror + - Boss Heart Container + - Blue Mail \ No newline at end of file