diff --git a/worlds/witness/items.py b/worlds/witness/items.py index 3a8b3579..41bc3c1b 100644 --- a/worlds/witness/items.py +++ b/worlds/witness/items.py @@ -112,30 +112,12 @@ class WitnessPlayerItems: or name in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME } - # Adjust item classifications based on game settings. - eps_shuffled = self._world.options.shuffle_EPs - come_to_you = self._world.options.elevators_come_to_you - difficulty = self._world.options.puzzle_randomization + # Downgrade door items for item_name, item_data in self.item_data.items(): - if not eps_shuffled and item_name in {"Monastery Garden Entry (Door)", - "Monastery Shortcuts", - "Quarry Boathouse Hook Control (Panel)", - "Windmill Turn Control (Panel)"}: - # Downgrade doors that only gate progress in EP shuffle. - item_data.classification = ItemClassification.useful - elif not come_to_you and not eps_shuffled and item_name in {"Quarry Elevator Control (Panel)", - "Swamp Long Bridge (Panel)"}: - # These Bridges/Elevators are not logical access because they may leave you stuck. - item_data.classification = ItemClassification.useful - elif item_name in {"River Monastery Garden Shortcut (Door)", - "Monastery Laser Shortcut (Door)", - "Orchard Second Gate (Door)", - "Jungle Bamboo Laser Shortcut (Door)", - "Caves Elevator Controls (Panel)"}: - # Downgrade doors that don't gate progress. - item_data.classification = ItemClassification.useful - elif item_name == "Keep Pressure Plates 2 Exit (Door)" and not (difficulty == "none" and eps_shuffled): - # PP2EP requires the door in vanilla puzzles, otherwise it's unnecessary + if not isinstance(item_data.definition, DoorItemDefinition): + continue + + if all(not self._logic.solvability_guaranteed(e_hex) for e_hex in item_data.definition.panel_id_hexes): item_data.classification = ItemClassification.useful # Build the mandatory item list. diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index 1f73c2c0..781cc4e2 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -543,7 +543,7 @@ class WitnessPlayerLocations: ) event_locations = { - p for p in player_logic.EVENT_PANELS + p for p in player_logic.USED_EVENT_NAMES_BY_HEX } self.EVENT_LOCATION_TABLE = { diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 24dfa7d9..229da0a2 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -101,8 +101,11 @@ class WitnessPlayerLogic: for option_entity in option: dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity) - if option_entity in self.EVENT_NAMES_BY_HEX: + if option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX: new_items = frozenset({frozenset([option_entity])}) + elif (panel_hex, option_entity) in self.CONDITIONAL_EVENTS: + new_items = frozenset({frozenset([option_entity])}) + self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[(panel_hex, option_entity)] elif option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect", "PP2 Weirdness", "Theater to Tunnels"}: new_items = frozenset({frozenset([option_entity])}) @@ -170,14 +173,11 @@ class WitnessPlayerLogic: if adj_type == "Event Items": line_split = line.split(" - ") new_event_name = line_split[0] - hex_set = line_split[1].split(",") + entity_hex = line_split[1] + dependent_hex_set = line_split[2].split(",") - for entity, event_name in self.EVENT_NAMES_BY_HEX.items(): - if event_name == new_event_name: - self.DONT_MAKE_EVENTS.add(entity) - - for hex_code in hex_set: - self.EVENT_NAMES_BY_HEX[hex_code] = new_event_name + for dependent_hex in dependent_hex_set: + self.CONDITIONAL_EVENTS[(entity_hex, dependent_hex)] = new_event_name return @@ -437,7 +437,7 @@ class WitnessPlayerLogic: obelisk = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[self.REFERENCE_LOGIC.EP_TO_OBELISK_SIDE[ep_hex]] obelisk_name = obelisk["checkName"] ep_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[ep_hex]["checkName"] - self.EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}" + self.ALWAYS_EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}" else: adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:]) @@ -505,7 +505,8 @@ class WitnessPlayerLogic: for option in connection[1]: individual_entity_requirements = [] for entity in option: - if entity in self.EVENT_NAMES_BY_HEX or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX: + if (entity in self.ALWAYS_EVENT_NAMES_BY_HEX + or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX): individual_entity_requirements.append(frozenset({frozenset({entity})})) else: entity_req = self.reduce_req_within_region(entity) @@ -522,6 +523,72 @@ class WitnessPlayerLogic: self.CONNECTIONS_BY_REGION_NAME[region] = new_connections + def solvability_guaranteed(self, entity_hex: str): + return not ( + entity_hex in self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY + or entity_hex in self.COMPLETELY_DISABLED_ENTITIES + or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES + ) + + def determine_unrequired_entities(self, world: "WitnessWorld"): + """Figure out which major items are actually useless in this world's settings""" + + # Gather quick references to relevant options + eps_shuffled = world.options.shuffle_EPs + come_to_you = world.options.elevators_come_to_you + difficulty = world.options.puzzle_randomization + discards_shuffled = world.options.shuffle_discarded_panels + boat_shuffled = world.options.shuffle_boat + symbols_shuffled = world.options.shuffle_symbols + disable_non_randomized = world.options.disable_non_randomized_puzzles + postgame_included = world.options.shuffle_postgame + goal = world.options.victory_condition + doors = world.options.shuffle_doors + shortbox_req = world.options.mountain_lasers + longbox_req = world.options.challenge_lasers + + # Make some helper booleans so it is easier to follow what's going on + mountain_upper_is_in_postgame = ( + goal == "mountain_box_short" + or goal == "mountain_box_long" and longbox_req <= shortbox_req + ) + mountain_upper_included = postgame_included or not mountain_upper_is_in_postgame + remote_doors = doors >= 2 + door_panels = doors == "panels" or doors == "mixed" + + # It is easier to think about when these items *are* required, so we make that dict first + # If the entity is disabled anyway, we don't need to consider that case + is_item_required_dict = { + "0x03750": eps_shuffled, # Monastery Garden Entry Door + "0x275FA": eps_shuffled, # Boathouse Hook Control + "0x17D02": eps_shuffled, # Windmill Turn Control + "0x0368A": symbols_shuffled or door_panels, # Quarry Stoneworks Stairs Door + "0x3865F": symbols_shuffled or door_panels or eps_shuffled, # Quarry Boathouse 2nd Barrier + "0x17CC4": come_to_you or eps_shuffled, # Quarry Elevator Panel + "0x17E2B": come_to_you and boat_shuffled or eps_shuffled, # Swamp Long Bridge + "0x0CF2A": False, # Jungle Monastery Garden Shortcut + "0x17CAA": remote_doors, # Jungle Monastery Garden Shortcut Panel + "0x0364E": False, # Monastery Laser Shortcut Door + "0x03713": remote_doors, # Monastery Laser Shortcut Panel + "0x03313": False, # Orchard Second Gate + "0x337FA": remote_doors, # Jungle Bamboo Laser Shortcut Panel + "0x3873B": False, # Jungle Bamboo Laser Shortcut Door + "0x335AB": False, # Caves Elevator Controls + "0x335AC": False, # Caves Elevator Controls + "0x3369D": False, # Caves Elevator Controls + "0x01BEA": difficulty == "none" and eps_shuffled, # Keep PP2 + "0x0A0C9": eps_shuffled or discards_shuffled or disable_non_randomized, # Cargo Box Entry Door + "0x09EEB": discards_shuffled or mountain_upper_included, # Mountain Floor 2 Elevator Control Panel + "0x09EDD": mountain_upper_included, # Mountain Floor 2 Exit Door + "0x17CAB": symbols_shuffled or not disable_non_randomized or "0x17CAB" not in self.DOOR_ITEMS_BY_ID, + # Jungle Popup Wall Panel + } + + # Now, return the keys of the dict entries where the result is False to get unrequired major items + self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY |= { + item_name for item_name, is_required in is_item_required_dict.items() if not is_required + } + def make_event_item_pair(self, panel: str): """ Makes a pair of an event panel and its event item @@ -529,21 +596,23 @@ class WitnessPlayerLogic: action = " Opened" if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["entityType"] == "Door" else " Solved" name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["checkName"] + action - if panel not in self.EVENT_NAMES_BY_HEX: + if panel not in self.USED_EVENT_NAMES_BY_HEX: warning("Panel \"" + name + "\" does not have an associated event name.") - self.EVENT_NAMES_BY_HEX[panel] = name + " Event" - pair = (name, self.EVENT_NAMES_BY_HEX[panel]) + self.USED_EVENT_NAMES_BY_HEX[panel] = name + " Event" + pair = (name, self.USED_EVENT_NAMES_BY_HEX[panel]) return pair def make_event_panel_lists(self): - self.EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" + self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory" - for event_hex, event_name in self.EVENT_NAMES_BY_HEX.items(): - if event_hex in self.COMPLETELY_DISABLED_ENTITIES or event_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES: - continue - self.EVENT_PANELS.add(event_hex) + self.USED_EVENT_NAMES_BY_HEX.update(self.ALWAYS_EVENT_NAMES_BY_HEX) - for panel in self.EVENT_PANELS: + self.USED_EVENT_NAMES_BY_HEX = { + event_hex: event_name for event_hex, event_name in self.USED_EVENT_NAMES_BY_HEX.items() + if self.solvability_guaranteed(event_hex) + } + + for panel in self.USED_EVENT_NAMES_BY_HEX: pair = self.make_event_item_pair(panel) self.EVENT_ITEM_PAIRS[pair[0]] = pair[1] @@ -556,6 +625,8 @@ class WitnessPlayerLogic: self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES = set() + self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY = set() + self.THEORETICAL_ITEMS = set() self.THEORETICAL_ITEMS_NO_MULTI = set() self.MULTI_AMOUNTS = defaultdict(lambda: 1) @@ -580,16 +651,14 @@ class WitnessPlayerLogic: # Determining which panels need to be events is a difficult process. # At the end, we will have EVENT_ITEM_PAIRS for all the necessary ones. - self.EVENT_PANELS = set() self.EVENT_ITEM_PAIRS = dict() - self.DONT_MAKE_EVENTS = set() self.COMPLETELY_DISABLED_ENTITIES = set() self.PRECOMPLETED_LOCATIONS = set() self.EXCLUDED_LOCATIONS = set() self.ADDED_CHECKS = set() self.VICTORY_LOCATION = "0x0356B" - self.EVENT_NAMES_BY_HEX = { + self.ALWAYS_EVENT_NAMES_BY_HEX = { "0x00509": "+1 Laser (Symmetry Laser)", "0x012FB": "+1 Laser (Desert Laser)", "0x09F98": "Desert Laser Redirection", @@ -602,10 +671,14 @@ class WitnessPlayerLogic: "0x0C2B2": "+1 Laser (Bunker Laser)", "0x00BF6": "+1 Laser (Swamp Laser)", "0x028A4": "+1 Laser (Treehouse Laser)", - "0x09F7F": "Mountain Entry", + "0x17C34": "Mountain Entry", "0xFFF00": "Bottom Floor Discard Turns On", } + self.USED_EVENT_NAMES_BY_HEX = {} + self.CONDITIONAL_EVENTS = {} + self.make_options_adjustments(world) + self.determine_unrequired_entities(world) self.make_dependency_reduced_checklist() self.make_event_panel_lists() diff --git a/worlds/witness/rules.py b/worlds/witness/rules.py index 5eded11a..8636829a 100644 --- a/worlds/witness/rules.py +++ b/worlds/witness/rules.py @@ -170,7 +170,7 @@ def _has_item(item: str, world: "WitnessWorld", player: int, return lambda state: _can_do_expert_pp2(state, world) elif item == "Theater to Tunnels": return lambda state: _can_do_theater_to_tunnels(state, world) - if item in player_logic.EVENT_PANELS: + if item in player_logic.USED_EVENT_NAMES_BY_HEX: return _can_solve_panel(item, world, player, player_logic, locat) prog_item = StaticWitnessLogic.get_parent_progressive_item(item) diff --git a/worlds/witness/settings/Exclusions/Disable_Unrandomized.txt b/worlds/witness/settings/Exclusions/Disable_Unrandomized.txt index 2419bde0..09c366cf 100644 --- a/worlds/witness/settings/Exclusions/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Exclusions/Disable_Unrandomized.txt @@ -1,9 +1,9 @@ Event Items: -Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9 -Bunker Laser Activation - 0x00061,0x17D01,0x17C42 -Shadows Laser Activation - 0x00021,0x17D28,0x17C71 -Town Tower 4th Door Opens - 0x17CFB,0x3C12B,0x17CF7 -Jungle Popup Wall Lifts - 0x17FA0,0x17D27,0x17F9B,0x17CAB +Monastery Laser Activation - 0x17C65 - 0x00A5B,0x17CE7,0x17FA9 +Bunker Laser Activation - 0x0C2B2 - 0x00061,0x17D01,0x17C42 +Shadows Laser Activation - 0x181B3 - 0x00021,0x17D28,0x17C71 +Town Tower 4th Door Opens - 0x2779A - 0x17CFB,0x3C12B,0x17CF7 +Jungle Popup Wall Lifts - 0x1475B - 0x17FA0,0x17D27,0x17F9B,0x17CAB Requirement Changes: 0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9