diff options
Diffstat (limited to 'apworld')
| -rw-r--r-- | apworld/__init__.py | 19 | ||||
| -rw-r--r-- | apworld/client/gamedata.gd | 8 | ||||
| -rw-r--r-- | apworld/client/manager.gd | 31 | ||||
| -rw-r--r-- | apworld/client/maps/control_center.gd | 55 | ||||
| -rw-r--r-- | apworld/client/player.gd | 40 | ||||
| -rw-r--r-- | apworld/client/rteMenu.gd | 55 | ||||
| -rw-r--r-- | apworld/items.py | 5 | ||||
| -rw-r--r-- | apworld/locations.py | 106 | ||||
| -rw-r--r-- | apworld/options.py | 55 | ||||
| -rw-r--r-- | apworld/player_logic.py | 206 | ||||
| -rw-r--r-- | apworld/regions.py | 61 | ||||
| -rw-r--r-- | apworld/rules.py | 243 | ||||
| -rw-r--r-- | apworld/static_logic.py | 14 | ||||
| -rw-r--r-- | apworld/tracker.py | 4 |
14 files changed, 661 insertions, 241 deletions
| diff --git a/apworld/__init__.py b/apworld/__init__.py index 5bad63e..6b5338e 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py | |||
| @@ -7,7 +7,7 @@ from BaseClasses import ItemClassification, Item, Tutorial | |||
| 7 | from Options import OptionError | 7 | from Options import OptionError |
| 8 | from settings import Group, UserFilePath | 8 | from settings import Group, UserFilePath |
| 9 | from worlds.AutoWorld import WebWorld, World | 9 | from worlds.AutoWorld import WebWorld, World |
| 10 | from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS | 10 | from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS, ALL_LETTERS_UPPER |
| 11 | from .options import Lingo2Options | 11 | from .options import Lingo2Options |
| 12 | from .player_logic import Lingo2PlayerLogic | 12 | from .player_logic import Lingo2PlayerLogic |
| 13 | from .regions import create_regions, shuffle_entrances, connect_ports_from_ut | 13 | from .regions import create_regions, shuffle_entrances, connect_ports_from_ut |
| @@ -70,7 +70,14 @@ class Lingo2World(World): | |||
| 70 | self.player_logic = Lingo2PlayerLogic(self) | 70 | self.player_logic = Lingo2PlayerLogic(self) |
| 71 | self.port_pairings = {} | 71 | self.port_pairings = {} |
| 72 | 72 | ||
| 73 | if self.options.restrict_letter_placements: | ||
| 74 | self.options.local_items.value |= set(ALL_LETTERS_UPPER) | ||
| 75 | |||
| 73 | def create_regions(self): | 76 | def create_regions(self): |
| 77 | if hasattr(self.multiworld, "re_gen_passthrough") and "Lingo 2" in self.multiworld.re_gen_passthrough: | ||
| 78 | self.player_logic.rte_mapping = [self.world.static_logic.map_id_by_name[map_name] | ||
| 79 | for map_name in self.multiworld.re_gen_passthrough["Lingo 2"]["rte"]] | ||
| 80 | |||
| 74 | create_regions(self) | 81 | create_regions(self) |
| 75 | 82 | ||
| 76 | def connect_entrances(self): | 83 | def connect_entrances(self): |
| @@ -124,11 +131,14 @@ class Lingo2World(World): | |||
| 124 | self.push_precollected(self.create_item(name)) | 131 | self.push_precollected(self.create_item(name)) |
| 125 | 132 | ||
| 126 | def create_item(self, name: str) -> Item: | 133 | def create_item(self, name: str) -> Item: |
| 127 | return Lingo2Item(name, ItemClassification.filler if name == self.get_filler_item_name() else | 134 | item = Lingo2Item(name, ItemClassification.filler if name == self.get_filler_item_name() else |
| 128 | ItemClassification.trap if name in ANTI_COLLECTABLE_TRAPS else | 135 | ItemClassification.trap if name in ANTI_COLLECTABLE_TRAPS else |
| 129 | ItemClassification.progression, | 136 | ItemClassification.progression, |
| 130 | self.item_name_to_id.get(name), self.player) | 137 | self.item_name_to_id.get(name), self.player) |
| 131 | 138 | ||
| 139 | item.is_letter = (name in ALL_LETTERS_UPPER) | ||
| 140 | return item | ||
| 141 | |||
| 132 | def set_rules(self): | 142 | def set_rules(self): |
| 133 | self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) | 143 | self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) |
| 134 | 144 | ||
| @@ -140,12 +150,14 @@ class Lingo2World(World): | |||
| 140 | "enable_gift_maps", | 150 | "enable_gift_maps", |
| 141 | "enable_icarus", | 151 | "enable_icarus", |
| 142 | "endings_requirement", | 152 | "endings_requirement", |
| 153 | "fast_travel_access", | ||
| 143 | "keyholder_sanity", | 154 | "keyholder_sanity", |
| 144 | "masteries_requirement", | 155 | "masteries_requirement", |
| 145 | "shuffle_control_center_colors", | 156 | "shuffle_control_center_colors", |
| 146 | "shuffle_doors", | 157 | "shuffle_doors", |
| 147 | "shuffle_gallery_paintings", | 158 | "shuffle_gallery_paintings", |
| 148 | "shuffle_letters", | 159 | "shuffle_letters", |
| 160 | "shuffle_music", | ||
| 149 | "shuffle_symbols", | 161 | "shuffle_symbols", |
| 150 | "shuffle_worldports", | 162 | "shuffle_worldports", |
| 151 | "strict_cyan_ending", | 163 | "strict_cyan_ending", |
| @@ -155,6 +167,9 @@ class Lingo2World(World): | |||
| 155 | 167 | ||
| 156 | slot_data: dict[str, object] = { | 168 | slot_data: dict[str, object] = { |
| 157 | **self.options.as_dict(*slot_options), | 169 | **self.options.as_dict(*slot_options), |
| 170 | "custom_mint_ending": self.player_logic.custom_mint_ending or "", | ||
| 171 | "rte": [self.static_logic.objects.maps[map_id].name for map_id in self.player_logic.rte_mapping], | ||
| 172 | "seed": self.random.randint(0, 1000000), | ||
| 158 | "version": self.static_logic.get_data_version(), | 173 | "version": self.static_logic.get_data_version(), |
| 159 | } | 174 | } |
| 160 | 175 | ||
| diff --git a/apworld/client/gamedata.gd b/apworld/client/gamedata.gd index d7e3136..373f981 100644 --- a/apworld/client/gamedata.gd +++ b/apworld/client/gamedata.gd | |||
| @@ -16,6 +16,7 @@ var anti_trap_ids = {} | |||
| 16 | var location_name_by_id = {} | 16 | var location_name_by_id = {} |
| 17 | var ending_display_name_by_name = {} | 17 | var ending_display_name_by_name = {} |
| 18 | var port_id_by_ap_id = {} | 18 | var port_id_by_ap_id = {} |
| 19 | var map_id_by_rte_ap_id = {} | ||
| 19 | 20 | ||
| 20 | var kSYMBOL_ITEMS | 21 | var kSYMBOL_ITEMS |
| 21 | 22 | ||
| @@ -57,6 +58,9 @@ func load(data_bytes): | |||
| 57 | for map in objects.get_maps(): | 58 | for map in objects.get_maps(): |
| 58 | map_id_by_name[map.get_name()] = map.get_id() | 59 | map_id_by_name[map.get_name()] = map.get_id() |
| 59 | 60 | ||
| 61 | if map.has_rte_ap_id(): | ||
| 62 | map_id_by_rte_ap_id[map.get_rte_ap_id()] = map.get_id() | ||
| 63 | |||
| 60 | for door in objects.get_doors(): | 64 | for door in objects.get_doors(): |
| 61 | var map = objects.get_maps()[door.get_map_id()] | 65 | var map = objects.get_maps()[door.get_map_id()] |
| 62 | 66 | ||
| @@ -300,3 +304,7 @@ func _get_keyholder_location_name(keyholder): | |||
| 300 | "%s - %s Keyholder" | 304 | "%s - %s Keyholder" |
| 301 | % [_get_room_object_location_prefix(keyholder), keyholder.get_key().to_upper()] | 305 | % [_get_room_object_location_prefix(keyholder), keyholder.get_key().to_upper()] |
| 302 | ) | 306 | ) |
| 307 | |||
| 308 | |||
| 309 | func vec3d_to_vector3(input) -> Vector3: | ||
| 310 | return Vector3(input.get_x(), input.get_y(), input.get_z()) | ||
| diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index 1e0b549..f10a0b7 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd | |||
| @@ -46,6 +46,10 @@ const kCYAN_DOOR_BEHAVIOR_H2 = 0 | |||
| 46 | const kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER = 1 | 46 | const kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER = 1 |
| 47 | const kCYAN_DOOR_BEHAVIOR_ITEM = 2 | 47 | const kCYAN_DOOR_BEHAVIOR_ITEM = 2 |
| 48 | 48 | ||
| 49 | const kFAST_TRAVEL_ACCESS_VANILLA = 0 | ||
| 50 | const kFAST_TRAVEL_ACCESS_UNLOCKED = 1 | ||
| 51 | const kFAST_TRAVEL_ACCESS_ITEMS = 2 | ||
| 52 | |||
| 49 | const kEndingNameByVictoryValue = { | 53 | const kEndingNameByVictoryValue = { |
| 50 | 0: "GRAY", | 54 | 0: "GRAY", |
| 51 | 1: "PURPLE", | 55 | 1: "PURPLE", |
| @@ -63,21 +67,26 @@ const kEndingNameByVictoryValue = { | |||
| 63 | } | 67 | } |
| 64 | 68 | ||
| 65 | var apworld_version = [0, 0, 0] | 69 | var apworld_version = [0, 0, 0] |
| 70 | var custom_mint_ending = "" | ||
| 66 | var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2 | 71 | var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2 |
| 67 | var daedalus_only = false | 72 | var daedalus_only = false |
| 68 | var daedalus_roof_access = false | 73 | var daedalus_roof_access = false |
| 69 | var enable_gift_maps = [] | 74 | var enable_gift_maps = [] |
| 70 | var enable_icarus = false | 75 | var enable_icarus = false |
| 71 | var endings_requirement = 0 | 76 | var endings_requirement = 0 |
| 77 | var fast_travel_access = 0 | ||
| 72 | var keyholder_sanity = false | 78 | var keyholder_sanity = false |
| 73 | var masteries_requirement = 0 | 79 | var masteries_requirement = 0 |
| 80 | var music_mapping = {} | ||
| 74 | var port_pairings = {} | 81 | var port_pairings = {} |
| 82 | var rte_mapping = [] | ||
| 75 | var shuffle_control_center_colors = false | 83 | var shuffle_control_center_colors = false |
| 76 | var shuffle_doors = false | 84 | var shuffle_doors = false |
| 77 | var shuffle_gallery_paintings = false | 85 | var shuffle_gallery_paintings = false |
| 78 | var shuffle_letters = kSHUFFLE_LETTERS_VANILLA | 86 | var shuffle_letters = kSHUFFLE_LETTERS_VANILLA |
| 79 | var shuffle_symbols = false | 87 | var shuffle_symbols = false |
| 80 | var shuffle_worldports = false | 88 | var shuffle_worldports = false |
| 89 | var slot_rng = null | ||
| 81 | var strict_cyan_ending = false | 90 | var strict_cyan_ending = false |
| 82 | var strict_purple_ending = false | 91 | var strict_purple_ending = false |
| 83 | var victory_condition = -1 | 92 | var victory_condition = -1 |
| @@ -269,6 +278,13 @@ func _process_item(item, amount): | |||
| 269 | if item_id == gamedata.objects.get_special_ids()["Numbers"] and global.map == "the_fuzzy": | 278 | if item_id == gamedata.objects.get_special_ids()["Numbers"] and global.map == "the_fuzzy": |
| 270 | global.allow_numbers = true | 279 | global.allow_numbers = true |
| 271 | 280 | ||
| 281 | if gamedata.map_id_by_rte_ap_id.has(item_id): | ||
| 282 | var rteInner = get_tree().get_root().get_node_or_null( | ||
| 283 | "scene/player/pause_menu/menu/return/rteInner" | ||
| 284 | ) | ||
| 285 | if rteInner != null: | ||
| 286 | rteInner.refreshButtons() | ||
| 287 | |||
| 272 | # Show a message about the item if it's new. | 288 | # Show a message about the item if it's new. |
| 273 | if int(item["index"]) > _last_new_item: | 289 | if int(item["index"]) > _last_new_item: |
| 274 | _last_new_item = int(item["index"]) | 290 | _last_new_item = int(item["index"]) |
| @@ -463,12 +479,14 @@ func _client_connected(slot_data): | |||
| 463 | _last_new_item = localdata[0] | 479 | _last_new_item = localdata[0] |
| 464 | 480 | ||
| 465 | # Read slot data. | 481 | # Read slot data. |
| 482 | custom_mint_ending = slot_data.get("custom_mint_ending", "") | ||
| 466 | cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0)) | 483 | cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0)) |
| 467 | daedalus_only = bool(slot_data.get("daedalus_only", false)) | 484 | daedalus_only = bool(slot_data.get("daedalus_only", false)) |
| 468 | daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false)) | 485 | daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false)) |
| 469 | enable_gift_maps = slot_data.get("enable_gift_maps", []) | 486 | enable_gift_maps = slot_data.get("enable_gift_maps", []) |
| 470 | enable_icarus = bool(slot_data.get("enable_icarus", false)) | 487 | enable_icarus = bool(slot_data.get("enable_icarus", false)) |
| 471 | endings_requirement = int(slot_data.get("endings_requirement", 0)) | 488 | endings_requirement = int(slot_data.get("endings_requirement", 0)) |
| 489 | fast_travel_access = int(slot_data.get("fast_travel_access", 0)) | ||
| 472 | keyholder_sanity = bool(slot_data.get("keyholder_sanity", false)) | 490 | keyholder_sanity = bool(slot_data.get("keyholder_sanity", false)) |
| 473 | masteries_requirement = int(slot_data.get("masteries_requirement", 0)) | 491 | masteries_requirement = int(slot_data.get("masteries_requirement", 0)) |
| 474 | shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false)) | 492 | shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false)) |
| @@ -496,6 +514,19 @@ func _client_connected(slot_data): | |||
| 496 | raw_pp[p1] | 514 | raw_pp[p1] |
| 497 | )] | 515 | )] |
| 498 | 516 | ||
| 517 | rte_mapping.clear() | ||
| 518 | if slot_data.has("rte"): | ||
| 519 | rte_mapping = slot_data.get("rte") | ||
| 520 | |||
| 521 | slot_rng = RandomNumberGenerator.new() | ||
| 522 | slot_rng.seed = int(slot_data.get("seed", 0)) | ||
| 523 | |||
| 524 | music_mapping.clear() | ||
| 525 | if bool(slot_data.get("shuffle_music", false)): | ||
| 526 | for map_name in global.reserved_scenes: | ||
| 527 | var track_index = slot_rng.randi_range(0, musicPlayer.all_tracks.size() - 1) | ||
| 528 | music_mapping[map_name] = musicPlayer.all_tracks.keys()[track_index] | ||
| 529 | |||
| 499 | # Set up item locks. | 530 | # Set up item locks. |
| 500 | _item_locks = {} | 531 | _item_locks = {} |
| 501 | 532 | ||
| diff --git a/apworld/client/maps/control_center.gd b/apworld/client/maps/control_center.gd index fadfed9..8e919ab 100644 --- a/apworld/client/maps/control_center.gd +++ b/apworld/client/maps/control_center.gd | |||
| @@ -74,6 +74,61 @@ func on_map_load(root): | |||
| 74 | old_door.queue_free() | 74 | old_door.queue_free() |
| 75 | root.get_node("/root/scene/Components/Doors").add_child.call_deferred(new_door) | 75 | root.get_node("/root/scene/Components/Doors").add_child.call_deferred(new_door) |
| 76 | 76 | ||
| 77 | # Display White Ending requirements. | ||
| 78 | var ending_count = 0 | ||
| 79 | var mastery_count = 0 | ||
| 80 | for key in unlocks.data: | ||
| 81 | if unlocks.data[key] == "unlocked": | ||
| 82 | if key.ends_with("_ending") and key != "free_ending": | ||
| 83 | ending_count += 1 | ||
| 84 | elif key.ends_with("_mastery"): | ||
| 85 | mastery_count += 1 | ||
| 86 | |||
| 87 | var sign_prefab = preload("res://objects/nodes/sign.tscn") | ||
| 88 | var sign1 = sign_prefab.instantiate() | ||
| 89 | sign1.position = Vector3(87.5, 5, -42.01) | ||
| 90 | sign1.text = "Endings: %d/%d" % [ending_count, ap.endings_requirement] | ||
| 91 | root.get_node("/root/scene").add_child.call_deferred(sign1) | ||
| 92 | |||
| 93 | var sign2 = sign_prefab.instantiate() | ||
| 94 | sign2.position = Vector3(87.5, 5, -15.99) | ||
| 95 | sign2.rotation_degrees.y = 180 | ||
| 96 | sign2.text = "Masteries: %d/%d" % [mastery_count, ap.masteries_requirement] | ||
| 97 | root.get_node("/root/scene").add_child.call_deferred(sign2) | ||
| 98 | |||
| 99 | # Handle custom Mint Ending. | ||
| 100 | if ap.custom_mint_ending != "": | ||
| 101 | var panel_prefab = preload("res://objects/nodes/panel.tscn") | ||
| 102 | var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn") | ||
| 103 | |||
| 104 | var mint_ending = root.get_node("/root/scene/Components/Endings/mint_ending") | ||
| 105 | |||
| 106 | var mint_panel = panel_prefab.instantiate() | ||
| 107 | mint_panel.name = "mint_panel" | ||
| 108 | mint_panel.clue = ap.custom_mint_ending | ||
| 109 | mint_panel.symbol = "" | ||
| 110 | mint_panel.answer = ap.custom_mint_ending | ||
| 111 | mint_panel.position = Vector3(-63, 3, -29) | ||
| 112 | mint_panel.rotation_degrees = Vector3(-45, 90, 0) | ||
| 113 | root.get_node("/root/scene").add_child.call_deferred(mint_panel) | ||
| 114 | |||
| 115 | var mint_tpl = tpl_prefab.instantiate() | ||
| 116 | mint_tpl.name = "mint_tpl" | ||
| 117 | mint_tpl.teleport_point = mint_ending.position | ||
| 118 | mint_tpl.teleport_rotate = mint_ending.rotation_degrees | ||
| 119 | mint_tpl.target_path = mint_ending | ||
| 120 | mint_tpl.senders.append(NodePath("/root/scene/mint_panel")) | ||
| 121 | root.get_node("/root/scene").add_child.call_deferred(mint_tpl) | ||
| 122 | |||
| 123 | var mint_tpl2 = tpl_prefab.instantiate() | ||
| 124 | mint_tpl2.name = "mint_tpl2" | ||
| 125 | mint_tpl2.teleport_point = Vector3(0, -1000, 0) | ||
| 126 | mint_tpl2.target_path = mint_panel | ||
| 127 | mint_tpl2.senders.append(NodePath("/root/scene/mint_panel")) | ||
| 128 | root.get_node("/root/scene").add_child.call_deferred(mint_tpl2) | ||
| 129 | |||
| 130 | mint_ending.position.y = -1000 | ||
| 131 | |||
| 77 | 132 | ||
| 78 | func _set_up_mastery_listener(root, name): | 133 | func _set_up_mastery_listener(root, name): |
| 79 | var prefab = preload("res://objects/nodes/listeners/unlockReaderListener.tscn") | 134 | var prefab = preload("res://objects/nodes/listeners/unlockReaderListener.tscn") |
| diff --git a/apworld/client/player.gd b/apworld/client/player.gd index 5fac9fd..dabc15d 100644 --- a/apworld/client/player.gd +++ b/apworld/client/player.gd | |||
| @@ -13,6 +13,8 @@ func _ready(): | |||
| 13 | 13 | ||
| 14 | var ap = global.get_node("Archipelago") | 14 | var ap = global.get_node("Archipelago") |
| 15 | var gamedata = global.get_node("Gamedata") | 15 | var gamedata = global.get_node("Gamedata") |
| 16 | var map_id = gamedata.map_id_by_name.get(global.map) | ||
| 17 | var map_data = gamedata.objects.get_maps()[map_id] | ||
| 16 | 18 | ||
| 17 | compass = global.get_node("Compass") | 19 | compass = global.get_node("Compass") |
| 18 | compass.visible = ap.show_compass | 20 | compass.visible = ap.show_compass |
| @@ -26,8 +28,33 @@ func _ready(): | |||
| 26 | 28 | ||
| 27 | ap.update_job_well_done_sign() | 29 | ap.update_job_well_done_sign() |
| 28 | 30 | ||
| 31 | # Set up the RTE trigger, if there is one. | ||
| 32 | if map_data.has_rte_trigger_pos(): | ||
| 33 | var oneShotListener_prefab = preload("res://objects/nodes/listeners/oneShotListener.tscn") | ||
| 34 | var triggerArea_prefab = preload("res://objects/nodes/triggerArea.tscn") | ||
| 35 | var unlockSetterListener_prefab = preload( | ||
| 36 | "res://objects/nodes/listeners/unlockSetterListener.tscn" | ||
| 37 | ) | ||
| 38 | |||
| 39 | var triggerArea = triggerArea_prefab.instantiate() | ||
| 40 | triggerArea.name = "rte_triggerArea" | ||
| 41 | triggerArea.position = gamedata.vec3d_to_vector3(map_data.get_rte_trigger_pos()) | ||
| 42 | triggerArea.scale = gamedata.vec3d_to_vector3(map_data.get_rte_trigger_scale()) | ||
| 43 | get_parent().add_child.call_deferred(triggerArea) | ||
| 44 | |||
| 45 | var osl = oneShotListener_prefab.instantiate() | ||
| 46 | osl.name = "rte_osl" | ||
| 47 | osl.senders.append(NodePath("/root/scene/rte_triggerArea")) | ||
| 48 | get_parent().add_child.call_deferred(osl) | ||
| 49 | |||
| 50 | var usl = unlockSetterListener_prefab.instantiate() | ||
| 51 | usl.name = "rte_usl" | ||
| 52 | usl.key = "rte_%s" % global.map | ||
| 53 | usl.value = "unlocked" | ||
| 54 | usl.senders.append(NodePath("/root/scene/rte_osl")) | ||
| 55 | get_parent().add_child.call_deferred(usl) | ||
| 56 | |||
| 29 | # Set up door locations. | 57 | # Set up door locations. |
| 30 | var map_id = gamedata.map_id_by_name.get(global.map) | ||
| 31 | for door in gamedata.objects.get_doors(): | 58 | for door in gamedata.objects.get_doors(): |
| 32 | if door.get_map_id() != map_id: | 59 | if door.get_map_id() != map_id: |
| 33 | continue | 60 | continue |
| @@ -169,6 +196,17 @@ func _ready(): | |||
| 169 | minimap.visible = ap.show_minimap | 196 | minimap.visible = ap.show_minimap |
| 170 | get_parent().add_child.call_deferred(minimap) | 197 | get_parent().add_child.call_deferred(minimap) |
| 171 | 198 | ||
| 199 | if ap.music_mapping.has(global.map): | ||
| 200 | var song_setter = get_node_or_null("/root/scene/songSetter") | ||
| 201 | if song_setter: | ||
| 202 | song_setter.song_name = ap.music_mapping[global.map] | ||
| 203 | else: | ||
| 204 | var song_setter_prefab = preload("res://objects/nodes/songSetter.tscn") | ||
| 205 | song_setter = song_setter_prefab.instantiate() | ||
| 206 | song_setter.name = "songSetter" | ||
| 207 | song_setter.song_name = ap.music_mapping[global.map] | ||
| 208 | get_parent().add_child.call_deferred(song_setter) | ||
| 209 | |||
| 172 | super._ready() | 210 | super._ready() |
| 173 | 211 | ||
| 174 | await get_tree().process_frame | 212 | await get_tree().process_frame |
| diff --git a/apworld/client/rteMenu.gd b/apworld/client/rteMenu.gd index 5882d77..519f09f 100644 --- a/apworld/client/rteMenu.gd +++ b/apworld/client/rteMenu.gd | |||
| @@ -1,5 +1,7 @@ | |||
| 1 | extends "res://scripts/ui/rteMenu.gd" | 1 | extends "res://scripts/ui/rteMenu.gd" |
| 2 | 2 | ||
| 3 | var buttons = [] | ||
| 4 | |||
| 3 | 5 | ||
| 4 | func _readier(): | 6 | func _readier(): |
| 5 | var ap = global.get_node("Archipelago") | 7 | var ap = global.get_node("Archipelago") |
| @@ -8,5 +10,58 @@ func _readier(): | |||
| 8 | get_node("rte_daedalus").show() | 10 | get_node("rte_daedalus").show() |
| 9 | 11 | ||
| 10 | switcher.preload_map("res://objects/scenes/daedalus.tscn") | 12 | switcher.preload_map("res://objects/scenes/daedalus.tscn") |
| 13 | elif !ap.rte_mapping.is_empty(): | ||
| 14 | buttons = [$rte_the_plaza, $rte_the_gallery, $rte_daedalus, $rte_control_center] | ||
| 15 | for i in range(4): | ||
| 16 | buttons[i].name = "button_%d" % i | ||
| 17 | for i in range(4): | ||
| 18 | _setupButton(buttons[i], ap.rte_mapping[i]) | ||
| 19 | |||
| 20 | refreshButtons() | ||
| 11 | else: | 21 | else: |
| 12 | super()._readier() | 22 | super()._readier() |
| 23 | |||
| 24 | |||
| 25 | func _setupButton(button, map_name): | ||
| 26 | switcher.preload_map("res://objects/scenes/%s.tscn" % map_name) | ||
| 27 | |||
| 28 | button.hide() | ||
| 29 | button.text = map_name.replace("_", " ") | ||
| 30 | button.name = "rte_%s" % map_name | ||
| 31 | button.autowrap_mode = TextServer.AUTOWRAP_WORD | ||
| 32 | |||
| 33 | var ap = global.get_node("Archipelago") | ||
| 34 | if ( | ||
| 35 | ap.fast_travel_access == ap.kFAST_TRAVEL_ACCESS_VANILLA | ||
| 36 | and !unlocks.data.has("rte_%s" % map_name) | ||
| 37 | ): | ||
| 38 | unlocks.data["rte_%s" % map_name] = "" | ||
| 39 | |||
| 40 | |||
| 41 | func refreshButtons(): | ||
| 42 | var ap = global.get_node("Archipelago") | ||
| 43 | if ap.rte_mapping.is_empty(): | ||
| 44 | return | ||
| 45 | |||
| 46 | for i in range(4): | ||
| 47 | if _shouldShowButton(ap.rte_mapping[i]): | ||
| 48 | buttons[i].show() | ||
| 49 | else: | ||
| 50 | buttons[i].hide() | ||
| 51 | |||
| 52 | |||
| 53 | func _shouldShowButton(map_name): | ||
| 54 | var ap = global.get_node("Archipelago") | ||
| 55 | |||
| 56 | if ap.fast_travel_access == ap.kFAST_TRAVEL_ACCESS_VANILLA: | ||
| 57 | return unlocks.data["rte_%s" % map_name] == "unlocked" | ||
| 58 | elif ap.fast_travel_access == ap.kFAST_TRAVEL_ACCESS_UNLOCKED: | ||
| 59 | return true | ||
| 60 | elif ap.fast_travel_access == ap.kFAST_TRAVEL_ACCESS_ITEMS: | ||
| 61 | var gamedata = global.get_node("Gamedata") | ||
| 62 | var map_id = gamedata.map_id_by_name[map_name] | ||
| 63 | var rte_ap_id = gamedata.objects.get_maps()[map_id].get_rte_ap_id() | ||
| 64 | |||
| 65 | return ap.client.hasItem(rte_ap_id) | ||
| 66 | |||
| 67 | return false | ||
| diff --git a/apworld/items.py b/apworld/items.py index 28158c3..143ccb1 100644 --- a/apworld/items.py +++ b/apworld/items.py | |||
| @@ -5,6 +5,8 @@ from BaseClasses import Item | |||
| 5 | class Lingo2Item(Item): | 5 | class Lingo2Item(Item): |
| 6 | game: str = "Lingo 2" | 6 | game: str = "Lingo 2" |
| 7 | 7 | ||
| 8 | is_letter: bool | ||
| 9 | |||
| 8 | 10 | ||
| 9 | SYMBOL_ITEMS: dict[data_pb2.PuzzleSymbol, str] = { | 11 | SYMBOL_ITEMS: dict[data_pb2.PuzzleSymbol, str] = { |
| 10 | data_pb2.PuzzleSymbol.SUN: "Sun Symbol", | 12 | data_pb2.PuzzleSymbol.SUN: "Sun Symbol", |
| @@ -28,4 +30,5 @@ SYMBOL_ITEMS: dict[data_pb2.PuzzleSymbol, str] = { | |||
| 28 | data_pb2.PuzzleSymbol.QUESTION: "Question Symbol", | 30 | data_pb2.PuzzleSymbol.QUESTION: "Question Symbol", |
| 29 | } | 31 | } |
| 30 | 32 | ||
| 31 | ANTI_COLLECTABLE_TRAPS: list[str] = [f"Anti {letter}" for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"] | 33 | ALL_LETTERS_UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" |
| 34 | ANTI_COLLECTABLE_TRAPS: list[str] = [f"Anti {letter}" for letter in ALL_LETTERS_UPPER] | ||
| diff --git a/apworld/locations.py b/apworld/locations.py index 3d619dc..c92215e 100644 --- a/apworld/locations.py +++ b/apworld/locations.py | |||
| @@ -1,8 +1,112 @@ | |||
| 1 | from BaseClasses import Location | 1 | from enum import Enum |
| 2 | from typing import TYPE_CHECKING | ||
| 3 | |||
| 4 | from BaseClasses import Location, Item, Region, CollectionState, Entrance | ||
| 5 | from .items import Lingo2Item | ||
| 6 | from .rules import AccessRequirements | ||
| 7 | |||
| 8 | if TYPE_CHECKING: | ||
| 9 | from . import Lingo2World | ||
| 10 | |||
| 11 | |||
| 12 | class LetterPlacementType(Enum): | ||
| 13 | ANY = 0 | ||
| 14 | DISALLOW = 1 | ||
| 15 | FORCE = 2 | ||
| 16 | |||
| 17 | |||
| 18 | def get_required_regions(reqs: AccessRequirements, world: "Lingo2World", | ||
| 19 | regions: dict[str, Region] | None) -> list[Region]: | ||
| 20 | # Replace required rooms with regions for the top level requirement, which saves looking up the regions during rule | ||
| 21 | # checking. | ||
| 22 | if regions is not None: | ||
| 23 | return [regions[room_name] for room_name in reqs.rooms] | ||
| 24 | else: | ||
| 25 | return [world.multiworld.get_region(room_name, world.player) for room_name in reqs.rooms] | ||
| 2 | 26 | ||
| 3 | 27 | ||
| 4 | class Lingo2Location(Location): | 28 | class Lingo2Location(Location): |
| 5 | game: str = "Lingo 2" | 29 | game: str = "Lingo 2" |
| 6 | 30 | ||
| 31 | reqs: AccessRequirements | None | ||
| 32 | world: "Lingo2World" | ||
| 33 | required_regions: list[Region] | ||
| 34 | |||
| 7 | port_id: int | 35 | port_id: int |
| 8 | goal: bool | 36 | goal: bool |
| 37 | letter_placement_type: LetterPlacementType | ||
| 38 | |||
| 39 | @classmethod | ||
| 40 | def non_event_location(cls, world: "Lingo2World", code: int, region: Region): | ||
| 41 | result = cls(world.player, world.static_logic.location_id_to_name[code], code, region) | ||
| 42 | result.reqs = None | ||
| 43 | result.world = world | ||
| 44 | result.required_regions = [] | ||
| 45 | |||
| 46 | return result | ||
| 47 | |||
| 48 | @classmethod | ||
| 49 | def event_location(cls, world: "Lingo2World", name: str, region: Region): | ||
| 50 | result = cls(world.player, name, None, region) | ||
| 51 | result.reqs = None | ||
| 52 | result.world = world | ||
| 53 | result.required_regions = [] | ||
| 54 | |||
| 55 | return result | ||
| 56 | |||
| 57 | def set_access_rule(self, reqs: AccessRequirements, regions: dict[str, Region] | None): | ||
| 58 | self.reqs = reqs | ||
| 59 | self.required_regions = get_required_regions(reqs, self.world, regions) | ||
| 60 | self.access_rule = self._l2_access_rule | ||
| 61 | |||
| 62 | def _l2_access_rule(self, state: CollectionState) -> bool: | ||
| 63 | if self.reqs is not None and not self.reqs.check_access(state, self.world): | ||
| 64 | return False | ||
| 65 | |||
| 66 | if not all(state.can_reach(region) for region in self.required_regions): | ||
| 67 | return False | ||
| 68 | |||
| 69 | return True | ||
| 70 | |||
| 71 | def set_up_letter_rule(self, lpt: LetterPlacementType): | ||
| 72 | self.letter_placement_type = lpt | ||
| 73 | self.item_rule = self._l2_item_rule | ||
| 74 | |||
| 75 | def _l2_item_rule(self, item: Item) -> bool: | ||
| 76 | if not isinstance(item, Lingo2Item): | ||
| 77 | return True | ||
| 78 | |||
| 79 | if self.letter_placement_type == LetterPlacementType.FORCE: | ||
| 80 | return item.is_letter | ||
| 81 | elif self.letter_placement_type == LetterPlacementType.DISALLOW: | ||
| 82 | return not item.is_letter | ||
| 83 | |||
| 84 | return True | ||
| 85 | |||
| 86 | |||
| 87 | class Lingo2Entrance(Entrance): | ||
| 88 | reqs: AccessRequirements | None | ||
| 89 | world: "Lingo2World" | ||
| 90 | required_regions: list[Region] | ||
| 91 | |||
| 92 | def __init__(self, world: "Lingo2World", description: str, region: Region): | ||
| 93 | super().__init__(world.player, description, region) | ||
| 94 | |||
| 95 | self.reqs = None | ||
| 96 | self.world = world | ||
| 97 | self.required_regions = [] | ||
| 98 | |||
| 99 | def set_access_rule(self, reqs: AccessRequirements, regions: dict[str, Region] | None): | ||
| 100 | self.reqs = reqs | ||
| 101 | self.required_regions = get_required_regions(reqs, self.world, regions) | ||
| 102 | self.access_rule = self._l2_access_rule | ||
| 103 | |||
| 104 | def _l2_access_rule(self, state: CollectionState) -> bool: | ||
| 105 | if self.reqs is not None and not self.reqs.check_access(state, self.world): | ||
| 106 | return False | ||
| 107 | |||
| 108 | if not all(state.can_reach(region) for region in self.required_regions): | ||
| 109 | return False | ||
| 110 | |||
| 111 | return True | ||
| 112 | |||
| diff --git a/apworld/options.py b/apworld/options.py index 5661351..c1eab33 100644 --- a/apworld/options.py +++ b/apworld/options.py | |||
| @@ -1,6 +1,6 @@ | |||
| 1 | from dataclasses import dataclass | 1 | from dataclasses import dataclass |
| 2 | 2 | ||
| 3 | from Options import PerGameCommonOptions, Toggle, Choice, DefaultOnToggle, Range, OptionSet | 3 | from Options import PerGameCommonOptions, Toggle, Choice, DefaultOnToggle, Range, OptionSet, FreeText |
| 4 | 4 | ||
| 5 | 5 | ||
| 6 | class ShuffleDoors(DefaultOnToggle): | 6 | class ShuffleDoors(DefaultOnToggle): |
| @@ -44,6 +44,17 @@ class ShuffleLetters(Choice): | |||
| 44 | option_item_cyan = 4 | 44 | option_item_cyan = 4 |
| 45 | 45 | ||
| 46 | 46 | ||
| 47 | class RestrictLetterPlacements(Toggle): | ||
| 48 | """ | ||
| 49 | If enabled, letter items will be shuffled among letter locations in your local world. Shuffle Letters must be set to | ||
| 50 | Progressive or Item Cyan for this to be useful. | ||
| 51 | |||
| 52 | WARNING: This option may slow down generation. Additionally, it is only reliable with Shuffle Letters set to Item | ||
| 53 | Cyan. When set to Progressive, Shuffle Doors and Shuffle Symbols must be turned off. | ||
| 54 | """ | ||
| 55 | display_name = "Restrict Letter Placements" | ||
| 56 | |||
| 57 | |||
| 47 | class ShuffleSymbols(Toggle): | 58 | class ShuffleSymbols(Toggle): |
| 48 | """ | 59 | """ |
| 49 | If enabled, 19 items will be added to the pool, representing the different symbols that can appear on a panel. | 60 | If enabled, 19 items will be added to the pool, representing the different symbols that can appear on a panel. |
| @@ -91,6 +102,27 @@ class CyanDoorBehavior(Choice): | |||
| 91 | option_item = 2 | 102 | option_item = 2 |
| 92 | 103 | ||
| 93 | 104 | ||
| 105 | class ShuffleFastTravel(Toggle): | ||
| 106 | """If enabled, the list of maps you can fast travel to is randomized, except for The Entry, which is always | ||
| 107 | accessible.""" | ||
| 108 | display_name = "Shuffle Fast Travel" | ||
| 109 | |||
| 110 | |||
| 111 | class FastTravelAccess(Choice): | ||
| 112 | """ | ||
| 113 | Controls how the fast travel buttons on the pause menu work. | ||
| 114 | |||
| 115 | - **Vanilla**: You can only fast travel to maps once you have been to them and stepped foot in the general area that | ||
| 116 | the warp would place you. This option means that fast travel has no impact on logic. | ||
| 117 | - **Unlocked**: All five fast travel maps will be available from the start. | ||
| 118 | - **Items**: Only The Entry is available from the start. The other fast travel buttons are locked behind items. | ||
| 119 | """ | ||
| 120 | display_name = "Fast Travel Access" | ||
| 121 | option_vanilla = 0 | ||
| 122 | option_unlocked = 1 | ||
| 123 | option_items = 2 | ||
| 124 | |||
| 125 | |||
| 94 | class EnableIcarus(Toggle): | 126 | class EnableIcarus(Toggle): |
| 95 | """ | 127 | """ |
| 96 | Controls whether Icarus is randomized. If disabled, which is the default, no locations or items will be created for | 128 | Controls whether Icarus is randomized. If disabled, which is the default, no locations or items will be created for |
| @@ -146,6 +178,15 @@ class DaedalusRoofAccess(Toggle): | |||
| 146 | display_name = "Allow Daedalus Roof Access" | 178 | display_name = "Allow Daedalus Roof Access" |
| 147 | 179 | ||
| 148 | 180 | ||
| 181 | class CustomMintEnding(FreeText): | ||
| 182 | """ | ||
| 183 | If not blank, this will add a new panel that must be solved before collecting Mint Ending (EXIT in the Control | ||
| 184 | Center). The panel will only require typing the text provided for this option, which means the choice of letters | ||
| 185 | here has an impact on logic. | ||
| 186 | """ | ||
| 187 | display_name = "Custom Mint Ending" | ||
| 188 | |||
| 189 | |||
| 149 | class StrictPurpleEnding(DefaultOnToggle): | 190 | class StrictPurpleEnding(DefaultOnToggle): |
| 150 | """ | 191 | """ |
| 151 | If enabled, the player will be required to have all purple (level 1) letters in order to get Purple Ending. | 192 | If enabled, the player will be required to have all purple (level 1) letters in order to get Purple Ending. |
| @@ -224,23 +265,35 @@ class TrapPercentage(Range): | |||
| 224 | default = 0 | 265 | default = 0 |
| 225 | 266 | ||
| 226 | 267 | ||
| 268 | class ShuffleMusic(Toggle): | ||
| 269 | """ | ||
| 270 | If enabled, every map will be assigned a random music track. | ||
| 271 | """ | ||
| 272 | display_name = "Shuffle Music" | ||
| 273 | |||
| 274 | |||
| 227 | @dataclass | 275 | @dataclass |
| 228 | class Lingo2Options(PerGameCommonOptions): | 276 | class Lingo2Options(PerGameCommonOptions): |
| 229 | shuffle_doors: ShuffleDoors | 277 | shuffle_doors: ShuffleDoors |
| 230 | shuffle_control_center_colors: ShuffleControlCenterColors | 278 | shuffle_control_center_colors: ShuffleControlCenterColors |
| 231 | shuffle_gallery_paintings: ShuffleGalleryPaintings | 279 | shuffle_gallery_paintings: ShuffleGalleryPaintings |
| 232 | shuffle_letters: ShuffleLetters | 280 | shuffle_letters: ShuffleLetters |
| 281 | restrict_letter_placements: RestrictLetterPlacements | ||
| 233 | shuffle_symbols: ShuffleSymbols | 282 | shuffle_symbols: ShuffleSymbols |
| 234 | shuffle_worldports: ShuffleWorldports | 283 | shuffle_worldports: ShuffleWorldports |
| 235 | keyholder_sanity: KeyholderSanity | 284 | keyholder_sanity: KeyholderSanity |
| 236 | cyan_door_behavior: CyanDoorBehavior | 285 | cyan_door_behavior: CyanDoorBehavior |
| 286 | shuffle_fast_travel: ShuffleFastTravel | ||
| 287 | fast_travel_access: FastTravelAccess | ||
| 237 | enable_icarus: EnableIcarus | 288 | enable_icarus: EnableIcarus |
| 238 | enable_gift_maps: EnableGiftMaps | 289 | enable_gift_maps: EnableGiftMaps |
| 239 | daedalus_only: DaedalusOnly | 290 | daedalus_only: DaedalusOnly |
| 240 | daedalus_roof_access: DaedalusRoofAccess | 291 | daedalus_roof_access: DaedalusRoofAccess |
| 292 | custom_mint_ending: CustomMintEnding | ||
| 241 | strict_purple_ending: StrictPurpleEnding | 293 | strict_purple_ending: StrictPurpleEnding |
| 242 | strict_cyan_ending: StrictCyanEnding | 294 | strict_cyan_ending: StrictCyanEnding |
| 243 | victory_condition: VictoryCondition | 295 | victory_condition: VictoryCondition |
| 244 | endings_requirement: EndingsRequirement | 296 | endings_requirement: EndingsRequirement |
| 245 | masteries_requirement: MasteriesRequirement | 297 | masteries_requirement: MasteriesRequirement |
| 246 | trap_percentage: TrapPercentage | 298 | trap_percentage: TrapPercentage |
| 299 | shuffle_music: ShuffleMusic | ||
| diff --git a/apworld/player_logic.py b/apworld/player_logic.py index b946296..2c3e08b 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py | |||
| @@ -5,7 +5,8 @@ from .generated import data_pb2 as data_pb2 | |||
| 5 | from .items import SYMBOL_ITEMS | 5 | from .items import SYMBOL_ITEMS |
| 6 | from typing import TYPE_CHECKING, NamedTuple | 6 | from typing import TYPE_CHECKING, NamedTuple |
| 7 | 7 | ||
| 8 | from .options import ShuffleLetters, CyanDoorBehavior, VictoryCondition | 8 | from .options import ShuffleLetters, CyanDoorBehavior, VictoryCondition, FastTravelAccess |
| 9 | from .rules import AccessRequirements | ||
| 9 | 10 | ||
| 10 | if TYPE_CHECKING: | 11 | if TYPE_CHECKING: |
| 11 | from . import Lingo2World | 12 | from . import Lingo2World |
| @@ -21,177 +22,10 @@ def calculate_letter_histogram(solution: str) -> dict[str, int]: | |||
| 21 | return histogram | 22 | return histogram |
| 22 | 23 | ||
| 23 | 24 | ||
| 24 | class AccessRequirements: | ||
| 25 | items: set[str] | ||
| 26 | progressives: dict[str, int] | ||
| 27 | rooms: set[str] | ||
| 28 | letters: dict[str, int] | ||
| 29 | cyans: bool | ||
| 30 | |||
| 31 | # This is an AND of ORs. | ||
| 32 | or_logic: list[list["AccessRequirements"]] | ||
| 33 | |||
| 34 | # When complete_at is set, at least that many of the requirements in possibilities must be accessible. This should | ||
| 35 | # only be used for doors with complete_at > 1, as or_logic is more efficient for complete_at == 1. | ||
| 36 | complete_at: int | None | ||
| 37 | possibilities: list["AccessRequirements"] | ||
| 38 | |||
| 39 | def __init__(self): | ||
| 40 | self.items = set() | ||
| 41 | self.progressives = dict() | ||
| 42 | self.rooms = set() | ||
| 43 | self.letters = dict() | ||
| 44 | self.cyans = False | ||
| 45 | self.or_logic = list() | ||
| 46 | self.complete_at = None | ||
| 47 | self.possibilities = list() | ||
| 48 | |||
| 49 | def copy(self) -> "AccessRequirements": | ||
| 50 | reqs = AccessRequirements() | ||
| 51 | reqs.items = self.items.copy() | ||
| 52 | reqs.progressives = self.progressives.copy() | ||
| 53 | reqs.rooms = self.rooms.copy() | ||
| 54 | reqs.letters = self.letters.copy() | ||
| 55 | reqs.cyans = self.cyans | ||
| 56 | reqs.or_logic = [[other_req.copy() for other_req in disjunction] for disjunction in self.or_logic] | ||
| 57 | reqs.complete_at = self.complete_at | ||
| 58 | reqs.possibilities = self.possibilities.copy() | ||
| 59 | return reqs | ||
| 60 | |||
| 61 | def merge(self, other: "AccessRequirements"): | ||
| 62 | for item in other.items: | ||
| 63 | self.items.add(item) | ||
| 64 | |||
| 65 | for item, amount in other.progressives.items(): | ||
| 66 | self.progressives[item] = max(amount, self.progressives.get(item, 0)) | ||
| 67 | |||
| 68 | for room in other.rooms: | ||
| 69 | self.rooms.add(room) | ||
| 70 | |||
| 71 | for letter, level in other.letters.items(): | ||
| 72 | self.letters[letter] = max(self.letters.get(letter, 0), level) | ||
| 73 | |||
| 74 | self.cyans = self.cyans or other.cyans | ||
| 75 | |||
| 76 | for disjunction in other.or_logic: | ||
| 77 | self.or_logic.append([sub_req.copy() for sub_req in disjunction]) | ||
| 78 | |||
| 79 | if other.complete_at is not None: | ||
| 80 | # Merging multiple requirements that use complete_at sucks, and is part of why we want to minimize use of | ||
| 81 | # it. If both requirements use complete_at, we will cheat by using the or_logic field, which supports | ||
| 82 | # conjunctions of requirements. | ||
| 83 | if self.complete_at is not None: | ||
| 84 | print("Merging requirements with complete_at > 1. This is messy and should be avoided!") | ||
| 85 | |||
| 86 | left_req = AccessRequirements() | ||
| 87 | left_req.complete_at = self.complete_at | ||
| 88 | left_req.possibilities = [sub_req.copy() for sub_req in self.possibilities] | ||
| 89 | self.or_logic.append([left_req]) | ||
| 90 | |||
| 91 | self.complete_at = None | ||
| 92 | self.possibilities = list() | ||
| 93 | |||
| 94 | right_req = AccessRequirements() | ||
| 95 | right_req.complete_at = other.complete_at | ||
| 96 | right_req.possibilities = [sub_req.copy() for sub_req in other.possibilities] | ||
| 97 | self.or_logic.append([right_req]) | ||
| 98 | else: | ||
| 99 | self.complete_at = other.complete_at | ||
| 100 | self.possibilities = [sub_req.copy() for sub_req in other.possibilities] | ||
| 101 | |||
| 102 | def is_empty(self) -> bool: | ||
| 103 | return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0 | ||
| 104 | and not self.cyans and len(self.or_logic) == 0 and self.complete_at is None) | ||
| 105 | |||
| 106 | def __eq__(self, other: "AccessRequirements"): | ||
| 107 | return (self.items == other.items and self.progressives == other.progressives and self.rooms == other.rooms and | ||
| 108 | self.letters == other.letters and self.cyans == other.cyans and self.or_logic == other.or_logic and | ||
| 109 | self.complete_at == other.complete_at and self.possibilities == other.possibilities) | ||
| 110 | |||
| 111 | def simplify(self): | ||
| 112 | resimplify = False | ||
| 113 | |||
| 114 | if len(self.or_logic) > 0: | ||
| 115 | old_or_logic = self.or_logic | ||
| 116 | |||
| 117 | def remove_redundant(sub_reqs: "AccessRequirements"): | ||
| 118 | new_reqs = sub_reqs.copy() | ||
| 119 | new_reqs.letters = {l: v for l, v in new_reqs.letters.items() if self.letters.get(l, 0) < v} | ||
| 120 | if new_reqs != sub_reqs: | ||
| 121 | return new_reqs | ||
| 122 | else: | ||
| 123 | return sub_reqs | ||
| 124 | |||
| 125 | self.or_logic = [] | ||
| 126 | for disjunction in old_or_logic: | ||
| 127 | new_disjunction = [] | ||
| 128 | for ssr in disjunction: | ||
| 129 | new_ssr = remove_redundant(ssr) | ||
| 130 | if not new_ssr.is_empty(): | ||
| 131 | new_disjunction.append(new_ssr) | ||
| 132 | else: | ||
| 133 | new_disjunction.clear() | ||
| 134 | break | ||
| 135 | if len(new_disjunction) == 1: | ||
| 136 | self.merge(new_disjunction[0]) | ||
| 137 | resimplify = True | ||
| 138 | elif len(new_disjunction) > 1: | ||
| 139 | if all(cjr == new_disjunction[0] for cjr in new_disjunction): | ||
| 140 | self.merge(new_disjunction[0]) | ||
| 141 | resimplify = True | ||
| 142 | else: | ||
| 143 | self.or_logic.append(new_disjunction) | ||
| 144 | |||
| 145 | if resimplify: | ||
| 146 | self.simplify() | ||
| 147 | |||
| 148 | def get_referenced_rooms(self): | ||
| 149 | result = set(self.rooms) | ||
| 150 | |||
| 151 | for disjunction in self.or_logic: | ||
| 152 | for sub_req in disjunction: | ||
| 153 | result = result.union(sub_req.get_referenced_rooms()) | ||
| 154 | |||
| 155 | for sub_req in self.possibilities: | ||
| 156 | result = result.union(sub_req.get_referenced_rooms()) | ||
| 157 | |||
| 158 | return result | ||
| 159 | |||
| 160 | def remove_room(self, room: str): | ||
| 161 | if room in self.rooms: | ||
| 162 | self.rooms.remove(room) | ||
| 163 | |||
| 164 | for disjunction in self.or_logic: | ||
| 165 | for sub_req in disjunction: | ||
| 166 | sub_req.remove_room(room) | ||
| 167 | |||
| 168 | for sub_req in self.possibilities: | ||
| 169 | sub_req.remove_room(room) | ||
| 170 | |||
| 171 | def __repr__(self): | ||
| 172 | parts = [] | ||
| 173 | if len(self.items) > 0: | ||
| 174 | parts.append(f"items={self.items}") | ||
| 175 | if len(self.progressives) > 0: | ||
| 176 | parts.append(f"progressives={self.progressives}") | ||
| 177 | if len(self.rooms) > 0: | ||
| 178 | parts.append(f"rooms={self.rooms}") | ||
| 179 | if len(self.letters) > 0: | ||
| 180 | parts.append(f"letters={self.letters}") | ||
| 181 | if self.cyans: | ||
| 182 | parts.append(f"cyans=True") | ||
| 183 | if len(self.or_logic) > 0: | ||
| 184 | parts.append(f"or_logic={self.or_logic}") | ||
| 185 | if self.complete_at is not None: | ||
| 186 | parts.append(f"complete_at={self.complete_at}") | ||
| 187 | if len(self.possibilities) > 0: | ||
| 188 | parts.append(f"possibilities={self.possibilities}") | ||
| 189 | return "AccessRequirements(" + ", ".join(parts) + ")" | ||
| 190 | |||
| 191 | |||
| 192 | class PlayerLocation(NamedTuple): | 25 | class PlayerLocation(NamedTuple): |
| 193 | code: int | None | 26 | code: int | None |
| 194 | reqs: AccessRequirements | 27 | reqs: AccessRequirements |
| 28 | is_letter: bool = False | ||
| 195 | 29 | ||
| 196 | 30 | ||
| 197 | class LetterBehavior(IntEnum): | 31 | class LetterBehavior(IntEnum): |
| @@ -221,6 +55,8 @@ class Lingo2PlayerLogic: | |||
| 221 | 55 | ||
| 222 | double_letter_amount: dict[str, int] | 56 | double_letter_amount: dict[str, int] |
| 223 | goal_room_id: int | 57 | goal_room_id: int |
| 58 | rte_mapping: list[int] | ||
| 59 | custom_mint_ending: str | None | ||
| 224 | 60 | ||
| 225 | def __init__(self, world: "Lingo2World"): | 61 | def __init__(self, world: "Lingo2World"): |
| 226 | self.world = world | 62 | self.world = world |
| @@ -235,6 +71,7 @@ class Lingo2PlayerLogic: | |||
| 235 | self.real_items = list() | 71 | self.real_items = list() |
| 236 | self.starting_items = list() | 72 | self.starting_items = list() |
| 237 | self.double_letter_amount = dict() | 73 | self.double_letter_amount = dict() |
| 74 | self.custom_mint_ending = None | ||
| 238 | 75 | ||
| 239 | def should_shuffle_map(game_map) -> bool | set[int]: | 76 | def should_shuffle_map(game_map) -> bool | set[int]: |
| 240 | if world.options.daedalus_only: | 77 | if world.options.daedalus_only: |
| @@ -294,6 +131,18 @@ class Lingo2PlayerLogic: | |||
| 294 | self.shuffled_doors.update(set(door.id for door in world.static_logic.objects.doors | 131 | self.shuffled_doors.update(set(door.id for door in world.static_logic.objects.doors |
| 295 | if door.map_id == game_map.id and door.daedalus_only_allow)) | 132 | if door.map_id == game_map.id and door.daedalus_only_allow)) |
| 296 | 133 | ||
| 134 | if (world.options.restrict_letter_placements | ||
| 135 | and world.options.shuffle_letters == ShuffleLetters.option_progressive | ||
| 136 | and (world.options.shuffle_doors or world.options.shuffle_symbols)): | ||
| 137 | raise OptionError(f"When Restrict Letter Placements is enabled and Shuffle Letters is set to Progressive, " | ||
| 138 | f"both Shuffle Doors and Shuffle Symbols must be disabled (Player {world.player}).") | ||
| 139 | |||
| 140 | if world.options.custom_mint_ending.value != "": | ||
| 141 | self.custom_mint_ending = ''.join(filter(str.isalpha, world.options.custom_mint_ending.value)).lower() | ||
| 142 | |||
| 143 | if len(self.custom_mint_ending) > 52: | ||
| 144 | raise OptionError(f"Custom Mint Ending should not be greater than 52 letters (Player {world.player}).") | ||
| 145 | |||
| 297 | maximum_masteries = 13 + len(world.options.enable_gift_maps.value) | 146 | maximum_masteries = 13 + len(world.options.enable_gift_maps.value) |
| 298 | if world.options.enable_icarus: | 147 | if world.options.enable_icarus: |
| 299 | maximum_masteries += 1 | 148 | maximum_masteries += 1 |
| @@ -304,6 +153,19 @@ class Lingo2PlayerLogic: | |||
| 304 | if "The Fuzzy" in world.options.enable_gift_maps.value: | 153 | if "The Fuzzy" in world.options.enable_gift_maps.value: |
| 305 | self.real_items.append("Numbers") | 154 | self.real_items.append("Numbers") |
| 306 | 155 | ||
| 156 | if world.options.shuffle_fast_travel: | ||
| 157 | travelable_maps = [map_id for map_id in self.shuffled_maps | ||
| 158 | if world.static_logic.objects.maps[map_id].HasField("rte_room")] | ||
| 159 | self.rte_mapping = world.random.sample(travelable_maps, 4) | ||
| 160 | else: | ||
| 161 | canonical_rtes = ["the_plaza", "the_gallery", "daedalus", "control_center"] | ||
| 162 | self.rte_mapping = [world.static_logic.map_id_by_name[map_name] for map_name in canonical_rtes | ||
| 163 | if world.static_logic.map_id_by_name[map_name] in self.shuffled_maps] | ||
| 164 | |||
| 165 | if world.options.fast_travel_access == FastTravelAccess.option_items: | ||
| 166 | for rte_map in self.rte_mapping: | ||
| 167 | self.real_items.append(world.static_logic.get_map_rte_item_name(rte_map)) | ||
| 168 | |||
| 307 | if self.world.options.shuffle_doors: | 169 | if self.world.options.shuffle_doors: |
| 308 | for progressive in world.static_logic.objects.progressives: | 170 | for progressive in world.static_logic.objects.progressives: |
| 309 | for i in range(0, len(progressive.doors)): | 171 | for i in range(0, len(progressive.doors)): |
| @@ -392,9 +254,11 @@ class Lingo2PlayerLogic: | |||
| 392 | if not self.should_shuffle_room(letter.room_id): | 254 | if not self.should_shuffle_room(letter.room_id): |
| 393 | continue | 255 | continue |
| 394 | 256 | ||
| 395 | self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id, | ||
| 396 | AccessRequirements())) | ||
| 397 | behavior = self.get_letter_behavior(letter.key, letter.level2) | 257 | behavior = self.get_letter_behavior(letter.key, letter.level2) |
| 258 | |||
| 259 | self.locations_by_room.setdefault(letter.room_id, []).append( | ||
| 260 | PlayerLocation(letter.ap_id, AccessRequirements(), behavior == LetterBehavior.ITEM)) | ||
| 261 | |||
| 398 | if behavior == LetterBehavior.VANILLA: | 262 | if behavior == LetterBehavior.VANILLA: |
| 399 | if not world.for_tracker: | 263 | if not world.for_tracker: |
| 400 | letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}" | 264 | letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}" |
| diff --git a/apworld/regions.py b/apworld/regions.py index 2f9b571..313fd02 100644 --- a/apworld/regions.py +++ b/apworld/regions.py | |||
| @@ -4,9 +4,9 @@ import BaseClasses | |||
| 4 | from BaseClasses import Region, ItemClassification, Entrance | 4 | from BaseClasses import Region, ItemClassification, Entrance |
| 5 | from entrance_rando import randomize_entrances | 5 | from entrance_rando import randomize_entrances |
| 6 | from .items import Lingo2Item | 6 | from .items import Lingo2Item |
| 7 | from .locations import Lingo2Location | 7 | from .locations import Lingo2Location, LetterPlacementType, Lingo2Entrance |
| 8 | from .options import FastTravelAccess | ||
| 8 | from .player_logic import AccessRequirements | 9 | from .player_logic import AccessRequirements |
| 9 | from .rules import make_location_lambda | ||
| 10 | 10 | ||
| 11 | if TYPE_CHECKING: | 11 | if TYPE_CHECKING: |
| 12 | from . import Lingo2World | 12 | from . import Lingo2World |
| @@ -21,13 +21,17 @@ def create_locations(room, new_region: Region, world: "Lingo2World", regions: di | |||
| 21 | reqs = location.reqs.copy() | 21 | reqs = location.reqs.copy() |
| 22 | reqs.remove_room(new_region.name) | 22 | reqs.remove_room(new_region.name) |
| 23 | 23 | ||
| 24 | new_location = Lingo2Location(world.player, world.static_logic.location_id_to_name[location.code], | 24 | new_location = Lingo2Location.non_event_location(world, location.code, new_region) |
| 25 | location.code, new_region) | 25 | new_location.set_access_rule(reqs, regions) |
| 26 | new_location.access_rule = make_location_lambda(reqs, world, regions) | 26 | if world.options.restrict_letter_placements: |
| 27 | if location.is_letter: | ||
| 28 | new_location.set_up_letter_rule(LetterPlacementType.FORCE) | ||
| 29 | else: | ||
| 30 | new_location.set_up_letter_rule(LetterPlacementType.DISALLOW) | ||
| 27 | new_region.locations.append(new_location) | 31 | new_region.locations.append(new_location) |
| 28 | 32 | ||
| 29 | for event_name, item_name in world.player_logic.event_loc_item_by_room.get(room.id, {}).items(): | 33 | for event_name, item_name in world.player_logic.event_loc_item_by_room.get(room.id, {}).items(): |
| 30 | new_location = Lingo2Location(world.player, event_name, None, new_region) | 34 | new_location = Lingo2Location.event_location(world, event_name, new_region) |
| 31 | if world.for_tracker and item_name == "Victory": | 35 | if world.for_tracker and item_name == "Victory": |
| 32 | new_location.goal = True | 36 | new_location.goal = True |
| 33 | 37 | ||
| @@ -41,12 +45,11 @@ def create_locations(room, new_region: Region, world: "Lingo2World", regions: di | |||
| 41 | if port.no_shuffle: | 45 | if port.no_shuffle: |
| 42 | continue | 46 | continue |
| 43 | 47 | ||
| 44 | new_location = Lingo2Location(world.player, f"Worldport {port.id} Entered", None, new_region) | 48 | new_location = Lingo2Location.event_location(world, f"Worldport {port.id} Entered", new_region) |
| 45 | new_location.port_id = port.id | 49 | new_location.port_id = port.id |
| 46 | 50 | ||
| 47 | if port.HasField("required_door"): | 51 | if port.HasField("required_door"): |
| 48 | new_location.access_rule = \ | 52 | new_location.set_access_rule(world.player_logic.get_door_open_reqs(port.required_door), regions) |
| 49 | make_location_lambda(world.player_logic.get_door_open_reqs(port.required_door), world, regions) | ||
| 50 | 53 | ||
| 51 | new_region.locations.append(new_location) | 54 | new_region.locations.append(new_location) |
| 52 | 55 | ||
| @@ -135,6 +138,10 @@ def create_regions(world: "Lingo2World"): | |||
| 135 | if connection.HasField("cyan_ending") and connection.cyan_ending and world.options.strict_cyan_ending: | 138 | if connection.HasField("cyan_ending") and connection.cyan_ending and world.options.strict_cyan_ending: |
| 136 | world.player_logic.add_solution_reqs(reqs, "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz") | 139 | world.player_logic.add_solution_reqs(reqs, "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz") |
| 137 | 140 | ||
| 141 | if (connection.HasField("mint_ending") and connection.mint_ending | ||
| 142 | and world.player_logic.custom_mint_ending is not None): | ||
| 143 | world.player_logic.add_solution_reqs(reqs, world.player_logic.custom_mint_ending) | ||
| 144 | |||
| 138 | reqs.simplify() | 145 | reqs.simplify() |
| 139 | reqs.remove_room(from_region) | 146 | reqs.remove_room(from_region) |
| 140 | 147 | ||
| @@ -144,8 +151,8 @@ def create_regions(world: "Lingo2World"): | |||
| 144 | # what regions are dead ends. | 151 | # what regions are dead ends. |
| 145 | continue | 152 | continue |
| 146 | 153 | ||
| 147 | connection = Entrance(world.player, connection_name, regions[from_region]) | 154 | connection = Lingo2Entrance(world, connection_name, regions[from_region]) |
| 148 | connection.access_rule = make_location_lambda(reqs, world, regions) | 155 | connection.set_access_rule(reqs, regions) |
| 149 | 156 | ||
| 150 | regions[from_region].exits.append(connection) | 157 | regions[from_region].exits.append(connection) |
| 151 | connection.connect(regions[to_region]) | 158 | connection.connect(regions[to_region]) |
| @@ -153,6 +160,25 @@ def create_regions(world: "Lingo2World"): | |||
| 153 | for region in reqs.get_referenced_rooms(): | 160 | for region in reqs.get_referenced_rooms(): |
| 154 | world.multiworld.register_indirect_condition(regions[region], connection) | 161 | world.multiworld.register_indirect_condition(regions[region], connection) |
| 155 | 162 | ||
| 163 | if world.options.fast_travel_access != FastTravelAccess.option_vanilla: | ||
| 164 | for rte_map_id in world.player_logic.rte_mapping: | ||
| 165 | rte_map = world.static_logic.objects.maps[rte_map_id] | ||
| 166 | to_region = world.static_logic.get_room_region_name(rte_map.rte_room) | ||
| 167 | |||
| 168 | if to_region not in regions: | ||
| 169 | continue | ||
| 170 | |||
| 171 | connection_name = f"Return to {to_region}" | ||
| 172 | connection = Lingo2Entrance(world, connection_name, regions["Menu"]) | ||
| 173 | regions["Menu"].exits.append(connection) | ||
| 174 | connection.connect(regions[to_region]) | ||
| 175 | |||
| 176 | if world.options.fast_travel_access == FastTravelAccess.option_items: | ||
| 177 | reqs = AccessRequirements() | ||
| 178 | reqs.items.add(world.static_logic.get_map_rte_item_name(rte_map_id)) | ||
| 179 | |||
| 180 | connection.set_access_rule(reqs, regions) | ||
| 181 | |||
| 156 | world.multiworld.regions += regions.values() | 182 | world.multiworld.regions += regions.values() |
| 157 | 183 | ||
| 158 | 184 | ||
| @@ -184,13 +210,13 @@ def shuffle_entrances(world: "Lingo2World"): | |||
| 184 | from_region = world.multiworld.get_region(from_region_name, world.player) | 210 | from_region = world.multiworld.get_region(from_region_name, world.player) |
| 185 | to_region = world.multiworld.get_region(to_region_name, world.player) | 211 | to_region = world.multiworld.get_region(to_region_name, world.player) |
| 186 | 212 | ||
| 187 | connection = Entrance(world.player, f"{from_region_name} - {chosen_port.display_name}", from_region) | 213 | connection = Lingo2Entrance(world, f"{from_region_name} - {chosen_port.display_name}", from_region) |
| 188 | from_region.exits.append(connection) | 214 | from_region.exits.append(connection) |
| 189 | connection.connect(to_region) | 215 | connection.connect(to_region) |
| 190 | 216 | ||
| 191 | if chosen_port.HasField("required_door"): | 217 | if chosen_port.HasField("required_door"): |
| 192 | door_reqs = world.player_logic.get_door_open_reqs(chosen_port.required_door) | 218 | door_reqs = world.player_logic.get_door_open_reqs(chosen_port.required_door) |
| 193 | connection.access_rule = make_location_lambda(door_reqs, world, None) | 219 | connection.set_access_rule(door_reqs, None) |
| 194 | 220 | ||
| 195 | for region in door_reqs.get_referenced_rooms(): | 221 | for region in door_reqs.get_referenced_rooms(): |
| 196 | world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), | 222 | world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), |
| @@ -206,12 +232,13 @@ def shuffle_entrances(world: "Lingo2World"): | |||
| 206 | entrance = port_region.create_er_target(connection_name) | 232 | entrance = port_region.create_er_target(connection_name) |
| 207 | entrance.randomization_type = BaseClasses.EntranceType.TWO_WAY | 233 | entrance.randomization_type = BaseClasses.EntranceType.TWO_WAY |
| 208 | 234 | ||
| 209 | er_exit = port_region.create_exit(connection_name) | 235 | er_exit = Lingo2Entrance(world, connection_name, port_region) |
| 236 | port_region.exits.append(er_exit) | ||
| 210 | er_exit.randomization_type = BaseClasses.EntranceType.TWO_WAY | 237 | er_exit.randomization_type = BaseClasses.EntranceType.TWO_WAY |
| 211 | 238 | ||
| 212 | if port.HasField("required_door"): | 239 | if port.HasField("required_door"): |
| 213 | door_reqs = world.player_logic.get_door_open_reqs(port.required_door) | 240 | door_reqs = world.player_logic.get_door_open_reqs(port.required_door) |
| 214 | er_exit.access_rule = make_location_lambda(door_reqs, world, None) | 241 | er_exit.set_access_rule(door_reqs, None) |
| 215 | 242 | ||
| 216 | for region in door_reqs.get_referenced_rooms(): | 243 | for region in door_reqs.get_referenced_rooms(): |
| 217 | world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), | 244 | world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), |
| @@ -238,7 +265,7 @@ def connect_ports_from_ut(port_pairings: dict[int, int], world: "Lingo2World"): | |||
| 238 | from_region = world.multiworld.get_region(from_region_name, world.player) | 265 | from_region = world.multiworld.get_region(from_region_name, world.player) |
| 239 | to_region = world.multiworld.get_region(to_region_name, world.player) | 266 | to_region = world.multiworld.get_region(to_region_name, world.player) |
| 240 | 267 | ||
| 241 | connection = Entrance(world.player, f"{from_region_name} - {from_port.display_name}", from_region) | 268 | connection = Lingo2Entrance(world, f"{from_region_name} - {from_port.display_name}", from_region) |
| 242 | 269 | ||
| 243 | reqs = AccessRequirements() | 270 | reqs = AccessRequirements() |
| 244 | if from_port.HasField("required_door"): | 271 | if from_port.HasField("required_door"): |
| @@ -248,7 +275,7 @@ def connect_ports_from_ut(port_pairings: dict[int, int], world: "Lingo2World"): | |||
| 248 | reqs.items.add(f"Worldport {fpid} Entered") | 275 | reqs.items.add(f"Worldport {fpid} Entered") |
| 249 | 276 | ||
| 250 | if not reqs.is_empty(): | 277 | if not reqs.is_empty(): |
| 251 | connection.access_rule = make_location_lambda(reqs, world, None) | 278 | connection.set_access_rule(reqs, None) |
| 252 | 279 | ||
| 253 | for region in reqs.get_referenced_rooms(): | 280 | for region in reqs.get_referenced_rooms(): |
| 254 | world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), | 281 | world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), |
| diff --git a/apworld/rules.py b/apworld/rules.py index f859e75..70a76c0 100644 --- a/apworld/rules.py +++ b/apworld/rules.py | |||
| @@ -1,66 +1,215 @@ | |||
| 1 | from collections.abc import Callable | ||
| 2 | from typing import TYPE_CHECKING | 1 | from typing import TYPE_CHECKING |
| 3 | 2 | ||
| 4 | from BaseClasses import CollectionState, Region | 3 | from BaseClasses import CollectionState |
| 5 | from .player_logic import AccessRequirements | ||
| 6 | 4 | ||
| 7 | if TYPE_CHECKING: | 5 | if TYPE_CHECKING: |
| 8 | from . import Lingo2World | 6 | from . import Lingo2World |
| 9 | 7 | ||
| 10 | 8 | ||
| 11 | def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirements, regions: list[Region], | 9 | class AccessRequirements: |
| 12 | world: "Lingo2World") -> bool: | 10 | items: set[str] |
| 13 | if not all(state.has(item, world.player) for item in reqs.items): | 11 | progressives: dict[str, int] |
| 14 | return False | 12 | rooms: set[str] |
| 13 | letters: dict[str, int] | ||
| 14 | cyans: bool | ||
| 15 | 15 | ||
| 16 | if not all(state.has(item, world.player, amount) for item, amount in reqs.progressives.items()): | 16 | # This is an AND of ORs. |
| 17 | return False | 17 | or_logic: list[list["AccessRequirements"]] |
| 18 | 18 | ||
| 19 | if not all(state.can_reach_region(region_name, world.player) for region_name in reqs.rooms): | 19 | # When complete_at is set, at least that many of the requirements in possibilities must be accessible. This should |
| 20 | return False | 20 | # only be used for doors with complete_at > 1, as or_logic is more efficient for complete_at == 1. |
| 21 | complete_at: int | None | ||
| 22 | possibilities: list["AccessRequirements"] | ||
| 21 | 23 | ||
| 22 | if not all(state.can_reach(region) for region in regions): | 24 | def __init__(self): |
| 23 | return False | 25 | self.items = set() |
| 26 | self.progressives = dict() | ||
| 27 | self.rooms = set() | ||
| 28 | self.letters = dict() | ||
| 29 | self.cyans = False | ||
| 30 | self.or_logic = list() | ||
| 31 | self.complete_at = None | ||
| 32 | self.possibilities = list() | ||
| 24 | 33 | ||
| 25 | for letter_key, letter_level in reqs.letters.items(): | 34 | def copy(self) -> "AccessRequirements": |
| 26 | if not state.has(letter_key, world.player, letter_level): | 35 | reqs = AccessRequirements() |
| 36 | reqs.items = self.items.copy() | ||
| 37 | reqs.progressives = self.progressives.copy() | ||
| 38 | reqs.rooms = self.rooms.copy() | ||
| 39 | reqs.letters = self.letters.copy() | ||
| 40 | reqs.cyans = self.cyans | ||
| 41 | reqs.or_logic = [[other_req.copy() for other_req in disjunction] for disjunction in self.or_logic] | ||
| 42 | reqs.complete_at = self.complete_at | ||
| 43 | reqs.possibilities = self.possibilities.copy() | ||
| 44 | return reqs | ||
| 45 | |||
| 46 | def merge(self, other: "AccessRequirements"): | ||
| 47 | for item in other.items: | ||
| 48 | self.items.add(item) | ||
| 49 | |||
| 50 | for item, amount in other.progressives.items(): | ||
| 51 | self.progressives[item] = max(amount, self.progressives.get(item, 0)) | ||
| 52 | |||
| 53 | for room in other.rooms: | ||
| 54 | self.rooms.add(room) | ||
| 55 | |||
| 56 | for letter, level in other.letters.items(): | ||
| 57 | self.letters[letter] = max(self.letters.get(letter, 0), level) | ||
| 58 | |||
| 59 | self.cyans = self.cyans or other.cyans | ||
| 60 | |||
| 61 | for disjunction in other.or_logic: | ||
| 62 | self.or_logic.append([sub_req.copy() for sub_req in disjunction]) | ||
| 63 | |||
| 64 | if other.complete_at is not None: | ||
| 65 | # Merging multiple requirements that use complete_at sucks, and is part of why we want to minimize use of | ||
| 66 | # it. If both requirements use complete_at, we will cheat by using the or_logic field, which supports | ||
| 67 | # conjunctions of requirements. | ||
| 68 | if self.complete_at is not None: | ||
| 69 | print("Merging requirements with complete_at > 1. This is messy and should be avoided!") | ||
| 70 | |||
| 71 | left_req = AccessRequirements() | ||
| 72 | left_req.complete_at = self.complete_at | ||
| 73 | left_req.possibilities = [sub_req.copy() for sub_req in self.possibilities] | ||
| 74 | self.or_logic.append([left_req]) | ||
| 75 | |||
| 76 | self.complete_at = None | ||
| 77 | self.possibilities = list() | ||
| 78 | |||
| 79 | right_req = AccessRequirements() | ||
| 80 | right_req.complete_at = other.complete_at | ||
| 81 | right_req.possibilities = [sub_req.copy() for sub_req in other.possibilities] | ||
| 82 | self.or_logic.append([right_req]) | ||
| 83 | else: | ||
| 84 | self.complete_at = other.complete_at | ||
| 85 | self.possibilities = [sub_req.copy() for sub_req in other.possibilities] | ||
| 86 | |||
| 87 | def is_empty(self) -> bool: | ||
| 88 | return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0 | ||
| 89 | and not self.cyans and len(self.or_logic) == 0 and self.complete_at is None) | ||
| 90 | |||
| 91 | def __eq__(self, other: "AccessRequirements"): | ||
| 92 | return (self.items == other.items and self.progressives == other.progressives and self.rooms == other.rooms and | ||
| 93 | self.letters == other.letters and self.cyans == other.cyans and self.or_logic == other.or_logic and | ||
| 94 | self.complete_at == other.complete_at and self.possibilities == other.possibilities) | ||
| 95 | |||
| 96 | def simplify(self): | ||
| 97 | resimplify = False | ||
| 98 | |||
| 99 | if len(self.or_logic) > 0: | ||
| 100 | old_or_logic = self.or_logic | ||
| 101 | |||
| 102 | def remove_redundant(sub_reqs: "AccessRequirements"): | ||
| 103 | new_reqs = sub_reqs.copy() | ||
| 104 | new_reqs.letters = {l: v for l, v in new_reqs.letters.items() if self.letters.get(l, 0) < v} | ||
| 105 | if new_reqs != sub_reqs: | ||
| 106 | return new_reqs | ||
| 107 | else: | ||
| 108 | return sub_reqs | ||
| 109 | |||
| 110 | self.or_logic = [] | ||
| 111 | for disjunction in old_or_logic: | ||
| 112 | new_disjunction = [] | ||
| 113 | for ssr in disjunction: | ||
| 114 | new_ssr = remove_redundant(ssr) | ||
| 115 | if not new_ssr.is_empty(): | ||
| 116 | new_disjunction.append(new_ssr) | ||
| 117 | else: | ||
| 118 | new_disjunction.clear() | ||
| 119 | break | ||
| 120 | if len(new_disjunction) == 1: | ||
| 121 | self.merge(new_disjunction[0]) | ||
| 122 | resimplify = True | ||
| 123 | elif len(new_disjunction) > 1: | ||
| 124 | if all(cjr == new_disjunction[0] for cjr in new_disjunction): | ||
| 125 | self.merge(new_disjunction[0]) | ||
| 126 | resimplify = True | ||
| 127 | else: | ||
| 128 | self.or_logic.append(new_disjunction) | ||
| 129 | |||
| 130 | if resimplify: | ||
| 131 | self.simplify() | ||
| 132 | |||
| 133 | def get_referenced_rooms(self): | ||
| 134 | result = set(self.rooms) | ||
| 135 | |||
| 136 | for disjunction in self.or_logic: | ||
| 137 | for sub_req in disjunction: | ||
| 138 | result = result.union(sub_req.get_referenced_rooms()) | ||
| 139 | |||
| 140 | for sub_req in self.possibilities: | ||
| 141 | result = result.union(sub_req.get_referenced_rooms()) | ||
| 142 | |||
| 143 | return result | ||
| 144 | |||
| 145 | def remove_room(self, room: str): | ||
| 146 | if room in self.rooms: | ||
| 147 | self.rooms.remove(room) | ||
| 148 | |||
| 149 | for disjunction in self.or_logic: | ||
| 150 | for sub_req in disjunction: | ||
| 151 | sub_req.remove_room(room) | ||
| 152 | |||
| 153 | for sub_req in self.possibilities: | ||
| 154 | sub_req.remove_room(room) | ||
| 155 | |||
| 156 | def __repr__(self): | ||
| 157 | parts = [] | ||
| 158 | if len(self.items) > 0: | ||
| 159 | parts.append(f"items={self.items}") | ||
| 160 | if len(self.progressives) > 0: | ||
| 161 | parts.append(f"progressives={self.progressives}") | ||
| 162 | if len(self.rooms) > 0: | ||
| 163 | parts.append(f"rooms={self.rooms}") | ||
| 164 | if len(self.letters) > 0: | ||
| 165 | parts.append(f"letters={self.letters}") | ||
| 166 | if self.cyans: | ||
| 167 | parts.append(f"cyans=True") | ||
| 168 | if len(self.or_logic) > 0: | ||
| 169 | parts.append(f"or_logic={self.or_logic}") | ||
| 170 | if self.complete_at is not None: | ||
| 171 | parts.append(f"complete_at={self.complete_at}") | ||
| 172 | if len(self.possibilities) > 0: | ||
| 173 | parts.append(f"possibilities={self.possibilities}") | ||
| 174 | return "AccessRequirements(" + ", ".join(parts) + ")" | ||
| 175 | |||
| 176 | def check_access(self, state: CollectionState, world: "Lingo2World") -> bool: | ||
| 177 | if not all(state.has(item, world.player) for item in self.items): | ||
| 27 | return False | 178 | return False |
| 28 | 179 | ||
| 29 | if reqs.cyans: | 180 | if not all(state.has(item, world.player, amount) for item, amount in self.progressives.items()): |
| 30 | if not any(state.has(letter, world.player, amount) | ||
| 31 | for letter, amount in world.player_logic.double_letter_amount.items()): | ||
| 32 | return False | 181 | return False |
| 33 | 182 | ||
| 34 | if len(reqs.or_logic) > 0: | 183 | if not all(state.can_reach_region(region_name, world.player) for region_name in self.rooms): |
| 35 | if not all(any(lingo2_can_satisfy_requirements(state, sub_reqs, [], world) for sub_reqs in subjunction) | ||
| 36 | for subjunction in reqs.or_logic): | ||
| 37 | return False | 184 | return False |
| 38 | 185 | ||
| 39 | if reqs.complete_at is not None: | 186 | for letter_key, letter_level in self.letters.items(): |
| 40 | completed = 0 | 187 | if not state.has(letter_key, world.player, letter_level): |
| 41 | checked = 0 | 188 | return False |
| 42 | for possibility in reqs.possibilities: | 189 | |
| 43 | checked += 1 | 190 | if self.cyans: |
| 44 | if lingo2_can_satisfy_requirements(state, possibility, [], world): | 191 | if not any(state.has(letter, world.player, amount) |
| 45 | completed += 1 | 192 | for letter, amount in world.player_logic.double_letter_amount.items()): |
| 46 | if completed >= reqs.complete_at: | 193 | return False |
| 47 | break | 194 | |
| 48 | elif len(reqs.possibilities) - checked + completed < reqs.complete_at: | 195 | if len(self.or_logic) > 0: |
| 49 | # There aren't enough remaining possibilities for the check to pass. | 196 | if not all(any(sub_reqs.check_access(state, world) for sub_reqs in subjunction) |
| 197 | for subjunction in self.or_logic): | ||
| 198 | return False | ||
| 199 | |||
| 200 | if self.complete_at is not None: | ||
| 201 | completed = 0 | ||
| 202 | checked = 0 | ||
| 203 | for possibility in self.possibilities: | ||
| 204 | checked += 1 | ||
| 205 | if possibility.check_access(state, world): | ||
| 206 | completed += 1 | ||
| 207 | if completed >= self.complete_at: | ||
| 208 | break | ||
| 209 | elif len(self.possibilities) - checked + completed < self.complete_at: | ||
| 210 | # There aren't enough remaining possibilities for the check to pass. | ||
| 211 | return False | ||
| 212 | if completed < self.complete_at: | ||
| 50 | return False | 213 | return False |
| 51 | if completed < reqs.complete_at: | ||
| 52 | return False | ||
| 53 | 214 | ||
| 54 | return True | 215 | return True |
| 55 | |||
| 56 | def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World", | ||
| 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 | ||
| 59 | # checking. | ||
| 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] | ||
| 64 | new_reqs = reqs.copy() | ||
| 65 | new_reqs.rooms.clear() | ||
| 66 | return lambda state: lingo2_can_satisfy_requirements(state, new_reqs, required_regions, world) | ||
| diff --git a/apworld/static_logic.py b/apworld/static_logic.py index 715178e..48ad78e 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py | |||
| @@ -18,6 +18,8 @@ class Lingo2StaticLogic: | |||
| 18 | door_id_by_ap_id: dict[int, int] | 18 | door_id_by_ap_id: dict[int, int] |
| 19 | port_id_by_ap_id: dict[int, int] | 19 | port_id_by_ap_id: dict[int, int] |
| 20 | 20 | ||
| 21 | map_id_by_name: dict[str, int] | ||
| 22 | |||
| 21 | def __init__(self): | 23 | def __init__(self): |
| 22 | self.item_id_to_name = {} | 24 | self.item_id_to_name = {} |
| 23 | self.location_id_to_name = {} | 25 | self.location_id_to_name = {} |
| @@ -73,12 +75,18 @@ class Lingo2StaticLogic: | |||
| 73 | self.item_id_to_name[self.objects.special_ids["A Job Well Done"]] = "A Job Well Done" | 75 | self.item_id_to_name[self.objects.special_ids["A Job Well Done"]] = "A Job Well Done" |
| 74 | self.item_id_to_name[self.objects.special_ids["Numbers"]] = "Numbers" | 76 | self.item_id_to_name[self.objects.special_ids["Numbers"]] = "Numbers" |
| 75 | 77 | ||
| 78 | self.item_name_groups["Symbols"] = [] | ||
| 76 | for symbol_name in SYMBOL_ITEMS.values(): | 79 | for symbol_name in SYMBOL_ITEMS.values(): |
| 77 | self.item_id_to_name[self.objects.special_ids[symbol_name]] = symbol_name | 80 | self.item_id_to_name[self.objects.special_ids[symbol_name]] = symbol_name |
| 81 | self.item_name_groups["Symbols"].append(symbol_name) | ||
| 78 | 82 | ||
| 79 | for trap_name in ANTI_COLLECTABLE_TRAPS: | 83 | for trap_name in ANTI_COLLECTABLE_TRAPS: |
| 80 | self.item_id_to_name[self.objects.special_ids[trap_name]] = trap_name | 84 | self.item_id_to_name[self.objects.special_ids[trap_name]] = trap_name |
| 81 | 85 | ||
| 86 | for game_map in self.objects.maps: | ||
| 87 | if game_map.HasField("rte_room"): | ||
| 88 | self.item_id_to_name[game_map.rte_ap_id] = self.get_map_rte_item_name(game_map.id) | ||
| 89 | |||
| 82 | self.item_name_to_id = {name: ap_id for ap_id, name in self.item_id_to_name.items()} | 90 | self.item_name_to_id = {name: ap_id for ap_id, name in self.item_id_to_name.items()} |
| 83 | self.location_name_to_id = {name: ap_id for ap_id, name in self.location_id_to_name.items()} | 91 | self.location_name_to_id = {name: ap_id for ap_id, name in self.location_id_to_name.items()} |
| 84 | 92 | ||
| @@ -90,6 +98,8 @@ class Lingo2StaticLogic: | |||
| 90 | self.door_id_by_ap_id = {door.ap_id: door.id for door in self.objects.doors if door.HasField("ap_id")} | 98 | self.door_id_by_ap_id = {door.ap_id: door.id for door in self.objects.doors if door.HasField("ap_id")} |
| 91 | self.port_id_by_ap_id = {port.ap_id: port.id for port in self.objects.ports if port.HasField("ap_id")} | 99 | self.port_id_by_ap_id = {port.ap_id: port.id for port in self.objects.ports if port.HasField("ap_id")} |
| 92 | 100 | ||
| 101 | self.map_id_by_name = {game_map.name: game_map.id for game_map in self.objects.maps} | ||
| 102 | |||
| 93 | def get_door_item_name(self, door: data_pb2.Door) -> str: | 103 | def get_door_item_name(self, door: data_pb2.Door) -> str: |
| 94 | return f"{self.get_map_object_map_name(door)} - {door.name}" | 104 | return f"{self.get_map_object_map_name(door)} - {door.name}" |
| 95 | 105 | ||
| @@ -177,6 +187,10 @@ class Lingo2StaticLogic: | |||
| 177 | def get_room_object_map_id(self, obj) -> int: | 187 | def get_room_object_map_id(self, obj) -> int: |
| 178 | return self.objects.rooms[obj.room_id].map_id | 188 | return self.objects.rooms[obj.room_id].map_id |
| 179 | 189 | ||
| 190 | def get_map_rte_item_name(self, map_id: int) -> str: | ||
| 191 | game_map = self.objects.maps[map_id] | ||
| 192 | return f"Return to {game_map.display_name}" | ||
| 193 | |||
| 180 | def get_data_version(self) -> list[int]: | 194 | def get_data_version(self) -> list[int]: |
| 181 | version = self.objects.version | 195 | version = self.objects.version |
| 182 | return [version.major, version.minor, version.patch] | 196 | return [version.major, version.minor, version.patch] |
| diff --git a/apworld/tracker.py b/apworld/tracker.py index a84c3f8..3e1cafb 100644 --- a/apworld/tracker.py +++ b/apworld/tracker.py | |||
| @@ -44,6 +44,10 @@ class Tracker: | |||
| 44 | for k, t in Lingo2Options.type_hints.items()}) | 44 | for k, t in Lingo2Options.type_hints.items()}) |
| 45 | 45 | ||
| 46 | self.world.generate_early() | 46 | self.world.generate_early() |
| 47 | |||
| 48 | self.world.player_logic.rte_mapping = [self.world.static_logic.map_id_by_name[map_name] | ||
| 49 | for map_name in slot_data.get("rte", [])] | ||
| 50 | |||
| 47 | self.world.create_regions() | 51 | self.world.create_regions() |
| 48 | 52 | ||
| 49 | if self.world.options.shuffle_worldports: | 53 | if self.world.options.shuffle_worldports: |
