diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 48c16f1aea..bf4fed5804 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -54,6 +54,7 @@ class SeedGroup(TypedDict): ladder_storage: int # ls value laurels_at_10_fairies: bool # laurels location value entrance_layout: int # entrance layout value + decoupled: bool plando: TunicPlandoConnections # consolidated plando connections for the seed group @@ -113,6 +114,7 @@ class TunicWorld(World): self.options.entrance_rando.value = passthrough["entrance_rando"] self.options.shuffle_ladders.value = passthrough["shuffle_ladders"] self.options.entrance_layout.value = EntranceLayout.option_standard + self.options.decoupled = passthrough["decoupled"] self.options.laurels_location.value = LaurelsLocation.option_anywhere self.options.combat_logic.value = passthrough["combat_logic"] @@ -132,7 +134,8 @@ class TunicWorld(World): ladder_storage=tunic.options.ladder_storage.value, laurels_at_10_fairies=tunic.options.laurels_location == LaurelsLocation.option_10_fairies, entrance_layout=tunic.options.entrance_layout.value, - plando=multiworld.plando_connections[tunic.player]) + decoupled=bool(tunic.options.decoupled), + plando=tunic.options.plando_connections) continue # off is more restrictive @@ -154,33 +157,41 @@ class TunicWorld(World): elif cls.seed_groups[group]["entrance_layout"] != tunic.options.entrance_layout.value: raise OptionError(f"TUNIC: Conflict between seed group {group}'s Entrance Layout options. " f"Seed group cannot have both Fixed Shop and Direction Pairs enabled.") - - # todo: make it break if you don't have matching portal directions with direction pairs on - if multiworld.plando_connections[tunic.player]: + # decoupled loses to coupled, I could instead make this fail but eh + if not tunic.options.decoupled: + cls.seed_groups[group]["decoupled"] = False + if tunic.options.plando_connections: # loop through the connections in the player's yaml - for cxn in multiworld.plando_connections[tunic.player]: + for cxn in tunic.options.plando_connections: new_cxn = True + # if they used the entrance direction, just swap it around + if cxn.direction == "entrance" and cls.seed_groups[group]["decoupled"]: + player_cxn = PlandoConnection(entrance=cxn.exit, exit=cxn.entrance, direction="exit", percentage=cxn.percentage) + else: + player_cxn = cxn for group_cxn in cls.seed_groups[group]["plando"]: # if neither entrance nor exit match anything in the group, add to group - if ((cxn.entrance == group_cxn.entrance and cxn.exit == group_cxn.exit) - or (cxn.exit == group_cxn.entrance and cxn.entrance == group_cxn.exit)): + if ((player_cxn.entrance == group_cxn.entrance and player_cxn.exit == group_cxn.exit) + # if decoupled is off, the entrance and exit can be swapped + or (player_cxn.exit == group_cxn.entrance and player_cxn.entrance == group_cxn.exit + and not cls.seed_groups[group]["decoupled"])): new_cxn = False break - + # check if this pair is the same as a pair in the group already is_mismatched = ( - cxn.entrance == group_cxn.entrance and cxn.exit != group_cxn.exit - or cxn.entrance == group_cxn.exit and cxn.exit != group_cxn.entrance - or cxn.exit == group_cxn.entrance and cxn.entrance != group_cxn.exit - or cxn.exit == group_cxn.exit and cxn.entrance != group_cxn.entrance + player_cxn.entrance == group_cxn.entrance and player_cxn.exit != group_cxn.exit + or player_cxn.entrance == group_cxn.exit and player_cxn.exit != group_cxn.entrance + or player_cxn.exit == group_cxn.entrance and player_cxn.entrance != group_cxn.exit + or player_cxn.exit == group_cxn.exit and player_cxn.entrance != group_cxn.entrance ) if is_mismatched: raise OptionError(f"TUNIC: Conflict between seed group {group}'s plando " f"connection {group_cxn.entrance} <-> {group_cxn.exit} and " f"{tunic.multiworld.get_player_name(tunic.player)}'s plando " - f"connection {cxn.entrance} <-> {cxn.exit}") + f"connection {player_cxn.entrance} <-> {player_cxn.exit}") if new_cxn: - cls.seed_groups[group]["plando"].value.append(cxn) + cls.seed_groups[group]["plando"].value.append(player_cxn) def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem: item_data = item_table[name] @@ -400,6 +411,7 @@ class TunicWorld(World): "lanternless": self.options.lanternless.value, "maskless": self.options.maskless.value, "entrance_rando": int(bool(self.options.entrance_rando.value)), + "decoupled": self.options.decoupled.value, "shuffle_ladders": self.options.shuffle_ladders.value, "combat_logic": self.options.combat_logic.value, "Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"], diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 57fc238a3a..4d95cf671c 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -557,3 +557,53 @@ def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[s connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic) return connected_regions + + +# which directions are opposites +direction_pairs: Dict[int, int] = { + Direction.north: Direction.south, + Direction.south: Direction.north, + Direction.east: Direction.west, + Direction.west: Direction.east, + Direction.ladder_up: Direction.ladder_down, + Direction.ladder_down: Direction.ladder_up, + Direction.floor: Direction.floor, +} + + +# verify that two portals are in compatible directions +def verify_direction_pair(portal1: Portal, portal2: Portal) -> bool: + if portal1.direction == direction_pairs[portal2.direction]: + return True + elif portal1.name.startswith("Shop"): + if portal2.direction in [Direction.north, Direction.east]: + return True + elif portal2.name.startswith("Shop"): + if portal1.direction in [Direction.north, Direction.east]: + return True + else: + return False + + +# verify that two plando'd portals are in compatible directions +def verify_plando_directions(connection: PlandoConnection) -> bool: + entrance_portal = None + exit_portal = None + for portal in portal_mapping: + if connection.entrance == portal.name: + entrance_portal = portal + if connection.exit == portal.name: + exit_portal = portal + if entrance_portal and exit_portal: + if entrance_portal.direction == direction_pairs[exit_portal.direction]: + return True + # this is two shop portals, they can never pair directions + elif not entrance_portal and not exit_portal: + return False + # if one of them is none, it's a shop, which has two possible directions + elif not entrance_portal: + if exit_portal.direction in [Direction.north, Direction.east]: + return True + elif not exit_portal: + if entrance_portal.direction in [Direction.north, Direction.east]: + return True diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index e213088100..069201b3fe 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -146,6 +146,15 @@ class EntranceLayout(Choice): default = 0 +class Decoupled(Toggle): + """ + Decouple the entrances, so that when you go from one entrance to another, the return trip won't necessarily bring you back to the same place. + Note: For seed groups, if any player in a seed group does not have Decoupled enabled, no one in the seed group will have Decoupled entrances. + """ + internal_name = "decoupled" + display_name = "Decoupled Entrances" + + class LaurelsLocation(Choice): """ Force the Hero's Laurels to be placed at a location in your world. @@ -176,7 +185,9 @@ class TunicPlandoConnections(PlandoConnections): Generic connection plando. Format is: - entrance: "Entrance Name" exit: "Exit Name" + direction: "Direction" percentage: 100 + Direction must be one of 'entrance', 'exit', or 'both', and defaults to 'both' if omitted. Percentage is an integer from 0 to 100 which determines whether that connection will be made. Defaults to 100 if omitted. If the Entrance Layout option is set to Standard or Fixed Shop, you can plando multiple shops. Note that you will wrong warp if you have multiple shops in the same scene. @@ -272,6 +283,7 @@ class TunicOptions(PerGameCommonOptions): shuffle_ladders: ShuffleLadders entrance_rando: EntranceRando entrance_layout: EntranceLayout + decoupled: Decoupled plando_connections: TunicPlandoConnections fool_traps: FoolTraps hexagon_quest: HexagonQuest @@ -298,8 +310,14 @@ tunic_option_groups = [ LaurelsZips, IceGrappling, LadderStorage, - LadderStorageWithoutItems - ]) + LadderStorageWithoutItems, + ]), + OptionGroup("Entrance Randomizer", [ + EntranceRando, + EntranceLayout, + Decoupled, + PlandoConnections, + ]), ] tunic_option_presets: Dict[str, Dict[str, Any]] = {