about summary refs log tree commit diff stats
path: root/apworld
diff options
context:
space:
mode:
Diffstat (limited to 'apworld')
-rw-r--r--apworld/__init__.py13
-rw-r--r--apworld/client/gamedata.gd8
-rw-r--r--apworld/client/manager.gd31
-rw-r--r--apworld/client/maps/control_center.gd55
-rw-r--r--apworld/client/player.gd40
-rw-r--r--apworld/client/rteMenu.gd55
-rw-r--r--apworld/items.py5
-rw-r--r--apworld/locations.py27
-rw-r--r--apworld/options.py32
-rw-r--r--apworld/player_logic.py21
-rw-r--r--apworld/regions.py11
-rw-r--r--apworld/static_logic.py2
12 files changed, 291 insertions, 9 deletions
diff --git a/apworld/__init__.py b/apworld/__init__.py index 42350bc..6b5338e 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py
@@ -7,7 +7,7 @@ from BaseClasses import ItemClassification, Item, Tutorial
7from Options import OptionError 7from Options import OptionError
8from settings import Group, UserFilePath 8from settings import Group, UserFilePath
9from worlds.AutoWorld import WebWorld, World 9from worlds.AutoWorld import WebWorld, World
10from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS 10from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS, ALL_LETTERS_UPPER
11from .options import Lingo2Options 11from .options import Lingo2Options
12from .player_logic import Lingo2PlayerLogic 12from .player_logic import Lingo2PlayerLogic
13from .regions import create_regions, shuffle_entrances, connect_ports_from_ut 13from .regions import create_regions, shuffle_entrances, connect_ports_from_ut
@@ -70,6 +70,9 @@ 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):
74 if hasattr(self.multiworld, "re_gen_passthrough") and "Lingo 2" in self.multiworld.re_gen_passthrough: 77 if hasattr(self.multiworld, "re_gen_passthrough") and "Lingo 2" in self.multiworld.re_gen_passthrough:
75 self.player_logic.rte_mapping = [self.world.static_logic.map_id_by_name[map_name] 78 self.player_logic.rte_mapping = [self.world.static_logic.map_id_by_name[map_name]
@@ -128,11 +131,14 @@ class Lingo2World(World):
128 self.push_precollected(self.create_item(name)) 131 self.push_precollected(self.create_item(name))
129 132
130 def create_item(self, name: str) -> Item: 133 def create_item(self, name: str) -> Item:
131 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
132 ItemClassification.trap if name in ANTI_COLLECTABLE_TRAPS else 135 ItemClassification.trap if name in ANTI_COLLECTABLE_TRAPS else
133 ItemClassification.progression, 136 ItemClassification.progression,
134 self.item_name_to_id.get(name), self.player) 137 self.item_name_to_id.get(name), self.player)
135 138
139 item.is_letter = (name in ALL_LETTERS_UPPER)
140 return item
141
136 def set_rules(self): 142 def set_rules(self):
137 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)
138 144
@@ -151,6 +157,7 @@ class Lingo2World(World):
151 "shuffle_doors", 157 "shuffle_doors",
152 "shuffle_gallery_paintings", 158 "shuffle_gallery_paintings",
153 "shuffle_letters", 159 "shuffle_letters",
160 "shuffle_music",
154 "shuffle_symbols", 161 "shuffle_symbols",
155 "shuffle_worldports", 162 "shuffle_worldports",
156 "strict_cyan_ending", 163 "strict_cyan_ending",
@@ -160,7 +167,9 @@ class Lingo2World(World):
160 167
161 slot_data: dict[str, object] = { 168 slot_data: dict[str, object] = {
162 **self.options.as_dict(*slot_options), 169 **self.options.as_dict(*slot_options),
170 "custom_mint_ending": self.player_logic.custom_mint_ending or "",
163 "rte": [self.static_logic.objects.maps[map_id].name for map_id in self.player_logic.rte_mapping], 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),
164 "version": self.static_logic.get_data_version(), 173 "version": self.static_logic.get_data_version(),
165 } 174 }
166 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 = {}
16var location_name_by_id = {} 16var location_name_by_id = {}
17var ending_display_name_by_name = {} 17var ending_display_name_by_name = {}
18var port_id_by_ap_id = {} 18var port_id_by_ap_id = {}
19var map_id_by_rte_ap_id = {}
19 20
20var kSYMBOL_ITEMS 21var 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
309func 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
46const kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER = 1 46const kCYAN_DOOR_BEHAVIOR_DOUBLE_LETTER = 1
47const kCYAN_DOOR_BEHAVIOR_ITEM = 2 47const kCYAN_DOOR_BEHAVIOR_ITEM = 2
48 48
49const kFAST_TRAVEL_ACCESS_VANILLA = 0
50const kFAST_TRAVEL_ACCESS_UNLOCKED = 1
51const kFAST_TRAVEL_ACCESS_ITEMS = 2
52
49const kEndingNameByVictoryValue = { 53const kEndingNameByVictoryValue = {
50 0: "GRAY", 54 0: "GRAY",
51 1: "PURPLE", 55 1: "PURPLE",
@@ -63,21 +67,26 @@ const kEndingNameByVictoryValue = {
63} 67}
64 68
65var apworld_version = [0, 0, 0] 69var apworld_version = [0, 0, 0]
70var custom_mint_ending = ""
66var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2 71var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2
67var daedalus_only = false 72var daedalus_only = false
68var daedalus_roof_access = false 73var daedalus_roof_access = false
69var enable_gift_maps = [] 74var enable_gift_maps = []
70var enable_icarus = false 75var enable_icarus = false
71var endings_requirement = 0 76var endings_requirement = 0
77var fast_travel_access = 0
72var keyholder_sanity = false 78var keyholder_sanity = false
73var masteries_requirement = 0 79var masteries_requirement = 0
80var music_mapping = {}
74var port_pairings = {} 81var port_pairings = {}
82var rte_mapping = []
75var shuffle_control_center_colors = false 83var shuffle_control_center_colors = false
76var shuffle_doors = false 84var shuffle_doors = false
77var shuffle_gallery_paintings = false 85var shuffle_gallery_paintings = false
78var shuffle_letters = kSHUFFLE_LETTERS_VANILLA 86var shuffle_letters = kSHUFFLE_LETTERS_VANILLA
79var shuffle_symbols = false 87var shuffle_symbols = false
80var shuffle_worldports = false 88var shuffle_worldports = false
89var slot_rng = null
81var strict_cyan_ending = false 90var strict_cyan_ending = false
82var strict_purple_ending = false 91var strict_purple_ending = false
83var victory_condition = -1 92var 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
78func _set_up_mastery_listener(root, name): 133func _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 @@
1extends "res://scripts/ui/rteMenu.gd" 1extends "res://scripts/ui/rteMenu.gd"
2 2
3var buttons = []
4
3 5
4func _readier(): 6func _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
25func _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
41func 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
53func _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
5class Lingo2Item(Item): 5class Lingo2Item(Item):
6 game: str = "Lingo 2" 6 game: str = "Lingo 2"
7 7
8 is_letter: bool
9
8 10
9SYMBOL_ITEMS: dict[data_pb2.PuzzleSymbol, str] = { 11SYMBOL_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
31ANTI_COLLECTABLE_TRAPS: list[str] = [f"Anti {letter}" for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"] 33ALL_LETTERS_UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
34ANTI_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..174a0dd 100644 --- a/apworld/locations.py +++ b/apworld/locations.py
@@ -1,4 +1,13 @@
1from BaseClasses import Location 1from enum import Enum
2
3from BaseClasses import Location, Item
4from .items import Lingo2Item
5
6
7class LetterPlacementType(Enum):
8 ANY = 0
9 DISALLOW = 1
10 FORCE = 2
2 11
3 12
4class Lingo2Location(Location): 13class Lingo2Location(Location):
@@ -6,3 +15,19 @@ class Lingo2Location(Location):
6 15
7 port_id: int 16 port_id: int
8 goal: bool 17 goal: bool
18 letter_placement_type: LetterPlacementType
19
20 def set_up_letter_rule(self, lpt: LetterPlacementType):
21 self.letter_placement_type = lpt
22 self.item_rule = self._l2_item_rule
23
24 def _l2_item_rule(self, item: Item) -> bool:
25 if not isinstance(item, Lingo2Item):
26 return True
27
28 if self.letter_placement_type == LetterPlacementType.FORCE:
29 return item.is_letter
30 elif self.letter_placement_type == LetterPlacementType.DISALLOW:
31 return not item.is_letter
32
33 return True
diff --git a/apworld/options.py b/apworld/options.py index 063af21..c1eab33 100644 --- a/apworld/options.py +++ b/apworld/options.py
@@ -1,6 +1,6 @@
1from dataclasses import dataclass 1from dataclasses import dataclass
2 2
3from Options import PerGameCommonOptions, Toggle, Choice, DefaultOnToggle, Range, OptionSet 3from Options import PerGameCommonOptions, Toggle, Choice, DefaultOnToggle, Range, OptionSet, FreeText
4 4
5 5
6class ShuffleDoors(DefaultOnToggle): 6class ShuffleDoors(DefaultOnToggle):
@@ -44,6 +44,17 @@ class ShuffleLetters(Choice):
44 option_item_cyan = 4 44 option_item_cyan = 4
45 45
46 46
47class 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
47class ShuffleSymbols(Toggle): 58class 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.
@@ -167,6 +178,15 @@ class DaedalusRoofAccess(Toggle):
167 display_name = "Allow Daedalus Roof Access" 178 display_name = "Allow Daedalus Roof Access"
168 179
169 180
181class 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
170class StrictPurpleEnding(DefaultOnToggle): 190class StrictPurpleEnding(DefaultOnToggle):
171 """ 191 """
172 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.
@@ -245,12 +265,20 @@ class TrapPercentage(Range):
245 default = 0 265 default = 0
246 266
247 267
268class ShuffleMusic(Toggle):
269 """
270 If enabled, every map will be assigned a random music track.
271 """
272 display_name = "Shuffle Music"
273
274
248@dataclass 275@dataclass
249class Lingo2Options(PerGameCommonOptions): 276class Lingo2Options(PerGameCommonOptions):
250 shuffle_doors: ShuffleDoors 277 shuffle_doors: ShuffleDoors
251 shuffle_control_center_colors: ShuffleControlCenterColors 278 shuffle_control_center_colors: ShuffleControlCenterColors
252 shuffle_gallery_paintings: ShuffleGalleryPaintings 279 shuffle_gallery_paintings: ShuffleGalleryPaintings
253 shuffle_letters: ShuffleLetters 280 shuffle_letters: ShuffleLetters
281 restrict_letter_placements: RestrictLetterPlacements
254 shuffle_symbols: ShuffleSymbols 282 shuffle_symbols: ShuffleSymbols
255 shuffle_worldports: ShuffleWorldports 283 shuffle_worldports: ShuffleWorldports
256 keyholder_sanity: KeyholderSanity 284 keyholder_sanity: KeyholderSanity
@@ -261,9 +289,11 @@ class Lingo2Options(PerGameCommonOptions):
261 enable_gift_maps: EnableGiftMaps 289 enable_gift_maps: EnableGiftMaps
262 daedalus_only: DaedalusOnly 290 daedalus_only: DaedalusOnly
263 daedalus_roof_access: DaedalusRoofAccess 291 daedalus_roof_access: DaedalusRoofAccess
292 custom_mint_ending: CustomMintEnding
264 strict_purple_ending: StrictPurpleEnding 293 strict_purple_ending: StrictPurpleEnding
265 strict_cyan_ending: StrictCyanEnding 294 strict_cyan_ending: StrictCyanEnding
266 victory_condition: VictoryCondition 295 victory_condition: VictoryCondition
267 endings_requirement: EndingsRequirement 296 endings_requirement: EndingsRequirement
268 masteries_requirement: MasteriesRequirement 297 masteries_requirement: MasteriesRequirement
269 trap_percentage: TrapPercentage 298 trap_percentage: TrapPercentage
299 shuffle_music: ShuffleMusic
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index a02856e..ea74266 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py
@@ -192,6 +192,7 @@ class AccessRequirements:
192class PlayerLocation(NamedTuple): 192class PlayerLocation(NamedTuple):
193 code: int | None 193 code: int | None
194 reqs: AccessRequirements 194 reqs: AccessRequirements
195 is_letter: bool = False
195 196
196 197
197class LetterBehavior(IntEnum): 198class LetterBehavior(IntEnum):
@@ -222,6 +223,7 @@ class Lingo2PlayerLogic:
222 double_letter_amount: dict[str, int] 223 double_letter_amount: dict[str, int]
223 goal_room_id: int 224 goal_room_id: int
224 rte_mapping: list[int] 225 rte_mapping: list[int]
226 custom_mint_ending: str | None
225 227
226 def __init__(self, world: "Lingo2World"): 228 def __init__(self, world: "Lingo2World"):
227 self.world = world 229 self.world = world
@@ -236,6 +238,7 @@ class Lingo2PlayerLogic:
236 self.real_items = list() 238 self.real_items = list()
237 self.starting_items = list() 239 self.starting_items = list()
238 self.double_letter_amount = dict() 240 self.double_letter_amount = dict()
241 self.custom_mint_ending = None
239 242
240 def should_shuffle_map(game_map) -> bool | set[int]: 243 def should_shuffle_map(game_map) -> bool | set[int]:
241 if world.options.daedalus_only: 244 if world.options.daedalus_only:
@@ -295,6 +298,18 @@ class Lingo2PlayerLogic:
295 self.shuffled_doors.update(set(door.id for door in world.static_logic.objects.doors 298 self.shuffled_doors.update(set(door.id for door in world.static_logic.objects.doors
296 if door.map_id == game_map.id and door.daedalus_only_allow)) 299 if door.map_id == game_map.id and door.daedalus_only_allow))
297 300
301 if (world.options.restrict_letter_placements
302 and world.options.shuffle_letters == ShuffleLetters.option_progressive
303 and (world.options.shuffle_doors or world.options.shuffle_symbols)):
304 raise OptionError(f"When Restrict Letter Placements is enabled and Shuffle Letters is set to Progressive, "
305 f"both Shuffle Doors and Shuffle Symbols must be disabled (Player {world.player}).")
306
307 if world.options.custom_mint_ending.value != "":
308 self.custom_mint_ending = ''.join(filter(str.isalpha, world.options.custom_mint_ending.value)).lower()
309
310 if len(self.custom_mint_ending) > 52:
311 raise OptionError(f"Custom Mint Ending should not be greater than 52 letters (Player {world.player}).")
312
298 maximum_masteries = 13 + len(world.options.enable_gift_maps.value) 313 maximum_masteries = 13 + len(world.options.enable_gift_maps.value)
299 if world.options.enable_icarus: 314 if world.options.enable_icarus:
300 maximum_masteries += 1 315 maximum_masteries += 1
@@ -406,9 +421,11 @@ class Lingo2PlayerLogic:
406 if not self.should_shuffle_room(letter.room_id): 421 if not self.should_shuffle_room(letter.room_id):
407 continue 422 continue
408 423
409 self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id,
410 AccessRequirements()))
411 behavior = self.get_letter_behavior(letter.key, letter.level2) 424 behavior = self.get_letter_behavior(letter.key, letter.level2)
425
426 self.locations_by_room.setdefault(letter.room_id, []).append(
427 PlayerLocation(letter.ap_id, AccessRequirements(), behavior == LetterBehavior.ITEM))
428
412 if behavior == LetterBehavior.VANILLA: 429 if behavior == LetterBehavior.VANILLA:
413 if not world.for_tracker: 430 if not world.for_tracker:
414 letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}" 431 letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}"
diff --git a/apworld/regions.py b/apworld/regions.py index 500139f..3996153 100644 --- a/apworld/regions.py +++ b/apworld/regions.py
@@ -4,7 +4,7 @@ import BaseClasses
4from BaseClasses import Region, ItemClassification, Entrance 4from BaseClasses import Region, ItemClassification, Entrance
5from entrance_rando import randomize_entrances 5from entrance_rando import randomize_entrances
6from .items import Lingo2Item 6from .items import Lingo2Item
7from .locations import Lingo2Location 7from .locations import Lingo2Location, LetterPlacementType
8from .options import FastTravelAccess 8from .options import FastTravelAccess
9from .player_logic import AccessRequirements 9from .player_logic import AccessRequirements
10from .rules import make_location_lambda 10from .rules import make_location_lambda
@@ -25,6 +25,11 @@ def create_locations(room, new_region: Region, world: "Lingo2World", regions: di
25 new_location = Lingo2Location(world.player, world.static_logic.location_id_to_name[location.code], 25 new_location = Lingo2Location(world.player, world.static_logic.location_id_to_name[location.code],
26 location.code, new_region) 26 location.code, new_region)
27 new_location.access_rule = make_location_lambda(reqs, world, regions) 27 new_location.access_rule = make_location_lambda(reqs, world, regions)
28 if world.options.restrict_letter_placements:
29 if location.is_letter:
30 new_location.set_up_letter_rule(LetterPlacementType.FORCE)
31 else:
32 new_location.set_up_letter_rule(LetterPlacementType.DISALLOW)
28 new_region.locations.append(new_location) 33 new_region.locations.append(new_location)
29 34
30 for event_name, item_name in world.player_logic.event_loc_item_by_room.get(room.id, {}).items(): 35 for event_name, item_name in world.player_logic.event_loc_item_by_room.get(room.id, {}).items():
@@ -136,6 +141,10 @@ def create_regions(world: "Lingo2World"):
136 if connection.HasField("cyan_ending") and connection.cyan_ending and world.options.strict_cyan_ending: 141 if connection.HasField("cyan_ending") and connection.cyan_ending and world.options.strict_cyan_ending:
137 world.player_logic.add_solution_reqs(reqs, "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz") 142 world.player_logic.add_solution_reqs(reqs, "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz")
138 143
144 if (connection.HasField("mint_ending") and connection.mint_ending
145 and world.player_logic.custom_mint_ending is not None):
146 world.player_logic.add_solution_reqs(reqs, world.player_logic.custom_mint_ending)
147
139 reqs.simplify() 148 reqs.simplify()
140 reqs.remove_room(from_region) 149 reqs.remove_room(from_region)
141 150
diff --git a/apworld/static_logic.py b/apworld/static_logic.py index 672ae5a..48ad78e 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py
@@ -75,8 +75,10 @@ class Lingo2StaticLogic:
75 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"
76 self.item_id_to_name[self.objects.special_ids["Numbers"]] = "Numbers" 76 self.item_id_to_name[self.objects.special_ids["Numbers"]] = "Numbers"
77 77
78 self.item_name_groups["Symbols"] = []
78 for symbol_name in SYMBOL_ITEMS.values(): 79 for symbol_name in SYMBOL_ITEMS.values():
79 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)
80 82
81 for trap_name in ANTI_COLLECTABLE_TRAPS: 83 for trap_name in ANTI_COLLECTABLE_TRAPS:
82 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