From 2a3916c1c58e033b06042d5d5413ea85cd94babf Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 6 Feb 2026 13:43:45 -0500 Subject: Add shuffle_fast_travel and fast_travel_access to apworld --- apworld/__init__.py | 2 ++ apworld/options.py | 23 +++++++++++++++++++++++ apworld/player_logic.py | 16 +++++++++++++++- apworld/regions.py | 18 ++++++++++++++++++ apworld/static_logic.py | 12 ++++++++++++ 5 files changed, 70 insertions(+), 1 deletion(-) (limited to 'apworld') diff --git a/apworld/__init__.py b/apworld/__init__.py index 5bad63e..ff1da66 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py @@ -140,6 +140,7 @@ class Lingo2World(World): "enable_gift_maps", "enable_icarus", "endings_requirement", + "fast_travel_access", "keyholder_sanity", "masteries_requirement", "shuffle_control_center_colors", @@ -155,6 +156,7 @@ class Lingo2World(World): slot_data: dict[str, object] = { **self.options.as_dict(*slot_options), + "rte": [self.static_logic.objects.maps[map_id].name for map_id in self.player_logic.rte_mapping], "version": self.static_logic.get_data_version(), } diff --git a/apworld/options.py b/apworld/options.py index 5661351..063af21 100644 --- a/apworld/options.py +++ b/apworld/options.py @@ -91,6 +91,27 @@ class CyanDoorBehavior(Choice): option_item = 2 +class ShuffleFastTravel(Toggle): + """If enabled, the list of maps you can fast travel to is randomized, except for The Entry, which is always + accessible.""" + display_name = "Shuffle Fast Travel" + + +class FastTravelAccess(Choice): + """ + Controls how the fast travel buttons on the pause menu work. + + - **Vanilla**: You can only fast travel to maps once you have been to them and stepped foot in the general area that + the warp would place you. This option means that fast travel has no impact on logic. + - **Unlocked**: All five fast travel maps will be available from the start. + - **Items**: Only The Entry is available from the start. The other fast travel buttons are locked behind items. + """ + display_name = "Fast Travel Access" + option_vanilla = 0 + option_unlocked = 1 + option_items = 2 + + class EnableIcarus(Toggle): """ Controls whether Icarus is randomized. If disabled, which is the default, no locations or items will be created for @@ -234,6 +255,8 @@ class Lingo2Options(PerGameCommonOptions): shuffle_worldports: ShuffleWorldports keyholder_sanity: KeyholderSanity cyan_door_behavior: CyanDoorBehavior + shuffle_fast_travel: ShuffleFastTravel + fast_travel_access: FastTravelAccess enable_icarus: EnableIcarus enable_gift_maps: EnableGiftMaps daedalus_only: DaedalusOnly diff --git a/apworld/player_logic.py b/apworld/player_logic.py index b946296..a02856e 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py @@ -5,7 +5,7 @@ from .generated import data_pb2 as data_pb2 from .items import SYMBOL_ITEMS from typing import TYPE_CHECKING, NamedTuple -from .options import ShuffleLetters, CyanDoorBehavior, VictoryCondition +from .options import ShuffleLetters, CyanDoorBehavior, VictoryCondition, FastTravelAccess if TYPE_CHECKING: from . import Lingo2World @@ -221,6 +221,7 @@ class Lingo2PlayerLogic: double_letter_amount: dict[str, int] goal_room_id: int + rte_mapping: list[int] def __init__(self, world: "Lingo2World"): self.world = world @@ -304,6 +305,19 @@ class Lingo2PlayerLogic: if "The Fuzzy" in world.options.enable_gift_maps.value: self.real_items.append("Numbers") + if world.options.shuffle_fast_travel: + travelable_maps = [map_id for map_id in self.shuffled_maps + if world.static_logic.objects.maps[map_id].HasField("rte_room")] + self.rte_mapping = world.random.sample(travelable_maps, 4) + else: + canonical_rtes = ["the_plaza", "the_gallery", "daedalus", "control_center"] + self.rte_mapping = [world.static_logic.map_id_by_name[map_name] for map_name in canonical_rtes + if world.static_logic.map_id_by_name[map_name] in self.shuffled_maps] + + if world.options.fast_travel_access == FastTravelAccess.option_items: + for rte_map in self.rte_mapping: + self.real_items.append(world.static_logic.get_map_rte_item_name(rte_map)) + if self.world.options.shuffle_doors: for progressive in world.static_logic.objects.progressives: for i in range(0, len(progressive.doors)): diff --git a/apworld/regions.py b/apworld/regions.py index 2f9b571..500139f 100644 --- a/apworld/regions.py +++ b/apworld/regions.py @@ -5,6 +5,7 @@ from BaseClasses import Region, ItemClassification, Entrance from entrance_rando import randomize_entrances from .items import Lingo2Item from .locations import Lingo2Location +from .options import FastTravelAccess from .player_logic import AccessRequirements from .rules import make_location_lambda @@ -153,6 +154,23 @@ def create_regions(world: "Lingo2World"): for region in reqs.get_referenced_rooms(): world.multiworld.register_indirect_condition(regions[region], connection) + if world.options.fast_travel_access != FastTravelAccess.option_vanilla: + for rte_map_id in world.player_logic.rte_mapping: + rte_map = world.static_logic.objects.maps[rte_map_id] + to_region = world.static_logic.get_room_region_name(rte_map.rte_room) + + if to_region not in regions: + continue + + connection_name = f"Return to {to_region}" + + reqs = AccessRequirements() + + if world.options.fast_travel_access == FastTravelAccess.option_items: + reqs.items.add(world.static_logic.get_map_rte_item_name(rte_map_id)) + + regions["Menu"].connect(regions[to_region], connection_name, make_location_lambda(reqs, world, None)) + world.multiworld.regions += regions.values() diff --git a/apworld/static_logic.py b/apworld/static_logic.py index 715178e..672ae5a 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py @@ -18,6 +18,8 @@ class Lingo2StaticLogic: door_id_by_ap_id: dict[int, int] port_id_by_ap_id: dict[int, int] + map_id_by_name: dict[str, int] + def __init__(self): self.item_id_to_name = {} self.location_id_to_name = {} @@ -79,6 +81,10 @@ class Lingo2StaticLogic: for trap_name in ANTI_COLLECTABLE_TRAPS: self.item_id_to_name[self.objects.special_ids[trap_name]] = trap_name + for game_map in self.objects.maps: + if game_map.HasField("rte_room"): + self.item_id_to_name[game_map.rte_ap_id] = self.get_map_rte_item_name(game_map.id) + self.item_name_to_id = {name: ap_id for ap_id, name in self.item_id_to_name.items()} self.location_name_to_id = {name: ap_id for ap_id, name in self.location_id_to_name.items()} @@ -90,6 +96,8 @@ class Lingo2StaticLogic: self.door_id_by_ap_id = {door.ap_id: door.id for door in self.objects.doors if door.HasField("ap_id")} self.port_id_by_ap_id = {port.ap_id: port.id for port in self.objects.ports if port.HasField("ap_id")} + self.map_id_by_name = {game_map.name: game_map.id for game_map in self.objects.maps} + def get_door_item_name(self, door: data_pb2.Door) -> str: return f"{self.get_map_object_map_name(door)} - {door.name}" @@ -177,6 +185,10 @@ class Lingo2StaticLogic: def get_room_object_map_id(self, obj) -> int: return self.objects.rooms[obj.room_id].map_id + def get_map_rte_item_name(self, map_id: int) -> str: + game_map = self.objects.maps[map_id] + return f"Return to {game_map.display_name}" + def get_data_version(self) -> list[int]: version = self.objects.version return [version.major, version.minor, version.patch] -- cgit 1.4.1