about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--apworld/__init__.py1
-rw-r--r--apworld/context.py7
-rw-r--r--apworld/options.py9
-rw-r--r--apworld/player_logic.py55
-rw-r--r--apworld/regions.py41
-rw-r--r--apworld/static_logic.py3
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):
130 slot_options = [ 130 slot_options = [
131 "cyan_door_behavior", 131 "cyan_door_behavior",
132 "daedalus_roof_access", 132 "daedalus_roof_access",
133 "enable_icarus",
133 "keyholder_sanity", 134 "keyholder_sanity",
134 "shuffle_control_center_colors", 135 "shuffle_control_center_colors",
135 "shuffle_doors", 136 "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):
550 elif cmd == "CheckWorldport": 550 elif cmd == "CheckWorldport":
551 port_id = args["port_id"] 551 port_id = args["port_id"]
552 worldports = {port_id} 552 worldports = {port_id}
553 if str(port_id) in manager.client_ctx.slot_data["port_pairings"]: 553
554 worldports.add(manager.client_ctx.slot_data["port_pairings"][str(port_id)]) 554 # Also check the reverse port if it's a two-way connection.
555 port_pairings = manager.client_ctx.slot_data["port_pairings"]
556 if str(port_id) in port_pairings and port_pairings.get(str(port_pairings[str(port_id)]), None) == port_id:
557 worldports.add(port_pairings[str(port_id)])
555 558
556 updates = manager.update_worldports(worldports) 559 updates = manager.update_worldports(worldports)
557 if len(updates) > 0: 560 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):
95 option_item = 2 95 option_item = 2
96 96
97 97
98class EnableIcarus(Toggle):
99 """
100 Controls whether Icarus is randomized. If disabled, which is the default, no locations or items will be created for
101 it, and its worldport will not be shuffled when worldport shuffle is on.
102 """
103 display_name = "Enable Icarus"
104
105
98class DaedalusRoofAccess(Toggle): 106class DaedalusRoofAccess(Toggle):
99 """ 107 """
100 If enabled, the player will be logically expected to be able to go from the castle entrance to any part of Daedalus 108 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):
173 shuffle_worldports: ShuffleWorldports 181 shuffle_worldports: ShuffleWorldports
174 keyholder_sanity: KeyholderSanity 182 keyholder_sanity: KeyholderSanity
175 cyan_door_behavior: CyanDoorBehavior 183 cyan_door_behavior: CyanDoorBehavior
184 enable_icarus: EnableIcarus
176 daedalus_roof_access: DaedalusRoofAccess 185 daedalus_roof_access: DaedalusRoofAccess
177 strict_purple_ending: StrictPurpleEnding 186 strict_purple_ending: StrictPurpleEnding
178 strict_cyan_ending: StrictCyanEnding 187 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):
202class Lingo2PlayerLogic: 202class Lingo2PlayerLogic:
203 world: "Lingo2World" 203 world: "Lingo2World"
204 204
205 shuffled_maps: set[int]
206
205 locations_by_room: dict[int, list[PlayerLocation]] 207 locations_by_room: dict[int, list[PlayerLocation]]
206 event_loc_item_by_room: dict[int, dict[str, str]] 208 event_loc_item_by_room: dict[int, dict[str, str]]
207 209
@@ -227,9 +229,24 @@ class Lingo2PlayerLogic:
227 self.real_items = list() 229 self.real_items = list()
228 self.double_letter_amount = dict() 230 self.double_letter_amount = dict()
229 231
232 def should_shuffle_map(game_map) -> bool:
233 if game_map.type == data_pb2.MapType.NORMAL_MAP:
234 return True
235 elif game_map.type == data_pb2.MapType.ICARUS:
236 return bool(world.options.enable_icarus)
237
238 return False
239
240 self.shuffled_maps = set(game_map.id for game_map in world.static_logic.objects.maps
241 if should_shuffle_map(game_map))
242
230 if self.world.options.shuffle_doors: 243 if self.world.options.shuffle_doors:
231 for progressive in world.static_logic.objects.progressives: 244 for progressive in world.static_logic.objects.progressives:
232 for i in range(0, len(progressive.doors)): 245 for i in range(0, len(progressive.doors)):
246 door = world.static_logic.objects.doors[progressive.doors[i]]
247 if door.map_id not in self.shuffled_maps:
248 continue
249
233 self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1) 250 self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1)
234 self.real_items.append(progressive.name) 251 self.real_items.append(progressive.name)
235 252
@@ -246,14 +263,21 @@ class Lingo2PlayerLogic:
246 else: 263 else:
247 continue 264 continue
248 265
249 for door in door_group.doors: 266 shuffleable_doors = [door_id for door_id in door_group.doors
250 self.item_by_door[door] = (door_group.name, 1) 267 if world.static_logic.objects.doors[door_id].map_id in self.shuffled_maps]
251 268
252 self.real_items.append(door_group.name) 269 if len(shuffleable_doors) > 0:
270 for door in shuffleable_doors:
271 self.item_by_door[door] = (door_group.name, 1)
272
273 self.real_items.append(door_group.name)
253 274
254 # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled 275 # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled
255 # before we calculate any access requirements. 276 # before we calculate any access requirements.
256 for door in world.static_logic.objects.doors: 277 for door in world.static_logic.objects.doors:
278 if door.map_id not in self.shuffled_maps:
279 continue
280
257 if door.type in [data_pb2.DoorType.EVENT, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: 281 if door.type in [data_pb2.DoorType.EVENT, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]:
258 continue 282 continue
259 283
@@ -282,18 +306,28 @@ class Lingo2PlayerLogic:
282 if door_group.type != data_pb2.DoorGroupType.CYAN_DOORS: 306 if door_group.type != data_pb2.DoorGroupType.CYAN_DOORS:
283 continue 307 continue
284 308
285 for door in door_group.doors: 309 shuffleable_doors = [door_id for door_id in door_group.doors
286 if not door in self.item_by_door: 310 if world.static_logic.objects.doors[door_id].map_id in self.shuffled_maps
311 and door_id not in self.item_by_door]
312
313 if len(shuffleable_doors) > 0:
314 for door in shuffleable_doors:
287 self.item_by_door[door] = (door_group.name, 1) 315 self.item_by_door[door] = (door_group.name, 1)
288 316
289 self.real_items.append(door_group.name) 317 self.real_items.append(door_group.name)
290 318
291 for door in world.static_logic.objects.doors: 319 for door in world.static_logic.objects.doors:
320 if door.map_id not in self.shuffled_maps:
321 continue
322
292 if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: 323 if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]:
293 self.locations_by_room.setdefault(door.room_id, []).append(PlayerLocation(door.ap_id, 324 self.locations_by_room.setdefault(door.room_id, []).append(PlayerLocation(door.ap_id,
294 self.get_door_reqs(door.id))) 325 self.get_door_reqs(door.id)))
295 326
296 for letter in world.static_logic.objects.letters: 327 for letter in world.static_logic.objects.letters:
328 if world.static_logic.get_room_object_map_id(letter) not in self.shuffled_maps:
329 continue
330
297 self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id, 331 self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id,
298 AccessRequirements())) 332 AccessRequirements()))
299 behavior = self.get_letter_behavior(letter.key, letter.level2) 333 behavior = self.get_letter_behavior(letter.key, letter.level2)
@@ -313,10 +347,16 @@ class Lingo2PlayerLogic:
313 self.double_letter_amount[letter.key.upper()] = self.double_letter_amount.get(letter.key.upper(), 0) + 1 347 self.double_letter_amount[letter.key.upper()] = self.double_letter_amount.get(letter.key.upper(), 0) + 1
314 348
315 for mastery in world.static_logic.objects.masteries: 349 for mastery in world.static_logic.objects.masteries:
350 if world.static_logic.get_room_object_map_id(mastery) not in self.shuffled_maps:
351 continue
352
316 self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id, 353 self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id,
317 AccessRequirements())) 354 AccessRequirements()))
318 355
319 for ending in world.static_logic.objects.endings: 356 for ending in world.static_logic.objects.endings:
357 if world.static_logic.get_room_object_map_id(ending) not in self.shuffled_maps:
358 continue
359
320 # Don't create a location for your selected ending, and never create a location for White Ending. 360 # Don't create a location for your selected ending, and never create a location for White Ending.
321 if world.options.victory_condition.current_key.removesuffix("_ending").upper() != ending.name\ 361 if world.options.victory_condition.current_key.removesuffix("_ending").upper() != ending.name\
322 and ending.name != "WHITE": 362 and ending.name != "WHITE":
@@ -335,6 +375,9 @@ class Lingo2PlayerLogic:
335 if self.world.options.keyholder_sanity: 375 if self.world.options.keyholder_sanity:
336 for keyholder in world.static_logic.objects.keyholders: 376 for keyholder in world.static_logic.objects.keyholders:
337 if keyholder.HasField("key"): 377 if keyholder.HasField("key"):
378 if world.static_logic.get_room_object_map_id(keyholder) not in self.shuffled_maps:
379 continue
380
338 reqs = AccessRequirements() 381 reqs = AccessRequirements()
339 382
340 if self.get_letter_behavior(keyholder.key, False) != LetterBehavior.UNLOCKED: 383 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"):
62 # locations. This allows us to reference the actual region objects in the access rules for the locations, which is 62 # locations. This allows us to reference the actual region objects in the access rules for the locations, which is
63 # faster than having to look them up during access checking. 63 # faster than having to look them up during access checking.
64 for room in world.static_logic.objects.rooms: 64 for room in world.static_logic.objects.rooms:
65 if room.map_id not in world.player_logic.shuffled_maps:
66 continue
67
65 region = create_region(room, world) 68 region = create_region(room, world)
66 regions[region.name] = region 69 regions[region.name] = region
67 region_and_room.append((region, room)) 70 region_and_room.append((region, room))
@@ -156,10 +159,42 @@ def shuffle_entrances(world: "Lingo2World"):
156 159
157 port_id_by_name: dict[str, int] = {} 160 port_id_by_name: dict[str, int] = {}
158 161
159 for port in world.static_logic.objects.ports: 162 shuffleable_ports = [port for port in world.static_logic.objects.ports
160 if port.no_shuffle: 163 if not port.no_shuffle
161 continue 164 and world.static_logic.get_room_object_map_id(port) in world.player_logic.shuffled_maps]
165
166 if len(shuffleable_ports) % 2 == 1:
167 # We have an odd number of shuffleable ports! Pick a port from a room that has more than one, and make it a
168 # redundant warp to another port.
169 redundant_rooms = set(room.id for room in world.static_logic.objects.rooms if len(room.ports) > 1)
170 redundant_ports = [port for port in shuffleable_ports if port.room_id in redundant_rooms]
171 chosen_port = world.random.choice(redundant_ports)
172
173 shuffleable_ports.remove(chosen_port)
174
175 chosen_destination = world.random.choice(shuffleable_ports)
176
177 world.port_pairings[chosen_port.id] = chosen_destination.id
178
179 from_region_name = world.static_logic.get_room_region_name(chosen_port.room_id)
180 to_region_name = world.static_logic.get_room_region_name(chosen_destination.room_id)
181
182 from_region = world.multiworld.get_region(from_region_name, world.player)
183 to_region = world.multiworld.get_region(to_region_name, world.player)
184
185 connection = Entrance(world.player, f"{from_region_name} - {chosen_port.display_name}", from_region)
186 from_region.exits.append(connection)
187 connection.connect(to_region)
188
189 if chosen_port.HasField("required_door"):
190 door_reqs = world.player_logic.get_door_open_reqs(chosen_port.required_door)
191 connection.access_rule = make_location_lambda(door_reqs, world, None)
192
193 for region in door_reqs.get_referenced_rooms():
194 world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player),
195 connection)
162 196
197 for port in shuffleable_ports:
163 port_region_name = world.static_logic.get_room_region_name(port.room_id) 198 port_region_name = world.static_logic.get_room_region_name(port.room_id)
164 port_region = world.multiworld.get_region(port_region_name, world.player) 199 port_region = world.multiworld.get_region(port_region_name, world.player)
165 200
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:
166 else: 166 else:
167 return game_map.display_name 167 return game_map.display_name
168 168
169 def get_room_object_map_id(self, obj) -> int:
170 return self.objects.rooms[obj.room_id].map_id
171
169 def get_data_version(self) -> list[int]: 172 def get_data_version(self) -> list[int]:
170 version = self.objects.version 173 version = self.objects.version
171 return [version.major, version.minor, version.patch] 174 return [version.major, version.minor, version.patch]