about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--apworld/__init__.py29
-rw-r--r--apworld/options.py11
-rw-r--r--apworld/player_logic.py4
-rw-r--r--apworld/regions.py70
-rw-r--r--apworld/rules.py7
5 files changed, 115 insertions, 6 deletions
diff --git a/apworld/__init__.py b/apworld/__init__.py index f1de503..2213e33 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py
@@ -6,7 +6,7 @@ from worlds.AutoWorld import WebWorld, World
6from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS 6from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS
7from .options import Lingo2Options 7from .options import Lingo2Options
8from .player_logic import Lingo2PlayerLogic 8from .player_logic import Lingo2PlayerLogic
9from .regions import create_regions 9from .regions import create_regions, shuffle_entrances, connect_ports_from_ut
10from .static_logic import Lingo2StaticLogic 10from .static_logic import Lingo2StaticLogic
11from .version import APWORLD_VERSION 11from .version import APWORLD_VERSION
12 12
@@ -46,12 +46,25 @@ class Lingo2World(World):
46 46
47 player_logic: Lingo2PlayerLogic 47 player_logic: Lingo2PlayerLogic
48 48
49 port_pairings: dict[int, int]
50
49 def generate_early(self): 51 def generate_early(self):
50 self.player_logic = Lingo2PlayerLogic(self) 52 self.player_logic = Lingo2PlayerLogic(self)
53 self.port_pairings = {}
51 54
52 def create_regions(self): 55 def create_regions(self):
53 create_regions(self) 56 create_regions(self)
54 57
58 def connect_entrances(self):
59 if self.options.shuffle_worldports:
60 if hasattr(self.multiworld, "re_gen_passthrough") and "Lingo 2" in self.multiworld.re_gen_passthrough:
61 slot_value = self.multiworld.re_gen_passthrough["Lingo 2"]["port_pairings"]
62 self.port_pairings = {int(fp): int(tp) for fp, tp in slot_value.items()}
63
64 connect_ports_from_ut(self.port_pairings, self)
65 else:
66 shuffle_entrances(self)
67
55 from Utils import visualize_regions 68 from Utils import visualize_regions
56 69
57 visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") 70 visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml")
@@ -100,17 +113,29 @@ class Lingo2World(World):
100 "shuffle_gallery_paintings", 113 "shuffle_gallery_paintings",
101 "shuffle_letters", 114 "shuffle_letters",
102 "shuffle_symbols", 115 "shuffle_symbols",
116 "shuffle_worldports",
103 "strict_cyan_ending", 117 "strict_cyan_ending",
104 "strict_purple_ending", 118 "strict_purple_ending",
105 "victory_condition", 119 "victory_condition",
106 ] 120 ]
107 121
108 slot_data = { 122 slot_data: dict[str, object] = {
109 **self.options.as_dict(*slot_options), 123 **self.options.as_dict(*slot_options),
110 "version": [self.static_logic.get_data_version(), APWORLD_VERSION], 124 "version": [self.static_logic.get_data_version(), APWORLD_VERSION],
111 } 125 }
112 126
127 if self.options.shuffle_worldports:
128 slot_data["port_pairings"] = self.port_pairings
129
113 return slot_data 130 return slot_data
114 131
115 def get_filler_item_name(self) -> str: 132 def get_filler_item_name(self) -> str:
116 return "A Job Well Done" 133 return "A Job Well Done"
134
135 # for the universal tracker, doesn't get called in standard gen
136 # docs: https://github.com/FarisTheAncient/Archipelago/blob/tracker/worlds/tracker/docs/re-gen-passthrough.md
137 @staticmethod
138 def interpret_slot_data(slot_data: dict[str, object]) -> dict[str, object]:
139 # returning slot_data so it regens, giving it back in multiworld.re_gen_passthrough
140 # we are using re_gen_passthrough over modifying the world here due to complexities with ER
141 return slot_data
diff --git a/apworld/options.py b/apworld/options.py index 3646eea..795010a 100644 --- a/apworld/options.py +++ b/apworld/options.py
@@ -52,6 +52,16 @@ class ShuffleSymbols(Toggle):
52 display_name = "Shuffle Symbols" 52 display_name = "Shuffle Symbols"
53 53
54 54
55class ShuffleWorldports(Toggle):
56 """
57 Randomizes the connections between maps. This affects worldports only, which are the loading zones you walk into in
58 order to change maps. This does not affect paintings, panels that teleport you, or certain other special connections
59 like the one between The Shop and Control Center. Connections that depend on placing letters in keyholders are also
60 currently not shuffled.
61 """
62 display_name = "Shuffle Worldports"
63
64
55class KeyholderSanity(Toggle): 65class KeyholderSanity(Toggle):
56 """ 66 """
57 If enabled, 26 locations will be created for placing each key into its respective Green Ending keyholder. 67 If enabled, 26 locations will be created for placing each key into its respective Green Ending keyholder.
@@ -157,6 +167,7 @@ class Lingo2Options(PerGameCommonOptions):
157 shuffle_gallery_paintings: ShuffleGalleryPaintings 167 shuffle_gallery_paintings: ShuffleGalleryPaintings
158 shuffle_letters: ShuffleLetters 168 shuffle_letters: ShuffleLetters
159 shuffle_symbols: ShuffleSymbols 169 shuffle_symbols: ShuffleSymbols
170 shuffle_worldports: ShuffleWorldports
160 keyholder_sanity: KeyholderSanity 171 keyholder_sanity: KeyholderSanity
161 cyan_door_behavior: CyanDoorBehavior 172 cyan_door_behavior: CyanDoorBehavior
162 daedalus_roof_access: DaedalusRoofAccess 173 daedalus_roof_access: DaedalusRoofAccess
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 4aa481d..966f712 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py
@@ -234,10 +234,10 @@ class Lingo2PlayerLogic:
234 234
235 for door_group in world.static_logic.objects.door_groups: 235 for door_group in world.static_logic.objects.door_groups:
236 if door_group.type == data_pb2.DoorGroupType.CONNECTOR: 236 if door_group.type == data_pb2.DoorGroupType.CONNECTOR:
237 if not self.world.options.shuffle_doors: 237 if not self.world.options.shuffle_doors or self.world.options.shuffle_worldports:
238 continue 238 continue
239 elif door_group.type == data_pb2.DoorGroupType.COLOR_CONNECTOR: 239 elif door_group.type == data_pb2.DoorGroupType.COLOR_CONNECTOR:
240 if not self.world.options.shuffle_control_center_colors: 240 if not self.world.options.shuffle_control_center_colors or self.world.options.shuffle_worldports:
241 continue 241 continue
242 elif door_group.type == data_pb2.DoorGroupType.SHUFFLE_GROUP: 242 elif door_group.type == data_pb2.DoorGroupType.SHUFFLE_GROUP:
243 if not self.world.options.shuffle_doors: 243 if not self.world.options.shuffle_doors:
diff --git a/apworld/regions.py b/apworld/regions.py index 993eec8..a7d9a1c 100644 --- a/apworld/regions.py +++ b/apworld/regions.py
@@ -1,6 +1,8 @@
1from typing import TYPE_CHECKING 1from typing import TYPE_CHECKING
2 2
3import BaseClasses
3from BaseClasses import Region, ItemClassification, Entrance 4from BaseClasses import Region, ItemClassification, Entrance
5from entrance_rando import randomize_entrances
4from .items import Lingo2Item 6from .items import Lingo2Item
5from .locations import Lingo2Location 7from .locations import Lingo2Location
6from .player_logic import AccessRequirements 8from .player_logic import AccessRequirements
@@ -76,6 +78,9 @@ def create_regions(world: "Lingo2World"):
76 port = world.static_logic.objects.ports[connection.port] 78 port = world.static_logic.objects.ports[connection.port]
77 connection_name = f"{connection_name} (via port {port.name})" 79 connection_name = f"{connection_name} (via port {port.name})"
78 80
81 if world.options.shuffle_worldports and not port.no_shuffle:
82 continue
83
79 if port.HasField("required_door"): 84 if port.HasField("required_door"):
80 reqs.merge(world.player_logic.get_door_open_reqs(port.required_door)) 85 reqs.merge(world.player_logic.get_door_open_reqs(port.required_door))
81 86
@@ -116,3 +121,68 @@ def create_regions(world: "Lingo2World"):
116 world.multiworld.register_indirect_condition(regions[region], connection) 121 world.multiworld.register_indirect_condition(regions[region], connection)
117 122
118 world.multiworld.regions += regions.values() 123 world.multiworld.regions += regions.values()
124
125
126def shuffle_entrances(world: "Lingo2World"):
127 er_entrances: list[Entrance] = []
128 er_exits: list[Entrance] = []
129
130 port_id_by_name: dict[str, int] = {}
131
132 for port in world.static_logic.objects.ports:
133 if port.no_shuffle:
134 continue
135
136 port_region_name = world.static_logic.get_room_region_name(port.room_id)
137 port_region = world.multiworld.get_region(port_region_name, world.player)
138
139 connection_name = f"{port_region_name} - {port.name}"
140 port_id_by_name[connection_name] = port.id
141
142 entrance = port_region.create_er_target(connection_name)
143 entrance.randomization_type = BaseClasses.EntranceType.TWO_WAY
144
145 er_exit = port_region.create_exit(connection_name)
146 er_exit.randomization_type = BaseClasses.EntranceType.TWO_WAY
147
148 if port.HasField("required_door"):
149 door_reqs = world.player_logic.get_door_open_reqs(port.required_door)
150 er_exit.access_rule = make_location_lambda(door_reqs, world, None)
151
152 for region in door_reqs.get_referenced_rooms():
153 world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player),
154 er_exit)
155
156 er_entrances.append(entrance)
157 er_exits.append(er_exit)
158
159 result = randomize_entrances(world, True, {0:[0]}, False, er_entrances,
160 er_exits)
161
162 for (f, to) in result.pairings:
163 world.port_pairings[port_id_by_name[f]] = port_id_by_name[to]
164
165
166def connect_ports_from_ut(port_pairings: dict[int, int], world: "Lingo2World"):
167 for fpid, tpid in port_pairings.items():
168 from_port = world.static_logic.objects.ports[fpid]
169 to_port = world.static_logic.objects.ports[tpid]
170
171 from_region_name = world.static_logic.get_room_region_name(from_port.room_id)
172 to_region_name = world.static_logic.get_room_region_name(to_port.room_id)
173
174 from_region = world.multiworld.get_region(from_region_name, world.player)
175 to_region = world.multiworld.get_region(to_region_name, world.player)
176
177 connection = Entrance(world.player, f"{from_region_name} - {from_port.name}", from_region)
178
179 if from_port.HasField("required_door"):
180 door_reqs = world.player_logic.get_door_open_reqs(from_port.required_door)
181 connection.access_rule = make_location_lambda(door_reqs, world, None)
182
183 for region in door_reqs.get_referenced_rooms():
184 world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player),
185 connection)
186
187 from_region.exits.append(connection)
188 connection.connect(to_region)
diff --git a/apworld/rules.py b/apworld/rules.py index c077858..f859e75 100644 --- a/apworld/rules.py +++ b/apworld/rules.py
@@ -54,10 +54,13 @@ def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirem
54 return True 54 return True
55 55
56def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World", 56def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World",
57 regions: dict[str, Region]) -> Callable[[CollectionState], bool]: 57 regions: dict[str, Region] | None) -> Callable[[CollectionState], bool]:
58 # Replace required rooms with regions for the top level requirement, which saves looking up the regions during rule 58 # Replace required rooms with regions for the top level requirement, which saves looking up the regions during rule
59 # checking. 59 # checking.
60 required_regions = [regions[room_name] for room_name in reqs.rooms] 60 if regions is not None:
61 required_regions = [regions[room_name] for room_name in reqs.rooms]
62 else:
63 required_regions = [world.multiworld.get_region(room_name, world.player) for room_name in reqs.rooms]
61 new_reqs = reqs.copy() 64 new_reqs = reqs.copy()
62 new_reqs.rooms.clear() 65 new_reqs.rooms.clear()
63 return lambda state: lingo2_can_satisfy_requirements(state, new_reqs, required_regions, world) 66 return lambda state: lingo2_can_satisfy_requirements(state, new_reqs, required_regions, world)