diff options
| author | Star Rauchenberger <fefferburbia@gmail.com> | 2025-09-22 12:05:18 -0400 |
|---|---|---|
| committer | Star Rauchenberger <fefferburbia@gmail.com> | 2025-09-22 12:05:18 -0400 |
| commit | 424f5d4a830fb43f86c76d73d795412890d55bc2 (patch) | |
| tree | e949a30d7d683cb45d3c0dcfd8f391a769c3dc7f /apworld | |
| parent | bc3f90b6bdfdb651570a7b3f0e80fea19db14974 (diff) | |
| download | lingo2-archipelago-424f5d4a830fb43f86c76d73d795412890d55bc2.tar.gz lingo2-archipelago-424f5d4a830fb43f86c76d73d795412890d55bc2.tar.bz2 lingo2-archipelago-424f5d4a830fb43f86c76d73d795412890d55bc2.zip | |
[Apworld] Added worldport shuffle
Diffstat (limited to 'apworld')
| -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) |
