diff --git a/BaseClasses.py b/BaseClasses.py index 702f5436..ab5fd9cb 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -78,6 +78,7 @@ class World(object): self._room_cache = {} self.dungeon_layouts = {} self.inaccessible_regions = {} + self.enabled_entrances = {} self.key_logic = {} self.pool_adjustment = {} self.key_layout = defaultdict(dict) @@ -143,6 +144,7 @@ class World(object): set_player_attr('force_fix', {'gt': False, 'sw': False, 'pod': False, 'tr': False}) set_player_attr('exp_cache', defaultdict(dict)) + set_player_attr('enabled_entrances', {}) def get_name_string_for_object(self, obj): return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' @@ -1171,6 +1173,8 @@ class CollectionState(object): def collect(self, item, event=False, location=None): if location: self.locations_checked.add(location) + if not item: + return changed = False if item.name.startswith('Progressive '): if 'Sword' in item.name: diff --git a/Bosses.py b/Bosses.py index 5c742015..7e797b42 100644 --- a/Bosses.py +++ b/Bosses.py @@ -181,11 +181,11 @@ def place_bosses(world, player): logging.getLogger('').debug('Bosses chosen %s', bosses) - random.shuffle(bosses) for [loc, level] in boss_locations: loc_text = loc + (' ('+level+')' if level else '') - boss = next((b for b in bosses if can_place_boss(world, player, b, loc, level)), None) - if not boss: + try: + boss = random.choice([b for b in bosses if can_place_boss(world, player, b, loc, level)]) + except IndexError: raise FillError('Could not place boss for location %s' % loc_text) bosses.remove(boss) diff --git a/DoorShuffle.py b/DoorShuffle.py index dc0f18f5..8ee16a33 100644 --- a/DoorShuffle.py +++ b/DoorShuffle.py @@ -778,7 +778,7 @@ def handle_split_dungeons(dungeon_builders, recombinant_builders, entrances_map, def main_dungeon_generation(dungeon_builders, recombinant_builders, connections_tuple, world, player): entrances_map, potentials, connections = connections_tuple - enabled_entrances = {} + enabled_entrances = world.enabled_entrances[player] = {} sector_queue = deque(dungeon_builders.values()) last_key, loops = None, 0 logging.getLogger('').info(world.fish.translate("cli", "cli", "generating.dungeon")) @@ -1607,7 +1607,7 @@ def find_key_door_candidates(region, checked, world, player): if d2.type == DoorType.Normal: room_b = world.get_room(d2.roomIndex, player) pos_b, kind_b = room_b.doorList[d2.doorListPos] - valid = kind in okay_normals and kind_b in okay_normals + valid = kind in okay_normals and kind_b in okay_normals and valid_key_door_pair(d, d2) else: valid = kind in okay_normals if valid and 0 <= d2.doorListPos < 4: @@ -1624,6 +1624,12 @@ def find_key_door_candidates(region, checked, world, player): return candidates, checked_doors +def valid_key_door_pair(door1, door2): + if door1.roomIndex != door2.roomIndex: + return True + return len(door1.entrance.parent_region.exits) <= 1 or len(door2.entrance.parent_region.exits) <= 1 + + def reassign_key_doors(builder, world, player): logger = logging.getLogger('') logger.debug('Key doors for %s', builder.name) @@ -1940,14 +1946,18 @@ def check_required_paths(paths, world, player): if dungeon_name in world.dungeon_layouts[player].keys(): builder = world.dungeon_layouts[player][dungeon_name] if len(paths[dungeon_name]) > 0: - states_to_explore = defaultdict(list) + states_to_explore = {} for path in paths[dungeon_name]: if type(path) is tuple: - states_to_explore[tuple([path[0]])] = path[1] + states_to_explore[tuple([path[0]])] = (path[1], 'any') else: - states_to_explore[tuple(builder.path_entrances)].append(path) + common_starts = tuple(builder.path_entrances) + if common_starts not in states_to_explore: + states_to_explore[common_starts] = ([], 'all') + states_to_explore[common_starts][0].append(path) cached_initial_state = None - for start_regs, dest_regs in states_to_explore.items(): + for start_regs, info in states_to_explore.items(): + dest_regs, path_type = info if type(dest_regs) is not list: dest_regs = [dest_regs] check_paths = convert_regions(dest_regs, world, player) @@ -1964,11 +1974,17 @@ def check_required_paths(paths, world, player): cached_initial_state = state else: state = cached_initial_state - valid, bad_region = check_if_regions_visited(state, check_paths) + if path_type == 'any': + valid, bad_region = check_if_any_regions_visited(state, check_paths) + else: + valid, bad_region = check_if_all_regions_visited(state, check_paths) if not valid: if check_for_pinball_fix(state, bad_region, world, player): explore_state(state, world, player) - valid, bad_region = check_if_regions_visited(state, check_paths) + if path_type == 'any': + valid, bad_region = check_if_any_regions_visited(state, check_paths) + else: + valid, bad_region = check_if_all_regions_visited(state, check_paths) if not valid: raise Exception('%s cannot reach %s' % (dungeon_name, bad_region.name)) @@ -2008,7 +2024,7 @@ def explore_state_not_inaccessible(state, world, player): state.add_all_doors_check_unattached(connect_region, world, player) -def check_if_regions_visited(state, check_paths): +def check_if_any_regions_visited(state, check_paths): valid = False breaking_region = None for region_target in check_paths: @@ -2020,6 +2036,13 @@ def check_if_regions_visited(state, check_paths): return valid, breaking_region +def check_if_all_regions_visited(state, check_paths): + for region_target in check_paths: + if not state.visited_at_all(region_target): + return False, region_target + return True, None + + def check_for_pinball_fix(state, bad_region, world, player): pinball_region = world.get_region('Skull Pinball', player) # todo: lobby shuffle @@ -2043,7 +2066,7 @@ class DROptions(Flag): Town_Portal = 0x02 # If on, Players will start with mirror scroll Map_Info = 0x04 Debug = 0x08 - Rails = 0x10 # If on, draws rails + # Rails = 0x10 # Unused bit now OriginalPalettes = 0x20 Open_PoD_Wall = 0x40 # If on, pre opens the PoD wall, no bow required Open_Desert_Wall = 0x80 # If on, pre opens the desert wall, no fire required diff --git a/DungeonGenerator.py b/DungeonGenerator.py index 1b4b3564..72938538 100644 --- a/DungeonGenerator.py +++ b/DungeonGenerator.py @@ -109,7 +109,8 @@ def generate_dungeon_find_proposal(builder, entrance_region_names, split_dungeon p_region = portal.door.entrance.connected_region access_region = next(x.parent_region for x in p_region.entrances if x.parent_region.type in [RegionType.LightWorld, RegionType.DarkWorld]) - if access_region.name in world.inaccessible_regions[player]: + if (access_region.name in world.inaccessible_regions[player] and + region.name not in world.enabled_entrances[player]): excluded[region] = None entrance_regions = [x for x in entrance_regions if x not in excluded.keys()] doors_to_connect = {} diff --git a/Fill.py b/Fill.py index 13de2a20..65aa2f85 100644 --- a/Fill.py +++ b/Fill.py @@ -161,7 +161,11 @@ def valid_key_placement(item, location, itempool, world): return True key_logic = world.key_logic[item.player][dungeon.name] unplaced_keys = len([x for x in itempool if x.name == key_logic.small_key_name and x.player == item.player]) - return key_logic.check_placement(unplaced_keys, location if item.bigkey else None) + prize_loc = None + if key_logic.prize_location: + prize_loc = world.get_location(key_logic.prize_location, location.player) + cr_count = world.crystals_needed_for_gt[location.player] + return key_logic.check_placement(unplaced_keys, location if item.bigkey else None, prize_loc, cr_count) else: return item.is_inside_dungeon_item(world) @@ -602,7 +606,7 @@ def balance_money_progression(world): base_value = sum(rupee_rooms.values()) available_money = {player: base_value for player in range(1, world.players+1)} for loc in world.get_locations(): - if loc.item.name in rupee_chart: + if loc.item and loc.item.name in rupee_chart: available_money[loc.item.player] += rupee_chart[loc.item.name] total_price = {player: 0 for player in range(1, world.players+1)} @@ -667,7 +671,7 @@ def balance_money_progression(world): slot = shop_to_location_table[location.parent_region.name].index(location.name) shop = location.parent_region.shop shop_item = shop.inventory[slot] - if interesting_item(location, location.item, world, location.item.player): + if location.item and interesting_item(location, location.item, world, location.item.player): if location.item.name.startswith('Rupee') and loc_player == location.item.player: if shop_item['price'] < rupee_chart[location.item.name]: wallet[loc_player] -= shop_item['price'] # will get picked up in the location_free block @@ -687,14 +691,15 @@ def balance_money_progression(world): if location_free: state.collect(location.item, True, location) unchecked_locations.remove(location) - if location.item.name.startswith('Rupee'): - wallet[location.item.player] += rupee_chart[location.item.name] - if location.item.name != 'Rupees (300)': + if location.item: + if location.item.name.startswith('Rupee'): + wallet[location.item.player] += rupee_chart[location.item.name] + if location.item.name != 'Rupees (300)': + balance_locations[location.item.player].add(location) + if interesting_item(location, location.item, world, location.item.player): + checked_locations.append(location) + elif location.item.name in acceptable_balancers: balance_locations[location.item.player].add(location) - if interesting_item(location, location.item, world, location.item.player): - checked_locations.append(location) - elif location.item.name in acceptable_balancers: - balance_locations[location.item.player].add(location) for room, income in rupee_rooms.items(): for player in range(1, world.players+1): if room not in rooms_visited[player] and world.get_region(room, player) in state.reachable_regions[player]: @@ -759,5 +764,5 @@ def balance_money_progression(world): else: state.collect(location.item, True, location) unchecked_locations.remove(location) - if location.item.name.startswith('Rupee'): + if location.item and location.item.name.startswith('Rupee'): wallet[location.item.player] += rupee_chart[location.item.name] diff --git a/ItemList.py b/ItemList.py index 5721aadc..4e95c7a8 100644 --- a/ItemList.py +++ b/ItemList.py @@ -609,6 +609,7 @@ def customize_shops(world, player): upgrade.location = loc change_shop_items_to_rupees(world, player, shops_to_customize) balance_prices(world, player) + check_hints(world, player) def randomize_price(price): @@ -712,6 +713,15 @@ def balance_prices(world, player): # loc.parent_region.shop.inventory[slot]['price'] = new_price +def check_hints(world, player): + if world.shuffle[player] in ['simple', 'restricted', 'full', 'crossed', 'insanity']: + for shop, location_list in shop_to_location_table.items(): + if shop in ['Capacity Upgrade', 'Light World Death Mountain Shop', 'Potion Shop']: + continue # near the queen, near potions, and near 7 chests are fine + for loc_name in location_list: # other shops are indistinguishable in ER + world.get_location(loc_name, player).hint_text = f'for sale' + + repeatable_shop_items = ['Single Arrow', 'Arrows (10)', 'Bombs (3)', 'Bombs (10)', 'Red Potion', 'Small Heart', 'Blue Shield', 'Red Shield', 'Bee', 'Small Key (Universal)', 'Blue Potion', 'Green Potion'] diff --git a/KeyDoorShuffle.py b/KeyDoorShuffle.py index 67654c8c..59e18fa5 100644 --- a/KeyDoorShuffle.py +++ b/KeyDoorShuffle.py @@ -27,7 +27,7 @@ class KeyLayout(object): self.item_locations = set() self.found_doors = set() - self.prize_relevant = False + self.prize_relevant = None # bk special? # bk required? True if big chests or big doors exists @@ -38,7 +38,7 @@ class KeyLayout(object): self.max_chests = calc_max_chests(builder, self, world, player) self.all_locations = set() self.item_locations = set() - self.prize_relevant = False + self.prize_relevant = None class KeyLogic(object): @@ -59,10 +59,11 @@ class KeyLogic(object): self.outside_keys = 0 self.dungeon = dungeon_name self.sm_doors = {} + self.prize_location = None - def check_placement(self, unplaced_keys, big_key_loc=None): + def check_placement(self, unplaced_keys, big_key_loc=None, prize_loc=None, cr_count=7): for rule in self.placement_rules: - if not rule.is_satisfiable(self.outside_keys, unplaced_keys, big_key_loc): + if not rule.is_satisfiable(self.outside_keys, unplaced_keys, big_key_loc, prize_loc, cr_count): return False if big_key_loc: for rule_a, rule_b in itertools.combinations(self.placement_rules, 2): @@ -121,6 +122,7 @@ class PlacementRule(object): self.check_locations_wo_bk = None self.bk_relevant = True self.key_reduced = False + self.prize_relevance = None def contradicts(self, rule, unplaced_keys, big_key_loc): bk_blocked = big_key_loc in self.bk_conditional_set if self.bk_conditional_set else False @@ -155,7 +157,14 @@ class PlacementRule(object): left -= rule_needed return False - def is_satisfiable(self, outside_keys, unplaced_keys, big_key_loc): + def is_satisfiable(self, outside_keys, unplaced_keys, big_key_loc, prize_location, cr_count): + if self.prize_relevance and prize_location: + if self.prize_relevance == 'BigBomb': + if prize_location.item.name not in ['Crystal 5', 'Crystal 6']: + return True + elif self.prize_relevance == 'GT': + if 'Crystal' not in prize_location.item.name or cr_count < 7: + return True bk_blocked = False if self.bk_conditional_set: for loc in self.bk_conditional_set: @@ -261,6 +270,7 @@ def analyze_dungeon(key_layout, world, player): find_bk_locked_sections(key_layout, world, player) key_logic.bk_chests.update(find_big_chest_locations(key_layout.all_chest_locations)) key_logic.bk_chests.update(find_big_key_locked_locations(key_layout.all_chest_locations)) + key_logic.prize_location = dungeon_table[key_layout.sector.name].prize if world.retro[player] and world.mode[player] != 'standard': return @@ -285,6 +295,12 @@ def analyze_dungeon(key_layout, world, player): if not big_avail: if chest_keys == non_big_locs and chest_keys > 0 and available <= possible_smalls and not avail_bigs: key_logic.bk_restricted.update(filter_big_chest(key_counter.free_locations)) + # note to self: this is due to the enough_small_locations function in validate_key_layout_sub_loop + # I don't like this exception here or there + elif available < possible_smalls and avail_bigs and non_big_locs > 0: + max_ctr = find_max_counter(key_layout) + bk_lockdown = [x for x in max_ctr.free_locations if x not in key_counter.free_locations] + key_logic.bk_restricted.update(filter_big_chest(bk_lockdown)) # try to relax the rules here? - smallest requirement that doesn't force a softlock child_queue = deque() for child in key_counter.child_doors.keys(): @@ -358,6 +374,7 @@ def create_exhaustive_placement_rules(key_layout, world, player): rule.bk_conditional_set = blocked_loc rule.needed_keys_wo_bk = min_keys rule.check_locations_wo_bk = set(filter_big_chest(accessible_loc)) + rule.prize_relevance = key_layout.prize_relevant if rule_prize_relevant(key_counter) else None if valid_rule: key_logic.placement_rules.append(rule) adjust_locations_rules(key_logic, rule, accessible_loc, key_layout, key_counter, max_ctr) @@ -365,6 +382,10 @@ def create_exhaustive_placement_rules(key_layout, world, player): refine_location_rules(key_layout) +def rule_prize_relevant(key_counter): + return not key_counter.prize_doors_opened and not key_counter.prize_received + + def skip_key_counter_due_to_prize(key_layout, key_counter): return key_layout.prize_relevant and key_counter.prize_received and not key_counter.prize_doors_opened @@ -464,7 +485,7 @@ def refine_placement_rules(key_layout, max_ctr): if rule.needed_keys_wo_bk == 0: rules_to_remove.append(rule) if len(rule.check_locations_wo_bk) < rule.needed_keys_wo_bk or rule.needed_keys_wo_bk > key_layout.max_chests: - if len(rule.bk_conditional_set) > 0: + if not rule.prize_relevance and len(rule.bk_conditional_set) > 0: key_logic.bk_restricted.update(rule.bk_conditional_set) rules_to_remove.append(rule) changed = True # impossible for bk to be here, I think @@ -1367,6 +1388,15 @@ def forced_big_key_avail(locations): return None +def prize_relevance(key_layout, dungeon_entrance): + if len(key_layout.start_regions) > 1 and dungeon_entrance and dungeon_table[key_layout.key_logic.dungeon].prize: + if dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower']: + return 'GT' + elif dungeon_entrance.name == 'Pyramid Fairy': + return 'BigBomb' + return None + + # Soft lock stuff def validate_key_layout(key_layout, world, player): # retro is all good - except for hyrule castle in standard mode @@ -1378,12 +1408,11 @@ def validate_key_layout(key_layout, world, player): state.big_key_special = check_bk_special(key_layout.sector.regions, world, player) for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) - if (len(key_layout.start_regions) > 1 and dungeon_entrance and - dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and dungeon_table[key_layout.key_logic.dungeon].prize): + prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance) + if prize_relevant_flag: state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance - key_layout.prize_relevant = True + key_layout.prize_relevant = prize_relevant_flag else: state.visit_region(region, key_checks=True) state.add_all_doors_check_keys(region, flat_proposal, world, player) @@ -1411,7 +1440,8 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa found_forced_bk = state.found_forced_bk() smalls_done = not smalls_avail or not enough_small_locations(state, available_small_locations) bk_done = state.big_key_opened or num_bigs == 0 or (available_big_locations == 0 and not found_forced_bk) - if smalls_done and bk_done: + prize_done = not key_layout.prize_relevant or state.prize_doors_opened + if smalls_done and bk_done and prize_done: return False else: # todo: pretty sure you should OR these paths together, maybe when there's one location and it can @@ -1447,6 +1477,7 @@ def validate_key_layout_sub_loop(key_layout, state, checked_states, flat_proposa if not valid: return False # todo: feel like you only open these if the boss is available??? + # todo: or if a crystal isn't valid placement on this boss if not state.prize_doors_opened and key_layout.prize_relevant: state_copy = state.copy() open_a_door(next(iter(state_copy.prize_door_set)), state_copy, flat_proposal, world, player) @@ -1541,12 +1572,11 @@ def create_key_counters(key_layout, world, player): state.big_key_special = True for region in key_layout.start_regions: dungeon_entrance, portal_door = find_outside_connection(region) - if (len(key_layout.start_regions) > 1 and dungeon_entrance and - dungeon_entrance.name in ['Ganons Tower', 'Inverted Ganons Tower', 'Pyramid Fairy'] - and dungeon_table[key_layout.key_logic.dungeon].prize): + prize_relevant_flag = prize_relevance(key_layout, dungeon_entrance) + if prize_relevant_flag: state.append_door_to_list(portal_door, state.prize_doors) state.prize_door_set[portal_door] = dungeon_entrance - key_layout.prize_relevant = True + key_layout.prize_relevant = prize_relevant_flag else: state.visit_region(region, key_checks=True) state.add_all_doors_check_keys(region, flat_proposal, world, player) @@ -1612,8 +1642,13 @@ def can_open_door_by_counter(door, counter: KeyCounter, layout, world, player): # ttl_small_key_only = len(counter.key_only_locations) return cnt_avail_small_locations_by_ctr(ttl_locations, counter, layout, world, player) > 0 elif door.bigKey: - available_big_locations = cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player) - return not counter.big_key_opened and available_big_locations > 0 and not layout.big_key_special + if counter.big_key_opened: + return False + if layout.big_key_special: + return any(x for x in counter.other_locations.keys() if x.forced_item and x.forced_item.bigkey) + else: + available_big_locations = cnt_avail_big_locations_by_ctr(ttl_locations, counter, layout, world, player) + return available_big_locations > 0 else: return True @@ -1970,8 +2005,10 @@ def validate_key_placement(key_layout, world, player): found_prize = any(x for x in counter.important_locations if '- Prize' in x.name) if not found_prize and dungeon_table[key_layout.sector.name].prize: prize_loc = world.get_location(dungeon_table[key_layout.sector.name].prize, player) - # todo: pyramid fairy only care about crystals 5 & 6 - found_prize = 'Crystal' not in prize_loc.item.name + if key_layout.prize_relevant == 'BigBomb': + found_prize = prize_loc.item.name not in ['Crystal 5', 'Crystal 6'] + elif key_layout.prize_relevant == 'GT': + found_prize = 'Crystal' not in prize_loc.item.name or world.crystals_needed_for_gt[player] < 7 else: found_prize = False can_progress = (not counter.big_key_opened and big_found and any(d.bigKey for d in counter.child_doors)) or \ diff --git a/Main.py b/Main.py index 912ca72f..455cbe41 100644 --- a/Main.py +++ b/Main.py @@ -31,7 +31,7 @@ from Utils import output_path, parse_player_names from source.item.BiasedFill import create_item_pool_config, massage_item_pool -__version__ = '0.5.1.0-u' +__version__ = '0.5.1.1-u' from source.classes.BabelFish import BabelFish diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 37877c89..4d212275 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,17 @@ CLI: ```--bombbag``` # Bug Fixes and Notes. +* 0.5.1.1 + * Shop hints in ER are now more generic instead of using "near X" because they aren't near that anymore + * Added memory location for mutliworld scripts to read what item was just obtain (longer than one frame) + * Fix for bias in boss shuffle "full" + * Fix for certain lone big chests in keysanity (allowed you to get contents without big key) + * Fix for pinball checking + * Fix for multi-entrance dungeons + * 2 fixes for big key placement logic + * ensure big key is placed early if the validator assumes it) + * Open big key doors appropriately when generating rules and big key is forced somewhere + * Updated cutoff entrances for intensity 3 * 0.5.1.0 * Large logic refactor introducing a new method of key logic * Some performance optimization diff --git a/Regions.py b/Regions.py index f26bbdb4..5dedc628 100644 --- a/Regions.py +++ b/Regions.py @@ -1382,9 +1382,9 @@ location_table = {'Mushroom': (0x180013, 0x186338, False, 'in the woods'), 'Red Shield Shop - Left': (None, None, False, 'for sale as a curiosity'), 'Red Shield Shop - Middle': (None, None, False, 'for sale as a curiosity'), 'Red Shield Shop - Right': (None, None, False, 'for sale as a curiosity'), - 'Potion Shop - Left': (None, None, False, 'for sale near the witch'), - 'Potion Shop - Middle': (None, None, False, 'for sale near the witch'), - 'Potion Shop - Right': (None, None, False, 'for sale near the witch'), + 'Potion Shop - Left': (None, None, False, 'for sale near potions'), + 'Potion Shop - Middle': (None, None, False, 'for sale near potions'), + 'Potion Shop - Right': (None, None, False, 'for sale near potions'), } lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int} diff --git a/Rom.py b/Rom.py index d39675db..d6248488 100644 --- a/Rom.py +++ b/Rom.py @@ -32,7 +32,7 @@ from source.classes.SFX import randomize_sfx JAP10HASH = '03a63945398191337e896e5771f77173' -RANDOMIZERBASEHASH = '988f1546b14d8f2e6ee30b9de44882da' +RANDOMIZERBASEHASH = 'ef6e3e1aa59838c01dbd5b1b2387e70c' class JsonRom(object): @@ -671,7 +671,19 @@ def patch_rom(world, rom, player, team, enemized, is_mystery=False): dr_flags |= DROptions.Debug if world.doorShuffle[player] == 'crossed' and world.logic[player] != 'nologic'\ and world.mixed_travel[player] == 'prevent': - dr_flags |= DROptions.Rails + # PoD Falling Bridge or Hammjump + # 1FA607: db $2D, $79, $69 ; 0x0069: Vertical Rail ↕ | { 0B, 1E } | Size: 05 + # 1FA60A: db $14, $99, $5D ; 0x005D: Large Horizontal Rail ↔ | { 05, 26 } | Size: 01 + rom.write_bytes(0xfa607, [0x2d, 0x79, 0x69, 0x14, 0x99, 0x5d]) + # PoD Arena + # 1FA573: db $D4, $B2, $22 ; 0x0022: Horizontal Rail ↔ | { 35, 2C } | Size: 02 + # 1FA576: db $D4, $CE, $22 ; 0x0022: Horizontal Rail ↔ | { 35, 33 } | Size: 01 + # 1FA579: db $D9, $AE, $69 ; 0x0069: Vertical Rail ↕ | { 36, 2B } | Size: 06 + rom.write_bytes(0xfa573, [0xd4, 0xb2, 0x22, 0xd4, 0xce, 0x22, 0xd9, 0xae, 0x69]) + # Mire BK Pond + # 1FB1FC: db $C8, $9D, $69 ; 0x0069: Vertical Rail ↕ | { 32, 27 } | Size: 01 + # 1FB1FF: db $B4, $AC, $5D ; 0x005D: Large Horizontal Rail ↔ | { 2D, 2B } | Size: 00 + rom.write_bytes(0xfb1fc, [0xc8, 0x9d, 0x69, 0xb4, 0xac, 0x5d]) if world.standardize_palettes[player] == 'original': dr_flags |= DROptions.OriginalPalettes if world.experimental[player]: diff --git a/Rules.py b/Rules.py index dec41c79..c0676fd7 100644 --- a/Rules.py +++ b/Rules.py @@ -1969,9 +1969,11 @@ def add_key_logic_rules(world, player): forbid_item(location, d_logic.small_key_name, player) for door in d_logic.bk_doors: add_rule(world.get_entrance(door.name, player), create_rule(d_logic.bk_name, player)) - if len(d_logic.bk_doors) > 0 or len(d_logic.bk_chests) > 1: - for chest in d_logic.bk_chests: - add_rule(world.get_location(chest.name, player), create_rule(d_logic.bk_name, player)) + for chest in d_logic.bk_chests: + big_chest = world.get_location(chest.name, player) + add_rule(big_chest, create_rule(d_logic.bk_name, player)) + if len(d_logic.bk_doors) == 0 and len(d_logic.bk_chests) <= 1: + set_always_allow(big_chest, lambda state, item: item.name == d_logic.bk_name and item.player == player) if world.retro[player]: for d_name, layout in world.key_layout[player].items(): for door in layout.flat_prop: diff --git a/asm/doortables.asm b/asm/doortables.asm index 8ee0dd60..eae16430 100644 --- a/asm/doortables.asm +++ b/asm/doortables.asm @@ -582,6 +582,8 @@ dw $00bc, $00a2, $00a3, $00c2, $001a, $0049, $0014, $008c dw $009f, $0066, $005d, $00a8, $00a9, $00aa, $00b9, $0052 ; HC West Hall, TR Dash Bridge, TR Hub, Pod Arena, GT Petting Zoo, Ice Spike Cross dw $0050, $00c5, $00c6, $0009, $0003, $002a, $007d, $005e +; Sewer Drop, Mire Cross, GT Crystal Circles +dw $0011, $00b2, $003d dw $ffff ; dungeon tables diff --git a/data/base2current.bps b/data/base2current.bps index 5f411710..17b3e497 100644 Binary files a/data/base2current.bps and b/data/base2current.bps differ diff --git a/source/item/FillUtil.py b/source/item/FillUtil.py new file mode 100644 index 00000000..6aa2f5cf --- /dev/null +++ b/source/item/FillUtil.py @@ -0,0 +1,25 @@ +from collections import defaultdict + +from Dungeons import dungeon_table + +class ItemPoolConfig(object): + + def __init__(self): + self.reserved_locations = defaultdict(set) + + +def create_item_pool_config(world): + world.item_pool_config = config = ItemPoolConfig() + player_set = set() + for player in range(1, world.players+1): + if world.restrict_boss_items[player] != 'none': + player_set.add(player) + if world.restrict_boss_items[player] == 'dungeon': + for dungeon, info in dungeon_table.items(): + if info.prize: + d_name = "Thieves' Town" if dungeon.startswith('Thieves') else dungeon + config.reserved_locations[player].add(f'{d_name} - Boss') + for dungeon in world.dungeons: + for item in dungeon.all_items: + if item.map or item.compass: + item.advancement = True