diff --git a/BaseClasses.py b/BaseClasses.py index f7fd2cc9..91404ebb 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -2287,6 +2287,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 95bfd4c7..d4d94905 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -2107,7 +2107,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: diff --git a/ItemList.py b/ItemList.py index 7080b330..4bbd0f32 100644 --- a/ItemList.py +++ b/ItemList.py @@ -286,6 +286,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)) @@ -435,6 +436,22 @@ def generate_itempool(world, player): if world.pottery[player] not in ['none', 'keys'] and not skip_pool_adjustments: add_pot_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', @@ -962,14 +979,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) @@ -1085,6 +1148,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_icon, lamps_needed_for_dark_rooms) @@ -1206,12 +1285,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 @@ -1227,9 +1314,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 820cf079..1dc01f14 100644 --- a/Main.py +++ b/Main.py @@ -34,7 +34,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/Rom.py b/Rom.py index d210ae01..b0121eea 100644 --- a/Rom.py +++ b/Rom.py @@ -2221,7 +2221,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 @@ -2231,34 +2231,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 38ff5666..4bf9cd83 100644 --- a/Rules.py +++ b/Rules.py @@ -124,6 +124,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)) @@ -277,8 +281,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)) 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