From dcecbb87a19c47c7d00f773f8df6bf98d65410ef Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Wed, 22 Oct 2025 19:27:09 -0400 Subject: Make icarus optional --- apworld/__init__.py | 1 + apworld/context.py | 7 +++++-- apworld/options.py | 9 ++++++++ apworld/player_logic.py | 55 +++++++++++++++++++++++++++++++++++++++++++------ apworld/regions.py | 41 +++++++++++++++++++++++++++++++++--- apworld/static_logic.py | 3 +++ 6 files changed, 105 insertions(+), 11 deletions(-) diff --git a/apworld/__init__.py b/apworld/__init__.py index e126fc0..6540b08 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py @@ -130,6 +130,7 @@ class Lingo2World(World): slot_options = [ "cyan_door_behavior", "daedalus_roof_access", + "enable_icarus", "keyholder_sanity", "shuffle_control_center_colors", "shuffle_doors", diff --git a/apworld/context.py b/apworld/context.py index a0ee34d..d59bf9d 100644 --- a/apworld/context.py +++ b/apworld/context.py @@ -550,8 +550,11 @@ async def process_game_cmd(manager: Lingo2Manager, args: dict): elif cmd == "CheckWorldport": port_id = args["port_id"] worldports = {port_id} - if str(port_id) in manager.client_ctx.slot_data["port_pairings"]: - worldports.add(manager.client_ctx.slot_data["port_pairings"][str(port_id)]) + + # Also check the reverse port if it's a two-way connection. + port_pairings = manager.client_ctx.slot_data["port_pairings"] + if str(port_id) in port_pairings and port_pairings.get(str(port_pairings[str(port_id)]), None) == port_id: + worldports.add(port_pairings[str(port_id)]) updates = manager.update_worldports(worldports) if len(updates) > 0: diff --git a/apworld/options.py b/apworld/options.py index 3d7c9a5..5d1fd7c 100644 --- a/apworld/options.py +++ b/apworld/options.py @@ -95,6 +95,14 @@ class CyanDoorBehavior(Choice): option_item = 2 +class EnableIcarus(Toggle): + """ + Controls whether Icarus is randomized. If disabled, which is the default, no locations or items will be created for + it, and its worldport will not be shuffled when worldport shuffle is on. + """ + display_name = "Enable Icarus" + + class DaedalusRoofAccess(Toggle): """ If enabled, the player will be logically expected to be able to go from the castle entrance to any part of Daedalus @@ -173,6 +181,7 @@ class Lingo2Options(PerGameCommonOptions): shuffle_worldports: ShuffleWorldports keyholder_sanity: KeyholderSanity cyan_door_behavior: CyanDoorBehavior + enable_icarus: EnableIcarus daedalus_roof_access: DaedalusRoofAccess strict_purple_ending: StrictPurpleEnding strict_cyan_ending: StrictCyanEnding diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 5271ed1..0cf0473 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py @@ -202,6 +202,8 @@ class LetterBehavior(IntEnum): class Lingo2PlayerLogic: world: "Lingo2World" + shuffled_maps: set[int] + locations_by_room: dict[int, list[PlayerLocation]] event_loc_item_by_room: dict[int, dict[str, str]] @@ -227,9 +229,24 @@ class Lingo2PlayerLogic: self.real_items = list() self.double_letter_amount = dict() + def should_shuffle_map(game_map) -> bool: + if game_map.type == data_pb2.MapType.NORMAL_MAP: + return True + elif game_map.type == data_pb2.MapType.ICARUS: + return bool(world.options.enable_icarus) + + return False + + self.shuffled_maps = set(game_map.id for game_map in world.static_logic.objects.maps + if should_shuffle_map(game_map)) + if self.world.options.shuffle_doors: for progressive in world.static_logic.objects.progressives: for i in range(0, len(progressive.doors)): + door = world.static_logic.objects.doors[progressive.doors[i]] + if door.map_id not in self.shuffled_maps: + continue + self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1) self.real_items.append(progressive.name) @@ -246,14 +263,21 @@ class Lingo2PlayerLogic: else: continue - for door in door_group.doors: - self.item_by_door[door] = (door_group.name, 1) + shuffleable_doors = [door_id for door_id in door_group.doors + if world.static_logic.objects.doors[door_id].map_id in self.shuffled_maps] - self.real_items.append(door_group.name) + if len(shuffleable_doors) > 0: + for door in shuffleable_doors: + self.item_by_door[door] = (door_group.name, 1) + + self.real_items.append(door_group.name) # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled # before we calculate any access requirements. for door in world.static_logic.objects.doors: + if door.map_id not in self.shuffled_maps: + continue + if door.type in [data_pb2.DoorType.EVENT, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: continue @@ -282,18 +306,28 @@ class Lingo2PlayerLogic: if door_group.type != data_pb2.DoorGroupType.CYAN_DOORS: continue - for door in door_group.doors: - if not door in self.item_by_door: + shuffleable_doors = [door_id for door_id in door_group.doors + if world.static_logic.objects.doors[door_id].map_id in self.shuffled_maps + and door_id not in self.item_by_door] + + if len(shuffleable_doors) > 0: + for door in shuffleable_doors: self.item_by_door[door] = (door_group.name, 1) - self.real_items.append(door_group.name) + self.real_items.append(door_group.name) for door in world.static_logic.objects.doors: + if door.map_id not in self.shuffled_maps: + continue + if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: self.locations_by_room.setdefault(door.room_id, []).append(PlayerLocation(door.ap_id, self.get_door_reqs(door.id))) for letter in world.static_logic.objects.letters: + if world.static_logic.get_room_object_map_id(letter) not in self.shuffled_maps: + continue + self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id, AccessRequirements())) behavior = self.get_letter_behavior(letter.key, letter.level2) @@ -313,10 +347,16 @@ class Lingo2PlayerLogic: self.double_letter_amount[letter.key.upper()] = self.double_letter_amount.get(letter.key.upper(), 0) + 1 for mastery in world.static_logic.objects.masteries: + if world.static_logic.get_room_object_map_id(mastery) not in self.shuffled_maps: + continue + self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id, AccessRequirements())) for ending in world.static_logic.objects.endings: + if world.static_logic.get_room_object_map_id(ending) not in self.shuffled_maps: + continue + # Don't create a location for your selected ending, and never create a location for White Ending. if world.options.victory_condition.current_key.removesuffix("_ending").upper() != ending.name\ and ending.name != "WHITE": @@ -335,6 +375,9 @@ class Lingo2PlayerLogic: if self.world.options.keyholder_sanity: for keyholder in world.static_logic.objects.keyholders: if keyholder.HasField("key"): + if world.static_logic.get_room_object_map_id(keyholder) not in self.shuffled_maps: + continue + reqs = AccessRequirements() if self.get_letter_behavior(keyholder.key, False) != LetterBehavior.UNLOCKED: diff --git a/apworld/regions.py b/apworld/regions.py index 0c3858d..1118603 100644 --- a/apworld/regions.py +++ b/apworld/regions.py @@ -62,6 +62,9 @@ def create_regions(world: "Lingo2World"): # locations. This allows us to reference the actual region objects in the access rules for the locations, which is # faster than having to look them up during access checking. for room in world.static_logic.objects.rooms: + if room.map_id not in world.player_logic.shuffled_maps: + continue + region = create_region(room, world) regions[region.name] = region region_and_room.append((region, room)) @@ -156,10 +159,42 @@ def shuffle_entrances(world: "Lingo2World"): port_id_by_name: dict[str, int] = {} - for port in world.static_logic.objects.ports: - if port.no_shuffle: - continue + shuffleable_ports = [port for port in world.static_logic.objects.ports + if not port.no_shuffle + and world.static_logic.get_room_object_map_id(port) in world.player_logic.shuffled_maps] + + if len(shuffleable_ports) % 2 == 1: + # We have an odd number of shuffleable ports! Pick a port from a room that has more than one, and make it a + # redundant warp to another port. + redundant_rooms = set(room.id for room in world.static_logic.objects.rooms if len(room.ports) > 1) + redundant_ports = [port for port in shuffleable_ports if port.room_id in redundant_rooms] + chosen_port = world.random.choice(redundant_ports) + + shuffleable_ports.remove(chosen_port) + + chosen_destination = world.random.choice(shuffleable_ports) + + world.port_pairings[chosen_port.id] = chosen_destination.id + + from_region_name = world.static_logic.get_room_region_name(chosen_port.room_id) + to_region_name = world.static_logic.get_room_region_name(chosen_destination.room_id) + + from_region = world.multiworld.get_region(from_region_name, world.player) + to_region = world.multiworld.get_region(to_region_name, world.player) + + connection = Entrance(world.player, f"{from_region_name} - {chosen_port.display_name}", from_region) + from_region.exits.append(connection) + connection.connect(to_region) + + if chosen_port.HasField("required_door"): + door_reqs = world.player_logic.get_door_open_reqs(chosen_port.required_door) + connection.access_rule = make_location_lambda(door_reqs, world, None) + + for region in door_reqs.get_referenced_rooms(): + world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), + connection) + for port in shuffleable_ports: port_region_name = world.static_logic.get_room_region_name(port.room_id) port_region = world.multiworld.get_region(port_region_name, world.player) diff --git a/apworld/static_logic.py b/apworld/static_logic.py index e59a47d..2546007 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py @@ -166,6 +166,9 @@ class Lingo2StaticLogic: else: return game_map.display_name + def get_room_object_map_id(self, obj) -> int: + return self.objects.rooms[obj.room_id].map_id + def get_data_version(self) -> list[int]: version = self.objects.version return [version.major, version.minor, version.patch] -- cgit 1.4.1