diff options
-rw-r--r-- | apworld/__init__.py | 29 | ||||
-rw-r--r-- | apworld/options.py | 11 | ||||
-rw-r--r-- | apworld/player_logic.py | 4 | ||||
-rw-r--r-- | apworld/regions.py | 70 | ||||
-rw-r--r-- | apworld/rules.py | 7 |
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 | |||
6 | from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS | 6 | from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS |
7 | from .options import Lingo2Options | 7 | from .options import Lingo2Options |
8 | from .player_logic import Lingo2PlayerLogic | 8 | from .player_logic import Lingo2PlayerLogic |
9 | from .regions import create_regions | 9 | from .regions import create_regions, shuffle_entrances, connect_ports_from_ut |
10 | from .static_logic import Lingo2StaticLogic | 10 | from .static_logic import Lingo2StaticLogic |
11 | from .version import APWORLD_VERSION | 11 | from .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 | ||
55 | class 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 | |||
55 | class KeyholderSanity(Toggle): | 65 | class 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 @@ | |||
1 | from typing import TYPE_CHECKING | 1 | from typing import TYPE_CHECKING |
2 | 2 | ||
3 | import BaseClasses | ||
3 | from BaseClasses import Region, ItemClassification, Entrance | 4 | from BaseClasses import Region, ItemClassification, Entrance |
5 | from entrance_rando import randomize_entrances | ||
4 | from .items import Lingo2Item | 6 | from .items import Lingo2Item |
5 | from .locations import Lingo2Location | 7 | from .locations import Lingo2Location |
6 | from .player_logic import AccessRequirements | 8 | from .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 | |||
126 | def 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 | |||
166 | def 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 | ||
56 | def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World", | 56 | def 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) |