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__.py28
-rw-r--r--apworld/client/allowNumbers.gd10
-rw-r--r--apworld/client/apworld_runtime.gd5
-rw-r--r--apworld/client/client.gd65
-rw-r--r--apworld/client/door.gd31
-rw-r--r--apworld/client/effects.gd32
-rw-r--r--apworld/client/gamedata.gd30
-rw-r--r--apworld/client/keyHolderResetterListener.gd2
-rw-r--r--apworld/client/keyboard.gd3
-rw-r--r--apworld/client/main.gd24
-rw-r--r--apworld/client/manager.gd142
-rw-r--r--apworld/client/maps/control_center.gd85
-rw-r--r--apworld/client/maps/daedalus.gd85
-rw-r--r--apworld/client/maps/icarus.gd38
-rw-r--r--apworld/client/maps/the_advanced.gd36
-rw-r--r--apworld/client/maps/the_charismatic.gd26
-rw-r--r--apworld/client/maps/the_crystalline.gd34
-rw-r--r--apworld/client/maps/the_entry.gd156
-rw-r--r--apworld/client/maps/the_fuzzy.gd25
-rw-r--r--apworld/client/maps/the_parthenon.gd51
-rw-r--r--apworld/client/maps/the_plaza.gd4
-rw-r--r--apworld/client/maps/the_stellar.gd30
-rw-r--r--apworld/client/maps/the_sun_temple.gd56
-rw-r--r--apworld/client/maps/the_unkempt.gd4
-rw-r--r--apworld/client/maps/the_unyielding.gd5
-rw-r--r--apworld/client/minimap.gd13
-rw-r--r--apworld/client/paintingAuto.gd43
-rw-r--r--apworld/client/player.gd191
-rw-r--r--apworld/client/settings_screen.gd10
-rw-r--r--apworld/client/source_runtime.gd4
-rw-r--r--apworld/client/textclient.gd258
-rw-r--r--apworld/client/unlockReaderListener.gd46
-rw-r--r--apworld/client/worldportListener.gd2
-rw-r--r--apworld/context.py216
-rw-r--r--apworld/options.py59
-rw-r--r--apworld/player_logic.py122
-rw-r--r--apworld/regions.py53
-rw-r--r--apworld/requirements.txt2
-rw-r--r--apworld/static_logic.py15
-rw-r--r--apworld/tracker.py41
40 files changed, 1786 insertions, 296 deletions
diff --git a/apworld/__init__.py b/apworld/__init__.py index 96f6804..3d2f075 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py
@@ -4,6 +4,7 @@ Archipelago init file for Lingo 2
4from typing import ClassVar 4from typing import ClassVar
5 5
6from BaseClasses import ItemClassification, Item, Tutorial 6from BaseClasses import ItemClassification, Item, Tutorial
7from Options import OptionError
7from settings import Group, UserFilePath 8from settings import Group, UserFilePath
8from worlds.AutoWorld import WebWorld, World 9from worlds.AutoWorld import WebWorld, World
9from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS 10from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS
@@ -11,7 +12,7 @@ from .options import Lingo2Options
11from .player_logic import Lingo2PlayerLogic 12from .player_logic import Lingo2PlayerLogic
12from .regions import create_regions, shuffle_entrances, connect_ports_from_ut 13from .regions import create_regions, shuffle_entrances, connect_ports_from_ut
13from .static_logic import Lingo2StaticLogic 14from .static_logic import Lingo2StaticLogic
14from ..LauncherComponents import Component, Type, components, launch as launch_component, icon_paths 15from worlds.LauncherComponents import Component, Type, components, launch as launch_component, icon_paths
15 16
16 17
17class Lingo2WebWorld(WebWorld): 18class Lingo2WebWorld(WebWorld):
@@ -33,6 +34,7 @@ class Lingo2Settings(Group):
33 is_exe = True 34 is_exe = True
34 35
35 exe_file: ExecutableFile = ExecutableFile() 36 exe_file: ExecutableFile = ExecutableFile()
37 start_game: bool = True
36 38
37 39
38class Lingo2World(World): 40class Lingo2World(World):
@@ -75,15 +77,18 @@ class Lingo2World(World):
75 if self.options.shuffle_worldports: 77 if self.options.shuffle_worldports:
76 if hasattr(self.multiworld, "re_gen_passthrough") and "Lingo 2" in self.multiworld.re_gen_passthrough: 78 if hasattr(self.multiworld, "re_gen_passthrough") and "Lingo 2" in self.multiworld.re_gen_passthrough:
77 slot_value = self.multiworld.re_gen_passthrough["Lingo 2"]["port_pairings"] 79 slot_value = self.multiworld.re_gen_passthrough["Lingo 2"]["port_pairings"]
78 self.port_pairings = {int(fp): int(tp) for fp, tp in slot_value.items()} 80 self.port_pairings = {
81 self.static_logic.port_id_by_ap_id[int(fp)]: self.static_logic.port_id_by_ap_id[int(tp)]
82 for fp, tp in slot_value.items()
83 }
79 84
80 connect_ports_from_ut(self.port_pairings, self) 85 connect_ports_from_ut(self.port_pairings, self)
81 else: 86 else:
82 shuffle_entrances(self) 87 shuffle_entrances(self)
83 88
84 from Utils import visualize_regions 89 #from Utils import visualize_regions
85 90
86 visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") 91 #visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml")
87 92
88 def create_items(self): 93 def create_items(self):
89 pool = [self.create_item(name) for name in self.player_logic.real_items] 94 pool = [self.create_item(name) for name in self.player_logic.real_items]
@@ -108,6 +113,11 @@ class Lingo2World(World):
108 for i in range(0, item_difference): 113 for i in range(0, item_difference):
109 pool.append(self.create_item(self.get_filler_item_name())) 114 pool.append(self.create_item(self.get_filler_item_name()))
110 115
116 if not any(ItemClassification.progression in item.classification for item in pool):
117 raise OptionError(f"Lingo 2 player {self.player} has no progression items. Please enable at least one "
118 f"option that would add progression gating to your world, such as Shuffle Doors or "
119 f"Shuffle Letters.")
120
111 self.multiworld.itempool += pool 121 self.multiworld.itempool += pool
112 122
113 def create_item(self, name: str) -> Item: 123 def create_item(self, name: str) -> Item:
@@ -123,7 +133,11 @@ class Lingo2World(World):
123 slot_options = [ 133 slot_options = [
124 "cyan_door_behavior", 134 "cyan_door_behavior",
125 "daedalus_roof_access", 135 "daedalus_roof_access",
136 "enable_gift_maps",
137 "enable_icarus",
138 "endings_requirement",
126 "keyholder_sanity", 139 "keyholder_sanity",
140 "masteries_requirement",
127 "shuffle_control_center_colors", 141 "shuffle_control_center_colors",
128 "shuffle_doors", 142 "shuffle_doors",
129 "shuffle_gallery_paintings", 143 "shuffle_gallery_paintings",
@@ -141,7 +155,11 @@ class Lingo2World(World):
141 } 155 }
142 156
143 if self.options.shuffle_worldports: 157 if self.options.shuffle_worldports:
144 slot_data["port_pairings"] = self.port_pairings 158 def get_port_ap_id(port_id):
159 return self.static_logic.objects.ports[port_id].ap_id
160
161 slot_data["port_pairings"] = {get_port_ap_id(from_id): get_port_ap_id(to_id)
162 for from_id, to_id in self.port_pairings.items()}
145 163
146 return slot_data 164 return slot_data
147 165
diff --git a/apworld/client/allowNumbers.gd b/apworld/client/allowNumbers.gd new file mode 100644 index 0000000..d958b50 --- /dev/null +++ b/apworld/client/allowNumbers.gd
@@ -0,0 +1,10 @@
1extends "res://scripts/nodes/allowNumbers.gd"
2
3
4func _readier():
5 var ap = global.get_node("Archipelago")
6 var gamedata = global.get_node("Gamedata")
7
8 var item_id = gamedata.objects.get_special_ids()["Numbers"]
9 if ap.client.getItemAmount(item_id) >= 1:
10 global.allow_numbers = true
diff --git a/apworld/client/apworld_runtime.gd b/apworld/client/apworld_runtime.gd index faf8e0c..03568bf 100644 --- a/apworld/client/apworld_runtime.gd +++ b/apworld/client/apworld_runtime.gd
@@ -15,6 +15,11 @@ func _get_true_path(path):
15 return "lingo2/client/%s" % path 15 return "lingo2/client/%s" % path
16 16
17 17
18func path_exists(path):
19 var true_path = _get_true_path(path)
20 return apworld_reader.file_exists(true_path)
21
22
18func load_script(path): 23func load_script(path):
19 var true_path = _get_true_path(path) 24 var true_path = _get_true_path(path)
20 25
diff --git a/apworld/client/client.gd b/apworld/client/client.gd index 62d7fd8..c149482 100644 --- a/apworld/client/client.gd +++ b/apworld/client/client.gd
@@ -25,6 +25,8 @@ var _slot_data = {}
25var _accessible_locations = [] 25var _accessible_locations = []
26var _accessible_worldports = [] 26var _accessible_worldports = []
27var _goal_accessible = false 27var _goal_accessible = false
28var _latched_doors = []
29var _hinted_locations = []
28 30
29signal could_not_connect 31signal could_not_connect
30signal connect_status 32signal connect_status
@@ -34,10 +36,13 @@ signal location_scout_received(location_id, item_name, player_name, flags, for_s
34signal text_message_received(message) 36signal text_message_received(message)
35signal item_sent_notification(message) 37signal item_sent_notification(message)
36signal hint_received(message) 38signal hint_received(message)
39signal door_latched(id)
37signal accessible_locations_updated 40signal accessible_locations_updated
38signal checked_locations_updated 41signal checked_locations_updated
42signal ignored_locations_updated(locations)
39signal checked_worldports_updated 43signal checked_worldports_updated
40signal keyboard_update_received 44signal keyboard_update_received
45signal hinted_locations_updated
41 46
42 47
43func _init(): 48func _init():
@@ -108,7 +113,7 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo
108 113
109 _checked_locations = [] 114 _checked_locations = []
110 for location in message["checked_locations"]: 115 for location in message["checked_locations"]:
111 _checked_locations.append(int(message["checked_locations"])) 116 _checked_locations.append(int(location))
112 117
113 client_connected.emit(_slot_data) 118 client_connected.emit(_slot_data)
114 119
@@ -158,11 +163,7 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo
158 elif cmd == "LocationInfo": 163 elif cmd == "LocationInfo":
159 for loc in message["locations"]: 164 for loc in message["locations"]:
160 location_scout_received.emit( 165 location_scout_received.emit(
161 int(loc["id"]), 166 int(loc["id"]), loc["item"], loc["player"], int(loc["flags"]), int(loc["self"])
162 loc["item"],
163 loc["player"],
164 int(loc["flags"]),
165 int(loc["for_self"])
166 ) 167 )
167 168
168 elif cmd == "AccessibleLocations": 169 elif cmd == "AccessibleLocations":
@@ -187,6 +188,35 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo
187 188
188 keyboard_update_received.emit(updates) 189 keyboard_update_received.emit(updates)
189 190
191 elif cmd == "PathReply":
192 var textclient = global.get_node("Textclient")
193 textclient.display_logical_path(
194 message["type"], int(message.get("id", null)), message["path"]
195 )
196
197 elif cmd == "UpdateLatches":
198 for id in message["latches"]:
199 var iid = int(id)
200 if not _latched_doors.has(iid):
201 _latched_doors.append(iid)
202
203 door_latched.emit(iid)
204
205 elif cmd == "SetIgnoredLocations":
206 var locs = []
207 for id in message["locations"]:
208 locs.append(int(id))
209
210 ignored_locations_updated.emit(locs)
211
212 elif cmd == "UpdateHintedLocations":
213 for id in message["locations"]:
214 var iid = int(id)
215 if !_hinted_locations.has(iid):
216 _hinted_locations.append(iid)
217
218 hinted_locations_updated.emit()
219
190 220
191func connectToServer(server, un, pw): 221func connectToServer(server, un, pw):
192 sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}]) 222 sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}])
@@ -253,6 +283,29 @@ func checkWorldport(port_id):
253 sendMessage([{"cmd": "CheckWorldport", "port_id": port_id}]) 283 sendMessage([{"cmd": "CheckWorldport", "port_id": port_id}])
254 284
255 285
286func latchDoor(id):
287 if not _latched_doors.has(id):
288 _latched_doors.append(id)
289
290 sendMessage([{"cmd": "LatchDoor", "door": id}])
291
292
293func getLogicalPath(object_type, object_id):
294 var msg = {"cmd": "GetPath", "type": object_type}
295 if object_id != null:
296 msg["id"] = object_id
297
298 sendMessage([msg])
299
300
301func addIgnoredLocation(loc_id):
302 sendMessage([{"cmd": "IgnoreLocation", "id": loc_id}])
303
304
305func removeIgnoredLocation(loc_id):
306 sendMessage([{"cmd": "UnignoreLocation", "id": loc_id}])
307
308
256func sendQuit(): 309func sendQuit():
257 sendMessage([{"cmd": "Quit"}]) 310 sendMessage([{"cmd": "Quit"}])
258 311
diff --git a/apworld/client/door.gd b/apworld/client/door.gd index 49f5728..63cfa99 100644 --- a/apworld/client/door.gd +++ b/apworld/client/door.gd
@@ -1,7 +1,9 @@
1extends "res://scripts/nodes/door.gd" 1extends "res://scripts/nodes/door.gd"
2 2
3var door_id
3var item_id 4var item_id
4var item_amount 5var item_amount
6var latched = false
5 7
6 8
7func _ready(): 9func _ready():
@@ -10,7 +12,7 @@ func _ready():
10 ) 12 )
11 13
12 var gamedata = global.get_node("Gamedata") 14 var gamedata = global.get_node("Gamedata")
13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path) 15 door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
14 if door_id != null: 16 if door_id != null:
15 var ap = global.get_node("Archipelago") 17 var ap = global.get_node("Archipelago")
16 var item_lock = ap.get_item_id_for_door(door_id) 18 var item_lock = ap.get_item_id_for_door(door_id)
@@ -27,6 +29,12 @@ func _ready():
27 self.excludeSenders = [] 29 self.excludeSenders = []
28 30
29 call_deferred("_readier") 31 call_deferred("_readier")
32 else:
33 var door_data = gamedata.objects.get_doors()[door_id]
34 if door_data.has_latch() and door_data.get_latch():
35 _check_latched.call_deferred(door_id)
36
37 latched = true
30 38
31 if global.map == "the_sun_temple": 39 if global.map == "the_sun_temple":
32 if name == "spe_EndPlatform" or name == "spe_entry_2": 40 if name == "spe_EndPlatform" or name == "spe_entry_2":
@@ -44,3 +52,24 @@ func _readier():
44 52
45 if ap.client.getItemAmount(item_id) >= item_amount: 53 if ap.client.getItemAmount(item_id) >= item_amount:
46 handleTriggered() 54 handleTriggered()
55
56
57func _check_latched(door_id):
58 var ap = global.get_node("Archipelago")
59
60 if ap.client._latched_doors.has(door_id):
61 triggered = total
62 handleTriggered()
63
64
65func handleTriggered():
66 super.handleTriggered()
67
68 if latched and ran:
69 var ap = global.get_node("Archipelago")
70 ap.client.latchDoor(door_id)
71
72
73func handleUntriggered():
74 if not latched or not ran:
75 super.handleUntriggered()
diff --git a/apworld/client/effects.gd b/apworld/client/effects.gd new file mode 100644 index 0000000..9dc1dd8 --- /dev/null +++ b/apworld/client/effects.gd
@@ -0,0 +1,32 @@
1extends CanvasLayer
2
3var _label
4
5var _disconnected = false
6
7
8func _ready():
9 _label = Label.new()
10 _label.name = "Label"
11 _label.offset_left = 20
12 _label.offset_top = 20
13 _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
14 _label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
15 _label.theme = preload("res://assets/themes/baseUI.tres")
16 _label.add_theme_font_size_override("font_size", 36)
17 add_child(_label)
18
19
20func set_connection_lost(arg):
21 _disconnected = arg
22
23 _update_label()
24
25
26func _update_label():
27 var text = []
28
29 if _disconnected:
30 text.append("Disconnected from multiworld.")
31
32 _label.text = "\n".join(text)
diff --git a/apworld/client/gamedata.gd b/apworld/client/gamedata.gd index 334d42a..d7e3136 100644 --- a/apworld/client/gamedata.gd +++ b/apworld/client/gamedata.gd
@@ -15,6 +15,7 @@ var symbol_item_ids = []
15var anti_trap_ids = {} 15var 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 = {}
18 19
19var kSYMBOL_ITEMS 20var kSYMBOL_ITEMS
20 21
@@ -72,7 +73,13 @@ func load(data_bytes):
72 73
73 if door.has_ap_id(): 74 if door.has_ap_id():
74 door_id_by_ap_id[door.get_ap_id()] = door.get_id() 75 door_id_by_ap_id[door.get_ap_id()] = door.get_id()
75 location_name_by_id[door.get_ap_id()] = _get_door_location_name(door) 76
77 if (
78 door.get_type() == SCRIPT_proto.DoorType.STANDARD
79 or door.get_type() == SCRIPT_proto.DoorType.LOCATION_ONLY
80 or door.get_type() == SCRIPT_proto.DoorType.GRAVESTONE
81 ):
82 location_name_by_id[door.get_ap_id()] = _get_door_location_name(door)
76 83
77 for painting in objects.get_paintings(): 84 for painting in objects.get_paintings():
78 var room = objects.get_rooms()[painting.get_room_id()] 85 var room = objects.get_rooms()[painting.get_room_id()]
@@ -93,6 +100,9 @@ func load(data_bytes):
93 var map_data = port_id_by_map_node_path[map.get_name()] 100 var map_data = port_id_by_map_node_path[map.get_name()]
94 map_data[port.get_path()] = port.get_id() 101 map_data[port.get_path()] = port.get_id()
95 102
103 if port.has_ap_id():
104 port_id_by_ap_id[port.get_ap_id()] = port.get_id()
105
96 for progressive in objects.get_progressives(): 106 for progressive in objects.get_progressives():
97 progressive_id_by_ap_id[progressive.get_ap_id()] = progressive.get_id() 107 progressive_id_by_ap_id[progressive.get_ap_id()] = progressive.get_id()
98 108
@@ -166,8 +176,7 @@ func get_door_ap_id(door_id):
166 176
167func get_door_map_name(door_id): 177func get_door_map_name(door_id):
168 var door = objects.get_doors()[door_id] 178 var door = objects.get_doors()[door_id]
169 var room = objects.get_rooms()[door.get_room_id()] 179 var map = objects.get_maps()[door.get_map_id()]
170 var map = objects.get_maps()[room.get_map_id()]
171 return map.get_name() 180 return map.get_name()
172 181
173 182
@@ -216,7 +225,11 @@ func _get_generated_door_location_name(door):
216 if door.get_type() != SCRIPT_proto.DoorType.STANDARD: 225 if door.get_type() != SCRIPT_proto.DoorType.STANDARD:
217 return null 226 return null
218 227
219 if door.get_keyholders().size() > 0 or door.get_endings().size() > 0 or door.has_complete_at(): 228 if (
229 door.get_keyholders().size() > 0
230 or (door.has_white_ending() and door.get_white_ending())
231 or door.has_complete_at()
232 ):
220 return null 233 return null
221 234
222 if door.get_panels().size() > 4: 235 if door.get_panels().size() > 4:
@@ -227,8 +240,11 @@ func _get_generated_door_location_name(door):
227 var panel = objects.get_panels()[panel_id.get_panel()] 240 var panel = objects.get_panels()[panel_id.get_panel()]
228 var panel_room = objects.get_rooms()[panel.get_room_id()] 241 var panel_room = objects.get_rooms()[panel.get_room_id()]
229 # It's okay if panel_display_name is not present because then it's coalesced with other unnamed areas. 242 # It's okay if panel_display_name is not present because then it's coalesced with other unnamed areas.
230 if not map_areas.has(panel_room.get_panel_display_name()): 243 var panel_display_name = ""
231 map_areas.append(panel_room.get_panel_display_name()) 244 if panel_room.has_panel_display_name():
245 panel_display_name = panel_room.get_panel_display_name()
246 if not map_areas.has(panel_display_name):
247 map_areas.append(panel_display_name)
232 248
233 if map_areas.size() > 1: 249 if map_areas.size() > 1:
234 return null 250 return null
@@ -264,7 +280,7 @@ func _get_generated_door_location_name(door):
264 280
265 281
266func _get_letter_location_name(letter): 282func _get_letter_location_name(letter):
267 var letter_level = 2 if letter.get_level2() else 1 283 var letter_level = 2 if (letter.has_level2() and letter.get_level2()) else 1
268 var letter_name = "%s%d" % [letter.get_key().to_upper(), letter_level] 284 var letter_name = "%s%d" % [letter.get_key().to_upper(), letter_level]
269 return "%s - %s" % [_get_room_object_map_name(letter), letter_name] 285 return "%s - %s" % [_get_room_object_map_name(letter), letter_name]
270 286
diff --git a/apworld/client/keyHolderResetterListener.gd b/apworld/client/keyHolderResetterListener.gd index d5300f3..9ab45f9 100644 --- a/apworld/client/keyHolderResetterListener.gd +++ b/apworld/client/keyHolderResetterListener.gd
@@ -6,3 +6,5 @@ func reset():
6 var was_removed = ap.keyboard.reset_keyholders() 6 var was_removed = ap.keyboard.reset_keyholders()
7 if was_removed: 7 if was_removed:
8 sfxPlayer.sfx_play("pickup") 8 sfxPlayer.sfx_play("pickup")
9
10 ap.client.requestSync()
diff --git a/apworld/client/keyboard.gd b/apworld/client/keyboard.gd index a59c4d0..9026c06 100644 --- a/apworld/client/keyboard.gd +++ b/apworld/client/keyboard.gd
@@ -191,9 +191,6 @@ func load_keyholders(map):
191 191
192 192
193func reset_keyholders(): 193func reset_keyholders():
194 if letters_in_keyholders.is_empty() and letters_blocked.is_empty():
195 return false
196
197 var cleared_anything = not letters_in_keyholders.is_empty() or not letters_blocked.is_empty() 194 var cleared_anything = not letters_in_keyholders.is_empty() or not letters_blocked.is_empty()
198 195
199 if keyholder_state.has(global.map): 196 if keyholder_state.has(global.map):
diff --git a/apworld/client/main.gd b/apworld/client/main.gd index 2d2e606..c90d6e7 100644 --- a/apworld/client/main.gd +++ b/apworld/client/main.gd
@@ -36,6 +36,7 @@ func _ready():
36 global.add_child(ap_instance) 36 global.add_child(ap_instance)
37 37
38 # Let's also inject any scripts we need to inject now. 38 # Let's also inject any scripts we need to inject now.
39 installScriptExtension(runtime.load_script("allowNumbers.gd"))
39 installScriptExtension(runtime.load_script("animationListener.gd")) 40 installScriptExtension(runtime.load_script("animationListener.gd"))
40 installScriptExtension(runtime.load_script("collectable.gd")) 41 installScriptExtension(runtime.load_script("collectable.gd"))
41 installScriptExtension(runtime.load_script("door.gd")) 42 installScriptExtension(runtime.load_script("door.gd"))
@@ -43,12 +44,14 @@ func _ready():
43 installScriptExtension(runtime.load_script("keyHolderChecker.gd")) 44 installScriptExtension(runtime.load_script("keyHolderChecker.gd"))
44 installScriptExtension(runtime.load_script("keyHolderResetterListener.gd")) 45 installScriptExtension(runtime.load_script("keyHolderResetterListener.gd"))
45 installScriptExtension(runtime.load_script("painting.gd")) 46 installScriptExtension(runtime.load_script("painting.gd"))
47 installScriptExtension(runtime.load_script("paintingAuto.gd"))
46 installScriptExtension(runtime.load_script("panel.gd")) 48 installScriptExtension(runtime.load_script("panel.gd"))
47 installScriptExtension(runtime.load_script("pauseMenu.gd")) 49 installScriptExtension(runtime.load_script("pauseMenu.gd"))
48 installScriptExtension(runtime.load_script("player.gd")) 50 installScriptExtension(runtime.load_script("player.gd"))
49 installScriptExtension(runtime.load_script("saver.gd")) 51 installScriptExtension(runtime.load_script("saver.gd"))
50 installScriptExtension(runtime.load_script("teleport.gd")) 52 installScriptExtension(runtime.load_script("teleport.gd"))
51 installScriptExtension(runtime.load_script("teleportListener.gd")) 53 installScriptExtension(runtime.load_script("teleportListener.gd"))
54 installScriptExtension(runtime.load_script("unlockReaderListener.gd"))
52 installScriptExtension(runtime.load_script("visibilityListener.gd")) 55 installScriptExtension(runtime.load_script("visibilityListener.gd"))
53 installScriptExtension(runtime.load_script("worldport.gd")) 56 installScriptExtension(runtime.load_script("worldport.gd"))
54 installScriptExtension(runtime.load_script("worldportListener.gd")) 57 installScriptExtension(runtime.load_script("worldportListener.gd"))
@@ -66,6 +69,11 @@ func _ready():
66 messages_instance.SCRIPT_rainbowText = runtime.load_script("rainbowText.gd") 69 messages_instance.SCRIPT_rainbowText = runtime.load_script("rainbowText.gd")
67 global.add_child(messages_instance) 70 global.add_child(messages_instance)
68 71
72 var effects_script = runtime.load_script("effects.gd")
73 var effects_instance = effects_script.new()
74 effects_instance.name = "Effects"
75 global.add_child(effects_instance)
76
69 var textclient_script = runtime.load_script("textclient.gd") 77 var textclient_script = runtime.load_script("textclient.gd")
70 var textclient_instance = textclient_script.new() 78 var textclient_instance = textclient_script.new()
71 textclient_instance.name = "Textclient" 79 textclient_instance.name = "Textclient"
@@ -77,6 +85,13 @@ func _ready():
77 compass_overlay_instance.SCRIPT_compass = runtime.load_script("compass.gd") 85 compass_overlay_instance.SCRIPT_compass = runtime.load_script("compass.gd")
78 global.add_child(compass_overlay_instance) 86 global.add_child(compass_overlay_instance)
79 87
88 unlocks.data["advanced_mastery"] = ""
89 unlocks.data["charismatic_mastery"] = ""
90 unlocks.data["crystalline_mastery"] = ""
91 unlocks.data["fuzzy_mastery"] = ""
92 unlocks.data["icarus_mastery"] = ""
93 unlocks.data["stellar_mastery"] = ""
94
80 var ap = global.get_node("Archipelago") 95 var ap = global.get_node("Archipelago")
81 var gamedata = global.get_node("Gamedata") 96 var gamedata = global.get_node("Gamedata")
82 ap.ap_connected.connect(connectionSuccessful) 97 ap.ap_connected.connect(connectionSuccessful)
@@ -218,11 +233,11 @@ func startGame():
218 233
219 unlocks.resetCollectables() 234 unlocks.resetCollectables()
220 unlocks.resetData() 235 unlocks.resetData()
236 unlocks.loadCollectables()
237 unlocks.loadData()
221 238
222 ap.setup_keys() 239 ap.setup_keys()
223 240
224 unlocks.loadCollectables()
225 unlocks.loadData()
226 unlocks.unlockKey("capslock", 1) 241 unlocks.unlockKey("capslock", 1)
227 242
228 if ap.shuffle_worldports: 243 if ap.shuffle_worldports:
@@ -231,6 +246,7 @@ func startGame():
231 settings.worldport_fades = "never" 246 settings.worldport_fades = "never"
232 247
233 clearResourceCache("res://objects/meshes/gridDoor.tscn") 248 clearResourceCache("res://objects/meshes/gridDoor.tscn")
249 clearResourceCache("res://objects/nodes/allowNumbers.tscn")
234 clearResourceCache("res://objects/nodes/collectable.tscn") 250 clearResourceCache("res://objects/nodes/collectable.tscn")
235 clearResourceCache("res://objects/nodes/door.tscn") 251 clearResourceCache("res://objects/nodes/door.tscn")
236 clearResourceCache("res://objects/nodes/keyHolder.tscn") 252 clearResourceCache("res://objects/nodes/keyHolder.tscn")
@@ -238,6 +254,7 @@ func startGame():
238 clearResourceCache("res://objects/nodes/listeners/keyHolderChecker.tscn") 254 clearResourceCache("res://objects/nodes/listeners/keyHolderChecker.tscn")
239 clearResourceCache("res://objects/nodes/listeners/keyHolderResetterListener.tscn") 255 clearResourceCache("res://objects/nodes/listeners/keyHolderResetterListener.tscn")
240 clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn") 256 clearResourceCache("res://objects/nodes/listeners/teleportListener.tscn")
257 clearResourceCache("res://objects/nodes/listeners/unlockReaderListener.tscn")
241 clearResourceCache("res://objects/nodes/listeners/visibilityListener.tscn") 258 clearResourceCache("res://objects/nodes/listeners/visibilityListener.tscn")
242 clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn") 259 clearResourceCache("res://objects/nodes/listeners/worldportListener.tscn")
243 clearResourceCache("res://objects/nodes/panel.tscn") 260 clearResourceCache("res://objects/nodes/panel.tscn")
@@ -274,6 +291,9 @@ func versionMismatchDeclined():
274 get_node("../Panel/AcceptDialog").hide() 291 get_node("../Panel/AcceptDialog").hide()
275 get_node("../Panel/connect_button").disabled = false 292 get_node("../Panel/connect_button").disabled = false
276 293
294 var ap = global.get_node("Archipelago")
295 ap.disconnect_from_ap()
296
277 297
278func historySelected(index): 298func historySelected(index):
279 var ap = global.get_node("Archipelago") 299 var ap = global.get_node("Archipelago")
diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index a5b9db0..8c981f9 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd
@@ -28,6 +28,9 @@ var _item_locks = {}
28var _inverse_item_locks = {} 28var _inverse_item_locks = {}
29var _held_letters = {} 29var _held_letters = {}
30var _letters_setup = false 30var _letters_setup = false
31var _already_connected = false
32var _ignored_locations = []
33var _map_scripts = {}
31 34
32const kSHUFFLE_LETTERS_VANILLA = 0 35const kSHUFFLE_LETTERS_VANILLA = 0
33const kSHUFFLE_LETTERS_UNLOCKED = 1 36const kSHUFFLE_LETTERS_UNLOCKED = 1
@@ -62,7 +65,11 @@ const kEndingNameByVictoryValue = {
62var apworld_version = [0, 0, 0] 65var apworld_version = [0, 0, 0]
63var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2 66var cyan_door_behavior = kCYAN_DOOR_BEHAVIOR_H2
64var daedalus_roof_access = false 67var daedalus_roof_access = false
68var enable_gift_maps = []
69var enable_icarus = false
70var endings_requirement = 0
65var keyholder_sanity = false 71var keyholder_sanity = false
72var masteries_requirement = 0
66var port_pairings = {} 73var port_pairings = {}
67var shuffle_control_center_colors = false 74var shuffle_control_center_colors = false
68var shuffle_doors = false 75var shuffle_doors = false
@@ -74,6 +81,8 @@ var strict_cyan_ending = false
74var strict_purple_ending = false 81var strict_purple_ending = false
75var victory_condition = -1 82var victory_condition = -1
76 83
84var color_by_material_path = {}
85
77signal could_not_connect 86signal could_not_connect
78signal connect_status 87signal connect_status
79signal ap_connected 88signal ap_connected
@@ -111,6 +120,20 @@ func _init():
111 if data.size() > 6: 120 if data.size() > 6:
112 show_minimap = data[6] 121 show_minimap = data[6]
113 122
123 # We need to create a mapping from material paths to the original colors of
124 # those materials. We force reload the materials, overwriting any custom
125 # textures, and create the mapping. We then reload the textures in case the
126 # player had a custom one enabled.
127 var directory = DirAccess.open("res://assets/materials")
128 for material_name in directory.get_files():
129 var material = ResourceLoader.load(
130 "res://assets/materials/" + material_name, "", ResourceLoader.CACHE_MODE_REPLACE
131 )
132
133 color_by_material_path[material.resource_path] = Color(material.albedo_color)
134
135 settings.load_user_textures()
136
114 137
115func _ready(): 138func _ready():
116 client = SCRIPT_client.new() 139 client = SCRIPT_client.new()
@@ -123,7 +146,10 @@ func _ready():
123 client.hint_received.connect(_process_hint_received) 146 client.hint_received.connect(_process_hint_received)
124 client.accessible_locations_updated.connect(_on_accessible_locations_updated) 147 client.accessible_locations_updated.connect(_on_accessible_locations_updated)
125 client.checked_locations_updated.connect(_on_checked_locations_updated) 148 client.checked_locations_updated.connect(_on_checked_locations_updated)
149 client.ignored_locations_updated.connect(_on_ignored_locations_updated)
150 client.hinted_locations_updated.connect(_on_hinted_locations_updated)
126 client.checked_worldports_updated.connect(_on_checked_worldports_updated) 151 client.checked_worldports_updated.connect(_on_checked_worldports_updated)
152 client.door_latched.connect(_on_door_latched)
127 153
128 client.could_not_connect.connect(_client_could_not_connect) 154 client.could_not_connect.connect(_client_could_not_connect)
129 client.connect_status.connect(_client_connect_status) 155 client.connect_status.connect(_client_connect_status)
@@ -178,6 +204,7 @@ func connectToServer():
178 _location_scouts = {} 204 _location_scouts = {}
179 _letters_setup = false 205 _letters_setup = false
180 _held_letters = {} 206 _held_letters = {}
207 _already_connected = false
181 208
182 client.connectToServer(ap_server, ap_user, ap_pass) 209 client.connectToServer(ap_server, ap_user, ap_pass)
183 210
@@ -187,6 +214,11 @@ func getSaveFileName():
187 214
188 215
189func disconnect_from_ap(): 216func disconnect_from_ap():
217 _already_connected = false
218
219 var effects = global.get_node("Effects")
220 effects.set_connection_lost(false)
221
190 client.disconnect_from_ap() 222 client.disconnect_from_ap()
191 223
192 224
@@ -230,6 +262,12 @@ func _process_item(item, amount):
230 if player != null: 262 if player != null:
231 player.evaluate_solvability.emit() 263 player.evaluate_solvability.emit()
232 264
265 if item_id == gamedata.objects.get_special_ids()["A Job Well Done"]:
266 update_job_well_done_sign()
267
268 if item_id == gamedata.objects.get_special_ids()["Numbers"] and global.map == "the_fuzzy":
269 global.allow_numbers = true
270
233 # Show a message about the item if it's new. 271 # Show a message about the item if it's new.
234 if int(item["index"]) > _last_new_item: 272 if int(item["index"]) > _last_new_item:
235 _last_new_item = int(item["index"]) 273 _last_new_item = int(item["index"])
@@ -340,7 +378,7 @@ func _on_accessible_locations_updated():
340func _on_checked_locations_updated(): 378func _on_checked_locations_updated():
341 var textclient_node = global.get_node("Textclient") 379 var textclient_node = global.get_node("Textclient")
342 if textclient_node != null: 380 if textclient_node != null:
343 textclient_node.update_locations() 381 textclient_node.update_locations(false)
344 382
345 383
346func _on_checked_worldports_updated(): 384func _on_checked_worldports_updated():
@@ -350,15 +388,60 @@ func _on_checked_worldports_updated():
350 textclient_node.update_worldports() 388 textclient_node.update_worldports()
351 389
352 390
391func _on_ignored_locations_updated(locations):
392 _ignored_locations = locations
393
394 var textclient_node = global.get_node("Textclient")
395 if textclient_node != null:
396 textclient_node.update_locations()
397
398
399func _on_hinted_locations_updated():
400 var textclient_node = global.get_node("Textclient")
401 if textclient_node != null:
402 textclient_node.update_locations()
403
404
405func _on_door_latched(door_id):
406 var gamedata = global.get_node("Gamedata")
407 if gamedata.get_door_map_name(door_id) != global.map:
408 return
409
410 var receivers = gamedata.get_door_receivers(door_id)
411 var scene = get_tree().get_root().get_node_or_null("scene")
412 if scene != null:
413 for receiver in receivers:
414 var rnode = scene.get_node_or_null(receiver)
415 if rnode != null:
416 rnode.handleTriggered()
417
418
353func _client_could_not_connect(message): 419func _client_could_not_connect(message):
354 could_not_connect.emit(message) 420 could_not_connect.emit(message)
355 421
422 if global.loaded:
423 var effects = global.get_node("Effects")
424 effects.set_connection_lost(true)
425
426 var messages = global.get_node("Messages")
427 messages.showMessage("Connection to multiworld lost.")
428
356 429
357func _client_connect_status(message): 430func _client_connect_status(message):
358 connect_status.emit(message) 431 connect_status.emit(message)
359 432
360 433
361func _client_connected(slot_data): 434func _client_connected(slot_data):
435 var effects = global.get_node("Effects")
436 effects.set_connection_lost(false)
437
438 if _already_connected:
439 var messages = global.get_node("Messages")
440 messages.showMessage("Reconnected to multiworld!")
441 return
442
443 _already_connected = true
444
362 var gamedata = global.get_node("Gamedata") 445 var gamedata = global.get_node("Gamedata")
363 446
364 _localdata_file = "user://archipelago_data/%s_%d" % [client._seed, client._slot] 447 _localdata_file = "user://archipelago_data/%s_%d" % [client._seed, client._slot]
@@ -381,7 +464,11 @@ func _client_connected(slot_data):
381 # Read slot data. 464 # Read slot data.
382 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0)) 465 cyan_door_behavior = int(slot_data.get("cyan_door_behavior", 0))
383 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false)) 466 daedalus_roof_access = bool(slot_data.get("daedalus_roof_access", false))
467 enable_gift_maps = slot_data.get("enable_gift_maps", [])
468 enable_icarus = bool(slot_data.get("enable_icarus", false))
469 endings_requirement = int(slot_data.get("endings_requirement", 0))
384 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false)) 470 keyholder_sanity = bool(slot_data.get("keyholder_sanity", false))
471 masteries_requirement = int(slot_data.get("masteries_requirement", 0))
385 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false)) 472 shuffle_control_center_colors = bool(slot_data.get("shuffle_control_center_colors", false))
386 shuffle_doors = bool(slot_data.get("shuffle_doors", false)) 473 shuffle_doors = bool(slot_data.get("shuffle_doors", false))
387 shuffle_gallery_paintings = bool(slot_data.get("shuffle_gallery_paintings", false)) 474 shuffle_gallery_paintings = bool(slot_data.get("shuffle_gallery_paintings", false))
@@ -403,7 +490,9 @@ func _client_connected(slot_data):
403 var raw_pp = slot_data.get("port_pairings") 490 var raw_pp = slot_data.get("port_pairings")
404 491
405 for p1 in raw_pp.keys(): 492 for p1 in raw_pp.keys():
406 port_pairings[int(p1)] = int(raw_pp[p1]) 493 port_pairings[gamedata.port_id_by_ap_id[int(p1)]] = gamedata.port_id_by_ap_id[int(
494 raw_pp[p1]
495 )]
407 496
408 # Set up item locks. 497 # Set up item locks.
409 _item_locks = {} 498 _item_locks = {}
@@ -479,6 +568,9 @@ func start_batching_locations():
479 568
480 569
481func send_location(loc_id): 570func send_location(loc_id):
571 if client._checked_locations.has(loc_id):
572 return
573
482 if _batch_locations: 574 if _batch_locations:
483 _held_locations.append(loc_id) 575 _held_locations.append(loc_id)
484 else: 576 else:
@@ -577,3 +669,49 @@ func _process_key_item(key, level):
577 level += 1 669 level += 1
578 670
579 keyboard.collect_remote_letter(key, level) 671 keyboard.collect_remote_letter(key, level)
672
673
674func update_job_well_done_sign():
675 if global.map != "daedalus":
676 return
677
678 var gamedata = global.get_node("Gamedata")
679 var job_item = gamedata.objects.get_special_ids()["A Job Well Done"]
680 var jobs_done = client.getItemAmount(job_item)
681
682 var sign2 = get_tree().get_root().get_node_or_null("scene/Meshes/Miscellaneous/sign2")
683 var sign3 = get_tree().get_root().get_node_or_null("scene/Meshes/Miscellaneous/sign3")
684
685 if sign2 != null and sign3 != null:
686 if jobs_done == 0:
687 sign2.text = "what are you doing"
688 sign3.text = "?"
689 elif jobs_done == 1:
690 sign2.text = "a job well done"
691 sign3.text = "is its own reward"
692 else:
693 sign2.text = "%d jobs well done" % jobs_done
694 sign3.text = "are their own reward"
695
696 sign2.get_node("MeshInstance3D").mesh.text = sign2.text
697 sign3.get_node("MeshInstance3D").mesh.text = sign3.text
698
699
700func toggle_ignored_location(loc_id):
701 if loc_id in _ignored_locations:
702 client.removeIgnoredLocation(loc_id)
703 else:
704 client.addIgnoredLocation(loc_id)
705
706
707func get_map_script(map_name):
708 if !_map_scripts.has(map_name):
709 var runtime = global.get_node("Runtime")
710 var script_path = "maps/%s.gd" % map_name
711 if runtime.path_exists(script_path):
712 var script = runtime.load_script(script_path)
713 _map_scripts[map_name] = script.new()
714 else:
715 _map_scripts[map_name] = null
716
717 return _map_scripts[map_name]
diff --git a/apworld/client/maps/control_center.gd b/apworld/client/maps/control_center.gd new file mode 100644 index 0000000..de9ae4b --- /dev/null +++ b/apworld/client/maps/control_center.gd
@@ -0,0 +1,85 @@
1func on_map_load(root):
2 var ap = global.get_node("Archipelago")
3
4 # Remove the door blocking the trophy case.
5 root.get_node("/root/scene/Components/Doors/entry_18").queue_free()
6
7 # Set up mastery listeners for extra maps.
8 _set_up_mastery_listener(root, "advanced")
9 _set_up_mastery_listener(root, "charismatic")
10 _set_up_mastery_listener(root, "crystalline")
11 _set_up_mastery_listener(root, "fuzzy")
12 _set_up_mastery_listener(root, "icarus")
13 _set_up_mastery_listener(root, "stellar")
14
15 if ap.endings_requirement != 12 or ap.masteries_requirement != 0:
16 # Set up listeners for the potential White Ending requirements.
17 var merging_prefab = preload("res://objects/nodes/listeners/mergingListener.tscn")
18
19 var old_door = root.get_node("/root/scene/Components/Doors/entry_19")
20 var new_door = old_door.duplicate()
21 new_door.name = "entry_19_new"
22 new_door.senders.clear()
23 new_door.senderGroup.clear()
24 new_door.excludeSenders.clear()
25
26 if ap.endings_requirement == 12:
27 new_door.senderGroup.append(NodePath("/root/scene/Meshes/Trophies/Listeners"))
28 elif ap.endings_requirement > 0:
29 if ap.masteries_requirement == 0:
30 new_door.senderGroup.append(NodePath("/root/scene/Meshes/Trophies/Listeners"))
31 new_door.complete_at = ap.endings_requirement
32 else:
33 var endings_merge = merging_prefab.instantiate()
34 endings_merge.name = "EndingsMerge"
35 endings_merge.senderGroup.append(NodePath("/root/scene/Meshes/Trophies/Listeners"))
36 endings_merge.complete_at = ap.endings_requirement
37 root.get_node("/root/scene/Components").add_child.call_deferred(endings_merge)
38 new_door.senders.append(NodePath("/root/scene/Components/EndingsMerge"))
39
40 var max_masteries = 13 + ap.enable_gift_maps.size()
41 if ap.enable_icarus:
42 max_masteries += 1
43
44 if ap.masteries_requirement == max_masteries:
45 new_door.senderGroup.append(NodePath("/root/scene/Meshes/Trophies/MasteryListeners"))
46 new_door.excludeSenders.append(
47 NodePath("/root/scene/Meshes/Trophies/MasteryListeners/unlockReaderListenerWhite")
48 )
49 elif ap.masteries_requirement > 0:
50 if ap.endings_requirement == 0:
51 new_door.senderGroup.append(
52 NodePath("/root/scene/Meshes/Trophies/MasteryListeners")
53 )
54 new_door.excludeSenders.append(
55 NodePath(
56 "/root/scene/Meshes/Trophies/MasteryListeners/unlockReaderListenerWhite"
57 )
58 )
59 new_door.complete_at = ap.masteries_requirement
60 else:
61 var masteries_merge = merging_prefab.instantiate()
62 masteries_merge.name = "MasteriesMerge"
63 masteries_merge.senderGroup.append(
64 NodePath("/root/scene/Meshes/Trophies/MasteryListeners")
65 )
66 masteries_merge.excludeSenders.append(
67 NodePath(
68 "/root/scene/Meshes/Trophies/MasteryListeners/unlockReaderListenerWhite"
69 )
70 )
71 masteries_merge.complete_at = ap.masteries_requirement
72 root.get_node("/root/scene/Components").add_child.call_deferred(masteries_merge)
73 new_door.senders.append(NodePath("/root/scene/Components/MasteriesMerge"))
74
75 old_door.queue_free()
76 root.get_node("/root/scene/Components/Doors").add_child.call_deferred(new_door)
77
78
79func _set_up_mastery_listener(root, name):
80 var prefab = preload("res://objects/nodes/listeners/unlockReaderListener.tscn")
81 var url = prefab.instantiate()
82 url.name = "unlockReaderListenerMastery_%s" % name
83 url.key = "%s_mastery" % name
84 url.value = "unlocked"
85 root.get_node("/root/scene/Meshes/Trophies/MasteryListeners").add_child.call_deferred(url)
diff --git a/apworld/client/maps/daedalus.gd b/apworld/client/maps/daedalus.gd new file mode 100644 index 0000000..5fcf7a5 --- /dev/null +++ b/apworld/client/maps/daedalus.gd
@@ -0,0 +1,85 @@
1func on_map_load(root):
2 var ap = global.get_node("Archipelago")
3
4 # Teleport the direction panels when the stairs are there.
5 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
6
7 var dir1 = root.get_node("/root/scene/Panels/Castle Entrance/castle_direction_1")
8 var dir1_tpl = tpl_prefab.instantiate()
9 dir1_tpl.target_path = dir1
10 dir1_tpl.teleport_point = Vector3(59.5, 8, -6.5)
11 dir1_tpl.teleport_rotate = Vector3(-45, 0, 0)
12 dir1_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_south"))
13 dir1_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_north"))
14 dir1_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_west"))
15 dir1.add_child.call_deferred(dir1_tpl)
16
17 var dir2 = root.get_node("/root/scene/Panels/Castle Entrance/castle_direction_2")
18 var dir2_tpl = tpl_prefab.instantiate()
19 dir2_tpl.target_path = dir2
20 dir2_tpl.teleport_point = Vector3(59.5, 8, 6.5)
21 dir2_tpl.teleport_rotate = Vector3(-45, -180, 0)
22 dir2_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_south"))
23 dir2_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_north"))
24 dir2_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_west"))
25 dir2.add_child.call_deferred(dir2_tpl)
26
27 var dir3 = root.get_node("/root/scene/Panels/Castle Entrance/castle_direction_3")
28 var dir3_tpl = tpl_prefab.instantiate()
29 dir3_tpl.target_path = dir3
30 dir3_tpl.teleport_point = Vector3(54, 8, 0)
31 dir3_tpl.teleport_rotate = Vector3(-45, 90, 0)
32 dir3_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_south"))
33 dir3_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_north"))
34 dir3_tpl.senders.append(NodePath("/root/scene/Panels/Castle Entrance/castle_west"))
35 dir3.add_child.call_deferred(dir3_tpl)
36
37 # Block off roof access in Daedalus.
38 if not ap.daedalus_roof_access:
39 _set_up_invis_wall(root, 75.5, 11, -24.5, 1, 10, 49)
40 _set_up_invis_wall(root, 51.5, 11, -17, 16, 10, 1)
41 _set_up_invis_wall(root, 46, 10, -9.5, 1, 10, 10)
42 _set_up_invis_wall(root, 67.5, 11, 17, 16, 10, 1)
43 _set_up_invis_wall(root, 50.5, 11, 14, 10, 10, 1)
44 _set_up_invis_wall(root, 39, 10, 18.5, 1, 10, 22)
45 _set_up_invis_wall(root, 20, 15, 18.5, 1, 10, 16)
46 _set_up_invis_wall(root, 11.5, 15, 3, 32, 10, 1)
47 _set_up_invis_wall(root, 11.5, 16, -20, 14, 20, 1)
48 _set_up_invis_wall(root, 14, 16, -26.5, 1, 20, 4)
49 _set_up_invis_wall(root, 28.5, 20.5, -26.5, 1, 15, 25)
50 _set_up_invis_wall(root, 40.5, 20.5, -11, 30, 15, 1)
51 _set_up_invis_wall(root, 50.5, 15, 5.5, 7, 10, 1)
52 _set_up_invis_wall(root, 83.5, 33.5, 5.5, 1, 7, 11)
53 _set_up_invis_wall(root, 83.5, 33.5, -5.5, 1, 7, 11)
54
55 var warp_exit_prefab = preload("res://objects/nodes/exit.tscn")
56 var warp_exit = warp_exit_prefab.instantiate()
57 warp_exit.name = "roof_access_blocker_warp_exit"
58 warp_exit.position = Vector3(58, 10, 0)
59 warp_exit.rotation_degrees.y = 90
60 root.get_node("/root/scene").add_child.call_deferred(warp_exit)
61
62 var warp_enter_prefab = preload("res://objects/nodes/teleportAuto.tscn")
63 var warp_enter = warp_enter_prefab.instantiate()
64 warp_enter.target = warp_exit
65 warp_enter.position = Vector3(76.5, 30, 1)
66 warp_enter.scale = Vector3(4, 1.5, 1)
67 warp_enter.rotation_degrees.y = 90
68 root.get_node("/root/scene").add_child.call_deferred(warp_enter)
69
70
71func _set_up_invis_wall(root, x, y, z, sx, sy, sz):
72 var prefab = preload("res://objects/nodes/block.tscn")
73 var newwall = prefab.instantiate()
74 newwall.position.x = x
75 newwall.position.y = y
76 newwall.position.z = z
77 newwall.scale.x = sz
78 newwall.scale.y = sy
79 newwall.scale.z = sx
80 newwall.set_surface_override_material(0, preload("res://assets/materials/blackMatte.material"))
81 newwall.visibility_range_end = 3
82 newwall.visibility_range_end_margin = 1
83 newwall.visibility_range_fade_mode = RenderingServer.VISIBILITY_RANGE_FADE_SELF
84 newwall.skeleton = ".."
85 root.get_node("/root/scene").add_child.call_deferred(newwall)
diff --git a/apworld/client/maps/icarus.gd b/apworld/client/maps/icarus.gd new file mode 100644 index 0000000..ad00741 --- /dev/null +++ b/apworld/client/maps/icarus.gd
@@ -0,0 +1,38 @@
1func on_map_load(root):
2 var ap = global.get_node("Archipelago")
3
4 # Add the mastery to Icarus.
5 if ap.enable_icarus:
6 var collectable_prefab = preload("res://objects/nodes/collectable.tscn")
7 var saver_prefab = preload("res://objects/nodes/saver.tscn")
8 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
9 var usl_prefab = preload("res://objects/nodes/listeners/unlockSetterListener.tscn")
10
11 var mastery = collectable_prefab.instantiate()
12 mastery.name = "collectable"
13 mastery.position = Vector3(0, -2000, 0)
14 mastery.unlock_type = "smiley"
15 mastery.material_override = load("res://assets/materials/gold.material")
16 root.get_node("/root/scene/Components/Collectables").add_child.call_deferred(mastery)
17
18 var tpl = tpl_prefab.instantiate()
19 tpl.teleport_point = Vector3(56.25, 0, -5.5)
20 tpl.teleport_rotate = Vector3(0, 0, 0)
21 tpl.target_path = mastery
22 tpl.name = "Teleport"
23 tpl.senderGroup.append(NodePath("/root/scene/Panels"))
24 tpl.nested = true
25 mastery.add_child.call_deferred(tpl)
26
27 var usl = usl_prefab.instantiate()
28 usl.name = "unlockSetterListenerMastery"
29 usl.key = "icarus_mastery"
30 usl.value = "unlocked"
31 usl.senders.append(NodePath("/root/scene/Components/Collectables/collectable"))
32 root.get_node("/root/scene/Components").add_child.call_deferred(usl)
33
34 var saver = saver_prefab.instantiate()
35 saver.name = "saver_collectables"
36 saver.type = "collectables"
37 saver.senderGroup.append(NodePath("/root/scene/Components/Collectables"))
38 root.get_node("/root/scene").add_child.call_deferred(saver)
diff --git a/apworld/client/maps/the_advanced.gd b/apworld/client/maps/the_advanced.gd new file mode 100644 index 0000000..b41549c --- /dev/null +++ b/apworld/client/maps/the_advanced.gd
@@ -0,0 +1,36 @@
1func on_map_load(root):
2 # Add the mastery to The Advanced.
3 var collectable_prefab = preload("res://objects/nodes/collectable.tscn")
4 var saver_prefab = preload("res://objects/nodes/saver.tscn")
5 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
6 var usl_prefab = preload("res://objects/nodes/listeners/unlockSetterListener.tscn")
7
8 var mastery = collectable_prefab.instantiate()
9 mastery.name = "collectable"
10 mastery.position = Vector3(0, -200, -5)
11 mastery.unlock_type = "smiley"
12 mastery.material_override = load("res://assets/materials/gold.material")
13 root.get_node("/root/scene/Components/Collectables").add_child.call_deferred(mastery)
14
15 var tpl = tpl_prefab.instantiate()
16 tpl.teleport_point = Vector3(0, 2, -5)
17 tpl.teleport_rotate = Vector3(0, 0, 0)
18 tpl.target_path = mastery
19 tpl.name = "Teleport"
20 tpl.senders.append(NodePath("/root/scene/Panels/Room_1/panel_29"))
21 tpl.senders.append(NodePath("/root/scene/Panels/Room_1/panel_30"))
22 tpl.senders.append(NodePath("/root/scene/Panels/Room_1/panel_31"))
23 mastery.add_child.call_deferred(tpl)
24
25 var usl = usl_prefab.instantiate()
26 usl.name = "unlockSetterListenerMastery"
27 usl.key = "advanced_mastery"
28 usl.value = "unlocked"
29 usl.senders.append(NodePath("/root/scene/Components/Collectables/collectable"))
30 root.get_node("/root/scene/Components").add_child.call_deferred(usl)
31
32 var saver = saver_prefab.instantiate()
33 saver.name = "saver_collectables"
34 saver.type = "collectables"
35 saver.senderGroup.append(NodePath("/root/scene/Components/Collectables"))
36 root.get_node("/root/scene").add_child.call_deferred(saver)
diff --git a/apworld/client/maps/the_charismatic.gd b/apworld/client/maps/the_charismatic.gd new file mode 100644 index 0000000..734001d --- /dev/null +++ b/apworld/client/maps/the_charismatic.gd
@@ -0,0 +1,26 @@
1func on_map_load(root):
2 # Add the mastery to The Charismatic.
3 var collectable_prefab = preload("res://objects/nodes/collectable.tscn")
4 var saver_prefab = preload("res://objects/nodes/saver.tscn")
5 var usl_prefab = preload("res://objects/nodes/listeners/unlockSetterListener.tscn")
6
7 var mastery = collectable_prefab.instantiate()
8 mastery.name = "collectable"
9 mastery.position = Vector3(-17, 2, -29)
10 mastery.rotation_degrees = Vector3(0, 45, 0)
11 mastery.unlock_type = "smiley"
12 mastery.material_override = load("res://assets/materials/gold.material")
13 root.get_node("/root/scene/Components/Collectables").add_child.call_deferred(mastery)
14
15 var usl = usl_prefab.instantiate()
16 usl.name = "unlockSetterListenerMastery"
17 usl.key = "charismatic_mastery"
18 usl.value = "unlocked"
19 usl.senders.append(NodePath("/root/scene/Components/Collectables/collectable"))
20 root.get_node("/root/scene/Components").add_child.call_deferred(usl)
21
22 var saver = saver_prefab.instantiate()
23 saver.name = "saver_collectables"
24 saver.type = "collectables"
25 saver.senderGroup.append(NodePath("/root/scene/Components/Collectables"))
26 root.get_node("/root/scene").add_child.call_deferred(saver)
diff --git a/apworld/client/maps/the_crystalline.gd b/apworld/client/maps/the_crystalline.gd new file mode 100644 index 0000000..7d43e78 --- /dev/null +++ b/apworld/client/maps/the_crystalline.gd
@@ -0,0 +1,34 @@
1func on_map_load(root):
2 # Add the mastery to The Crystalline.
3 var collectable_prefab = preload("res://objects/nodes/collectable.tscn")
4 var saver_prefab = preload("res://objects/nodes/saver.tscn")
5 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
6 var usl_prefab = preload("res://objects/nodes/listeners/unlockSetterListener.tscn")
7
8 var mastery = collectable_prefab.instantiate()
9 mastery.name = "collectable"
10 mastery.position = Vector3(0, 13, 37)
11 mastery.unlock_type = "smiley"
12 mastery.material_override = load("res://assets/materials/gold.material")
13 root.get_node("/root/scene/Components/Collectables").add_child.call_deferred(mastery)
14
15 var tpl = tpl_prefab.instantiate()
16 tpl.teleport_point = Vector3(0, 11.5, -20)
17 tpl.teleport_rotate = Vector3(0, 0, 180)
18 tpl.target_path = mastery
19 tpl.name = "Teleport"
20 tpl.senders.append(NodePath("/root/scene/Panels/Room_1/panel_3"))
21 mastery.add_child.call_deferred(tpl)
22
23 var usl = usl_prefab.instantiate()
24 usl.name = "unlockSetterListenerMastery"
25 usl.key = "crystalline_mastery"
26 usl.value = "unlocked"
27 usl.senders.append(NodePath("/root/scene/Components/Collectables/collectable"))
28 root.get_node("/root/scene/Components").add_child.call_deferred(usl)
29
30 var saver = saver_prefab.instantiate()
31 saver.name = "saver_collectables"
32 saver.type = "collectables"
33 saver.senderGroup.append(NodePath("/root/scene/Components/Collectables"))
34 root.get_node("/root/scene").add_child.call_deferred(saver)
diff --git a/apworld/client/maps/the_entry.gd b/apworld/client/maps/the_entry.gd new file mode 100644 index 0000000..3608bb3 --- /dev/null +++ b/apworld/client/maps/the_entry.gd
@@ -0,0 +1,156 @@
1func on_map_load(root):
2 var ap = global.get_node("Archipelago")
3
4 # Remove door behind X1.
5 var door_node = root.get_node("/root/scene/Components/Doors/exit_1")
6 door_node.handleTriggered()
7
8 # Display win condition.
9 var sign_prefab = preload("res://objects/nodes/sign.tscn")
10 var sign1 = sign_prefab.instantiate()
11 sign1.position = Vector3(-7, 5, -15.01)
12 sign1.text = "victory"
13 root.get_node("/root/scene").add_child.call_deferred(sign1)
14
15 var sign2 = sign_prefab.instantiate()
16 sign2.position = Vector3(-7, 4, -15.01)
17 sign2.text = "%s ending" % ap.kEndingNameByVictoryValue.get(ap.victory_condition, "?")
18
19 var sign2_color = ap.kEndingNameByVictoryValue.get(ap.victory_condition, "coral").to_lower()
20 if sign2_color == "white":
21 sign2_color = "silver"
22
23 sign2.material = load("res://assets/materials/%s.material" % sign2_color)
24 root.get_node("/root/scene").add_child.call_deferred(sign2)
25
26 # Add the gift map entry panel if needed.
27 if not ap.enable_gift_maps.is_empty():
28 var panel_prefab = preload("res://objects/nodes/panel.tscn")
29 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
30 var wpl_prefab = preload("res://objects/nodes/listeners/worldportListener.tscn")
31
32 var giftmap_parent = Node.new()
33 giftmap_parent.name = "GiftMapEntrance"
34 root.get_node("/root/scene/Components").add_child.call_deferred(giftmap_parent)
35
36 var symbolless_player = ""
37 for i in range(ap.client.ap_user.length()):
38 if "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".contains(
39 ap.client.ap_user[i]
40 ):
41 symbolless_player = symbolless_player + ap.client.ap_user[i].to_lower()
42
43 var giftmap_panel = panel_prefab.instantiate()
44 giftmap_panel.name = "Panel"
45 giftmap_panel.position = Vector3(33.5, -190, 5.5)
46 giftmap_panel.rotation_degrees = Vector3(-45, 0, 0)
47 giftmap_panel.clue = "player"
48 giftmap_panel.answer = symbolless_player
49
50 if ap.enable_gift_maps.has("The Advanced"):
51 var icely_panel = panel_prefab.instantiate()
52 icely_panel.name = "IcelyPanel"
53 icely_panel.answer = "icely"
54 icely_panel.position = Vector3(33.5, -200, 5.5)
55 giftmap_panel.proxies.append(NodePath("../IcelyPanel"))
56 giftmap_parent.add_child.call_deferred(icely_panel)
57
58 var icely_wpl = wpl_prefab.instantiate()
59 icely_wpl.name = "IcelyWpl"
60 icely_wpl.exit = "the_advanced"
61 icely_wpl.senders.append(NodePath("../IcelyPanel"))
62 giftmap_parent.add_child.call_deferred(icely_wpl)
63
64 if ap.enable_gift_maps.has("The Charismatic"):
65 var souvey_panel = panel_prefab.instantiate()
66 souvey_panel.name = "SouveyPanel"
67 souvey_panel.answer = "souvey"
68 souvey_panel.position = Vector3(33.5, -210, 5.5)
69 giftmap_panel.proxies.append(NodePath("../SouveyPanel"))
70 giftmap_parent.add_child.call_deferred(souvey_panel)
71
72 var souvey_wpl = wpl_prefab.instantiate()
73 souvey_wpl.name = "SouveyWpl"
74 souvey_wpl.exit = "the_charismatic"
75 souvey_wpl.senders.append(NodePath("../SouveyPanel"))
76 giftmap_parent.add_child.call_deferred(souvey_wpl)
77
78 if ap.enable_gift_maps.has("The Crystalline"):
79 var q_panel = panel_prefab.instantiate()
80 q_panel.name = "QPanel"
81 q_panel.answer = "q"
82 q_panel.position = Vector3(33.5, -220, 5.5)
83 giftmap_panel.proxies.append(NodePath("../QPanel"))
84 giftmap_parent.add_child.call_deferred(q_panel)
85
86 var q_wpl = wpl_prefab.instantiate()
87 q_wpl.name = "QWpl"
88 q_wpl.exit = "the_crystalline"
89 q_wpl.senders.append(NodePath("../QPanel"))
90 giftmap_parent.add_child.call_deferred(q_wpl)
91
92 if ap.enable_gift_maps.has("The Fuzzy"):
93 var gongus_panel = panel_prefab.instantiate()
94 gongus_panel.name = "GongusPanel"
95 gongus_panel.answer = "gongus"
96 gongus_panel.position = Vector3(33.5, -260, 5.5)
97 giftmap_panel.proxies.append(NodePath("../GongusPanel"))
98 giftmap_parent.add_child.call_deferred(gongus_panel)
99
100 var kiwi_panel = panel_prefab.instantiate()
101 kiwi_panel.name = "KiwiPanel"
102 kiwi_panel.answer = "kiwi"
103 kiwi_panel.position = Vector3(33.5, -270, 5.5)
104 giftmap_panel.proxies.append(NodePath("../KiwiPanel"))
105 giftmap_parent.add_child.call_deferred(kiwi_panel)
106
107 var fuzzy_wpl = wpl_prefab.instantiate()
108 fuzzy_wpl.name = "FuzzyWpl"
109 fuzzy_wpl.exit = "the_fuzzy"
110 fuzzy_wpl.senders.append(NodePath("../GongusPanel"))
111 fuzzy_wpl.senders.append(NodePath("../KiwiPanel"))
112 fuzzy_wpl.complete_at = 1
113 giftmap_parent.add_child.call_deferred(fuzzy_wpl)
114
115 if ap.enable_gift_maps.has("The Stellar"):
116 var hatkirby_panel = panel_prefab.instantiate()
117 hatkirby_panel.name = "HatkirbyPanel"
118 hatkirby_panel.answer = "hatkirby"
119 hatkirby_panel.position = Vector3(33.5, -230, 5.5)
120 giftmap_panel.proxies.append(NodePath("../HatkirbyPanel"))
121 giftmap_parent.add_child.call_deferred(hatkirby_panel)
122
123 var kirby_panel = panel_prefab.instantiate()
124 kirby_panel.name = "KirbyPanel"
125 kirby_panel.answer = "kirby"
126 kirby_panel.position = Vector3(33.5, -240, 5.5)
127 giftmap_panel.proxies.append(NodePath("../KirbyPanel"))
128 giftmap_parent.add_child.call_deferred(kirby_panel)
129
130 var star_panel = panel_prefab.instantiate()
131 star_panel.name = "StarPanel"
132 star_panel.answer = "star"
133 star_panel.position = Vector3(33.5, -250, 5.5)
134 giftmap_panel.proxies.append(NodePath("../StarPanel"))
135 giftmap_parent.add_child.call_deferred(star_panel)
136
137 var stellar_wpl = wpl_prefab.instantiate()
138 stellar_wpl.name = "StellarWpl"
139 stellar_wpl.exit = "the_stellar"
140 stellar_wpl.senders.append(NodePath("../HatkirbyPanel"))
141 stellar_wpl.senders.append(NodePath("../KirbyPanel"))
142 stellar_wpl.senders.append(NodePath("../StarPanel"))
143 stellar_wpl.complete_at = 1
144 giftmap_parent.add_child.call_deferred(stellar_wpl)
145
146 giftmap_parent.add_child.call_deferred(giftmap_panel)
147
148 var giftmap_tpl = tpl_prefab.instantiate()
149 giftmap_tpl.name = "PanelTeleporter"
150 giftmap_tpl.teleport_point = Vector3(33.5, 1, 5.5)
151 giftmap_tpl.teleport_rotate = Vector3(-45, 0, 0)
152 giftmap_tpl.target_path = giftmap_panel
153 giftmap_tpl.senders.append(
154 NodePath("/root/scene/Components/Listeners/unlockReaderListenerDoubles")
155 )
156 giftmap_parent.add_child.call_deferred(giftmap_tpl)
diff --git a/apworld/client/maps/the_fuzzy.gd b/apworld/client/maps/the_fuzzy.gd new file mode 100644 index 0000000..269dcee --- /dev/null +++ b/apworld/client/maps/the_fuzzy.gd
@@ -0,0 +1,25 @@
1func on_map_load(root):
2 # Add the mastery to The Fuzzy.
3 var collectable_prefab = preload("res://objects/nodes/collectable.tscn")
4 var saver_prefab = preload("res://objects/nodes/saver.tscn")
5 var usl_prefab = preload("res://objects/nodes/listeners/unlockSetterListener.tscn")
6
7 var mastery = collectable_prefab.instantiate()
8 mastery.name = "collectable"
9 mastery.position = Vector3(0, 2, -20)
10 mastery.unlock_type = "smiley"
11 mastery.material_override = load("res://assets/materials/gold.material")
12 root.get_node("/root/scene/Components/Collectables").add_child.call_deferred(mastery)
13
14 var usl = usl_prefab.instantiate()
15 usl.name = "unlockSetterListenerMastery"
16 usl.key = "fuzzy_mastery"
17 usl.value = "unlocked"
18 usl.senders.append(NodePath("/root/scene/Components/Collectables/collectable"))
19 root.get_node("/root/scene/Components").add_child.call_deferred(usl)
20
21 var saver = saver_prefab.instantiate()
22 saver.name = "saver_collectables"
23 saver.type = "collectables"
24 saver.senderGroup.append(NodePath("/root/scene/Components/Collectables"))
25 root.get_node("/root/scene").add_child.call_deferred(saver)
diff --git a/apworld/client/maps/the_parthenon.gd b/apworld/client/maps/the_parthenon.gd new file mode 100644 index 0000000..96510da --- /dev/null +++ b/apworld/client/maps/the_parthenon.gd
@@ -0,0 +1,51 @@
1func on_map_load(root):
2 var ap = global.get_node("Archipelago")
3
4 # Add the strict cyan ending validation.
5 if ap.strict_cyan_ending:
6 var panel_prefab = preload("res://objects/nodes/panel.tscn")
7 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
8 var reverse_prefab = preload("res://objects/nodes/listeners/reversingListener.tscn")
9
10 var previous_panel = null
11 var next_y = -100
12 var words = ["quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"]
13 for word in words:
14 var panel = panel_prefab.instantiate()
15 panel.position = Vector3(0, next_y, 0)
16 next_y -= 10
17 panel.clue = word
18 panel.symbol = "."
19 panel.answer = "%s%s" % [word, word]
20 panel.name = "EndCheck_%s" % word
21
22 var tpl = tpl_prefab.instantiate()
23 tpl.teleport_point = Vector3(0, 1, -11)
24 tpl.teleport_rotate = Vector3(-45, 0, 0)
25 tpl.target_path = panel
26 tpl.name = "Teleport"
27
28 if previous_panel == null:
29 tpl.senderGroup.append(NodePath("/root/scene/Panels/Rulers"))
30 else:
31 tpl.senders.append(NodePath("../../%s" % previous_panel.name))
32
33 var reversing = reverse_prefab.instantiate()
34 reversing.senders.append(NodePath(".."))
35 reversing.name = "Reversing"
36 tpl.senders.append(NodePath("../Reversing"))
37
38 panel.add_child.call_deferred(tpl)
39 panel.add_child.call_deferred(reversing)
40 root.get_node("/root/scene/Panels").add_child.call_deferred(panel)
41
42 previous_panel = panel
43
44 # Duplicate the door that usually waits on the rulers. We can't set the
45 # senders here for some reason so we actually set them in the door ready
46 # function.
47 var entry1 = root.get_node("/root/scene/Components/Doors/entry_1")
48 var entry12 = entry1.duplicate()
49 entry12.name = "spe_entry_1"
50 entry1.get_parent().add_child.call_deferred(entry12)
51 entry1.queue_free()
diff --git a/apworld/client/maps/the_plaza.gd b/apworld/client/maps/the_plaza.gd new file mode 100644 index 0000000..13e002d --- /dev/null +++ b/apworld/client/maps/the_plaza.gd
@@ -0,0 +1,4 @@
1func on_map_load(root):
2 # Move the Plaza RTE trigger outside of the turtle.
3 var rte_trigger = root.get_node("/root/scene/Components/Warps/triggerArea")
4 rte_trigger.position.z = 0
diff --git a/apworld/client/maps/the_stellar.gd b/apworld/client/maps/the_stellar.gd new file mode 100644 index 0000000..d633535 --- /dev/null +++ b/apworld/client/maps/the_stellar.gd
@@ -0,0 +1,30 @@
1func on_map_load(root):
2 # Add the mastery to The Stellar.
3 var collectable_prefab = preload("res://objects/nodes/collectable.tscn")
4 var saver_prefab = preload("res://objects/nodes/saver.tscn")
5 var usl_prefab = preload("res://objects/nodes/listeners/unlockSetterListener.tscn")
6
7 var collectables = Node.new()
8 collectables.name = "Collectables"
9
10 var mastery = collectable_prefab.instantiate()
11 mastery.name = "collectable"
12 mastery.position = Vector3(2, 2, -31)
13 mastery.rotation_degrees = Vector3(0, 90, 0)
14 mastery.unlock_type = "smiley"
15 mastery.material_override = load("res://assets/materials/gold.material")
16 collectables.add_child.call_deferred(mastery)
17 root.get_node("/root/scene/Components").add_child.call_deferred(collectables)
18
19 var usl = usl_prefab.instantiate()
20 usl.name = "unlockSetterListenerMastery"
21 usl.key = "stellar_mastery"
22 usl.value = "unlocked"
23 usl.senders.append(NodePath("/root/scene/Components/Collectables/collectable"))
24 root.get_node("/root/scene/Components").add_child.call_deferred(usl)
25
26 var saver = saver_prefab.instantiate()
27 saver.name = "saver_collectables"
28 saver.type = "collectables"
29 saver.senderGroup.append(NodePath("/root/scene/Components/Collectables"))
30 root.get_node("/root/scene").add_child.call_deferred(saver)
diff --git a/apworld/client/maps/the_sun_temple.gd b/apworld/client/maps/the_sun_temple.gd new file mode 100644 index 0000000..9804bf8 --- /dev/null +++ b/apworld/client/maps/the_sun_temple.gd
@@ -0,0 +1,56 @@
1func on_map_load(root):
2 var ap = global.get_node("Archipelago")
3
4 # Add the strict purple ending validation.
5 if ap.strict_purple_ending:
6 var panel_prefab = preload("res://objects/nodes/panel.tscn")
7 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
8 var reverse_prefab = preload("res://objects/nodes/listeners/reversingListener.tscn")
9
10 var previous_panel = null
11 var next_y = -100
12 var words = ["quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"]
13 for word in words:
14 var panel = panel_prefab.instantiate()
15 panel.position = Vector3(0, next_y, 0)
16 next_y -= 10
17 panel.clue = word
18 panel.symbol = ""
19 panel.answer = word
20 panel.name = "EndCheck_%s" % word
21
22 var tpl = tpl_prefab.instantiate()
23 tpl.teleport_point = Vector3(0, 1, 0)
24 tpl.teleport_rotate = Vector3(-45, 180, 0)
25 tpl.target_path = panel
26 tpl.name = "Teleport"
27
28 if previous_panel == null:
29 tpl.senders.append(NodePath("/root/scene/Panels/End/panel_24"))
30 else:
31 tpl.senders.append(NodePath("../../%s" % previous_panel.name))
32
33 var reversing = reverse_prefab.instantiate()
34 reversing.senders.append(NodePath(".."))
35 reversing.name = "Reversing"
36 tpl.senders.append(NodePath("../Reversing"))
37
38 panel.add_child.call_deferred(tpl)
39 panel.add_child.call_deferred(reversing)
40 root.get_node("/root/scene/Panels").add_child.call_deferred(panel)
41
42 previous_panel = panel
43
44 # Duplicate the doors that usually wait on EQUINOX. We can't set the senders
45 # here for some reason so we actually set them in the door ready function.
46 var endplat = root.get_node("/root/scene/Components/Doors/EndPlatform")
47 var endplat2 = endplat.duplicate()
48 endplat2.name = "spe_EndPlatform"
49 endplat.get_parent().add_child.call_deferred(endplat2)
50 endplat.queue_free()
51
52 var entry2 = root.get_node("/root/scene/Components/Doors/entry_2")
53 var entry22 = entry2.duplicate()
54 entry22.name = "spe_entry_2"
55 entry2.get_parent().add_child.call_deferred(entry22)
56 entry2.queue_free()
diff --git a/apworld/client/maps/the_unkempt.gd b/apworld/client/maps/the_unkempt.gd new file mode 100644 index 0000000..c907650 --- /dev/null +++ b/apworld/client/maps/the_unkempt.gd
@@ -0,0 +1,4 @@
1func on_map_load(root):
2 # Prevent the COLOR panel from disappearing.
3 var color_tpl = root.get_node("/root/scene/Panels/Assorted/panel_1/teleportListener")
4 color_tpl.target_path = color_tpl
diff --git a/apworld/client/maps/the_unyielding.gd b/apworld/client/maps/the_unyielding.gd new file mode 100644 index 0000000..a2f8eee --- /dev/null +++ b/apworld/client/maps/the_unyielding.gd
@@ -0,0 +1,5 @@
1func on_map_load(root):
2 # Shrink the painting trigger in The Unyielding.
3 var trigger_area = root.get_node("/root/scene/Components/PaintingUnlocker/triggerArea")
4 trigger_area.position = Vector3(0, 0, -6)
5 trigger_area.scale = Vector3(6, 1, 6)
diff --git a/apworld/client/minimap.gd b/apworld/client/minimap.gd index 5640716..bf70114 100644 --- a/apworld/client/minimap.gd +++ b/apworld/client/minimap.gd
@@ -126,6 +126,7 @@ func _process(_delta):
126 126
127 127
128func _renderMap(gridmap): 128func _renderMap(gridmap):
129 var ap = global.get_node("Archipelago")
129 var heights = {} 130 var heights = {}
130 131
131 var rendered = Image.create_empty(cell_width, cell_height, false, Image.FORMAT_RGBA8) 132 var rendered = Image.create_empty(cell_width, cell_height, false, Image.FORMAT_RGBA8)
@@ -133,7 +134,7 @@ func _renderMap(gridmap):
133 134
134 var meshes_node = get_tree().get_root().get_node("scene/Meshes") 135 var meshes_node = get_tree().get_root().get_node("scene/Meshes")
135 if meshes_node != null: 136 if meshes_node != null:
136 _renderMeshNode(gridmap, meshes_node, rendered) 137 _renderMeshNode(ap, gridmap, meshes_node, rendered)
137 138
138 for pos in gridmap.get_used_cells(): 139 for pos in gridmap.get_used_cells():
139 var in_plane = Vector2i(pos.x, pos.z) 140 var in_plane = Vector2i(pos.x, pos.z)
@@ -146,20 +147,22 @@ func _renderMap(gridmap):
146 var cell_item = gridmap.get_cell_item(pos) 147 var cell_item = gridmap.get_cell_item(pos)
147 var mesh = gridmap.mesh_library.get_item_mesh(cell_item) 148 var mesh = gridmap.mesh_library.get_item_mesh(cell_item)
148 var material = mesh.surface_get_material(0) 149 var material = mesh.surface_get_material(0)
149 var color = material.albedo_color 150 var color = ap.color_by_material_path.get(material.resource_path, Color.TRANSPARENT)
150 151
151 rendered.set_pixel(pos.x - cell_left, pos.z - cell_top, color) 152 rendered.set_pixel(pos.x - cell_left, pos.z - cell_top, color)
152 153
153 return rendered 154 return rendered
154 155
155 156
156func _renderMeshNode(gridmap, mesh, rendered): 157func _renderMeshNode(ap, gridmap, mesh, rendered):
157 if mesh is MeshInstance3D: 158 if mesh is MeshInstance3D:
158 var local_tl = gridmap.map_to_local(Vector3i(cell_left, 0, cell_top)) 159 var local_tl = gridmap.map_to_local(Vector3i(cell_left, 0, cell_top))
159 var global_tl = gridmap.to_global(local_tl) 160 var global_tl = gridmap.to_global(local_tl)
160 var mesh_material = mesh.get_surface_override_material(0) 161 var mesh_material = mesh.get_surface_override_material(0)
161 if mesh_material != null: 162 if mesh_material != null:
162 var mesh_color = mesh_material.albedo_color 163 var mesh_color = ap.color_by_material_path.get(
164 mesh_material.resource_path, Color.TRANSPARENT
165 )
163 166
164 for y in range( 167 for y in range(
165 max(mesh.position.z - mesh.scale.z / 2 - global_tl.z, 0), 168 max(mesh.position.z - mesh.scale.z / 2 - global_tl.z, 0),
@@ -172,4 +175,4 @@ func _renderMeshNode(gridmap, mesh, rendered):
172 rendered.set_pixel(x, y, mesh_color) 175 rendered.set_pixel(x, y, mesh_color)
173 176
174 for child in mesh.get_children(): 177 for child in mesh.get_children():
175 _renderMeshNode(gridmap, child, rendered) 178 _renderMeshNode(ap, gridmap, child, rendered)
diff --git a/apworld/client/paintingAuto.gd b/apworld/client/paintingAuto.gd new file mode 100644 index 0000000..553c2c9 --- /dev/null +++ b/apworld/client/paintingAuto.gd
@@ -0,0 +1,43 @@
1extends "res://scripts/nodes/paintingAuto.gd"
2
3var item_id
4var item_amount
5
6
7func _ready():
8 var node_path = String(
9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
10 )
11
12 var gamedata = global.get_node("Gamedata")
13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
14 if door_id != null:
15 var ap = global.get_node("Archipelago")
16 var item_lock = ap.get_item_id_for_door(door_id)
17
18 if item_lock != null:
19 item_id = item_lock[0]
20 item_amount = item_lock[1]
21
22 self.senders = []
23 self.senderGroup = []
24 self.nested = false
25 self.complete_at = 0
26 self.max_length = 0
27 self.excludeSenders = []
28
29 call_deferred("_readier")
30
31 super._ready()
32
33 if item_id != null and activate_on_sender_complete:
34 enabled = false
35 if not hide_particles:
36 get_node("Hinge/paintingColliders/TeleportParticles").emitting = false
37
38
39func _readier():
40 var ap = global.get_node("Archipelago")
41
42 if ap.client.getItemAmount(item_id) >= item_amount:
43 handleTriggered()
diff --git a/apworld/client/player.gd b/apworld/client/player.gd index 5417a48..5fac9fd 100644 --- a/apworld/client/player.gd +++ b/apworld/client/player.gd
@@ -19,6 +19,13 @@ func _ready():
19 19
20 ap.start_batching_locations() 20 ap.start_batching_locations()
21 21
22 # Run map-specific initialization.
23 var map_script = ap.get_map_script(global.map)
24 if map_script != null:
25 map_script.on_map_load(get_tree().get_root())
26
27 ap.update_job_well_done_sign()
28
22 # Set up door locations. 29 # Set up door locations.
23 var map_id = gamedata.map_id_by_name.get(global.map) 30 var map_id = gamedata.map_id_by_name.get(global.map)
24 for door in gamedata.objects.get_doors(): 31 for door in gamedata.objects.get_doors():
@@ -29,8 +36,12 @@ func _ready():
29 continue 36 continue
30 37
31 if ( 38 if (
32 door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY 39 not (door.has_legacy_location() and door.get_legacy_location())
33 or door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING 40 and (
41 door.get_type() == gamedata.SCRIPT_proto.DoorType.ITEM_ONLY
42 or door.get_type() == gamedata.SCRIPT_proto.DoorType.GALLERY_PAINTING
43 or door.get_type() == gamedata.SCRIPT_proto.DoorType.CONTROL_CENTER_COLOR
44 )
34 ): 45 ):
35 continue 46 continue
36 47
@@ -153,165 +164,6 @@ func _ready():
153 164
154 get_parent().add_child.call_deferred(locationListener) 165 get_parent().add_child.call_deferred(locationListener)
155 166
156 # Block off roof access in Daedalus.
157 if global.map == "daedalus" and not ap.daedalus_roof_access:
158 _set_up_invis_wall(75.5, 11, -24.5, 1, 10, 49)
159 _set_up_invis_wall(51.5, 11, -17, 16, 10, 1)
160 _set_up_invis_wall(46, 10, -9.5, 1, 10, 10)
161 _set_up_invis_wall(67.5, 11, 17, 16, 10, 1)
162 _set_up_invis_wall(50.5, 11, 14, 10, 10, 1)
163 _set_up_invis_wall(39, 10, 18.5, 1, 10, 22)
164 _set_up_invis_wall(20, 15, 18.5, 1, 10, 16)
165 _set_up_invis_wall(11.5, 15, 3, 32, 10, 1)
166 _set_up_invis_wall(11.5, 16, -20, 14, 20, 1)
167 _set_up_invis_wall(14, 16, -26.5, 1, 20, 4)
168 _set_up_invis_wall(28.5, 20.5, -26.5, 1, 15, 25)
169 _set_up_invis_wall(40.5, 20.5, -11, 30, 15, 1)
170 _set_up_invis_wall(50.5, 15, 5.5, 7, 10, 1)
171 _set_up_invis_wall(83.5, 33.5, 5.5, 1, 7, 11)
172 _set_up_invis_wall(83.5, 33.5, -5.5, 1, 7, 11)
173
174 var warp_exit_prefab = preload("res://objects/nodes/exit.tscn")
175 var warp_exit = warp_exit_prefab.instantiate()
176 warp_exit.name = "roof_access_blocker_warp_exit"
177 warp_exit.position = Vector3(58, 10, 0)
178 warp_exit.rotation_degrees.y = 90
179 get_parent().add_child.call_deferred(warp_exit)
180
181 var warp_enter_prefab = preload("res://objects/nodes/teleportAuto.tscn")
182 var warp_enter = warp_enter_prefab.instantiate()
183 warp_enter.target = warp_exit
184 warp_enter.position = Vector3(76.5, 30, 1)
185 warp_enter.scale = Vector3(4, 1.5, 1)
186 warp_enter.rotation_degrees.y = 90
187 get_parent().add_child.call_deferred(warp_enter)
188
189 if global.map == "the_entry":
190 # Remove door behind X1.
191 var door_node = get_tree().get_root().get_node("/root/scene/Components/Doors/exit_1")
192 door_node.handleTriggered()
193
194 # Display win condition.
195 var sign_prefab = preload("res://objects/nodes/sign.tscn")
196 var sign1 = sign_prefab.instantiate()
197 sign1.position = Vector3(-7, 5, -15.01)
198 sign1.text = "victory"
199 get_parent().add_child.call_deferred(sign1)
200
201 var sign2 = sign_prefab.instantiate()
202 sign2.position = Vector3(-7, 4, -15.01)
203 sign2.text = "%s ending" % ap.kEndingNameByVictoryValue.get(ap.victory_condition, "?")
204
205 var sign2_color = ap.kEndingNameByVictoryValue.get(ap.victory_condition, "coral").to_lower()
206 if sign2_color == "white":
207 sign2_color = "silver"
208
209 sign2.material = load("res://assets/materials/%s.material" % sign2_color)
210 get_parent().add_child.call_deferred(sign2)
211
212 # Add the strict purple ending validation.
213 if global.map == "the_sun_temple" and ap.strict_purple_ending:
214 var panel_prefab = preload("res://objects/nodes/panel.tscn")
215 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
216 var reverse_prefab = preload("res://objects/nodes/listeners/reversingListener.tscn")
217
218 var previous_panel = null
219 var next_y = -100
220 var words = ["quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"]
221 for word in words:
222 var panel = panel_prefab.instantiate()
223 panel.position = Vector3(0, next_y, 0)
224 next_y -= 10
225 panel.clue = word
226 panel.symbol = ""
227 panel.answer = word
228 panel.name = "EndCheck_%s" % word
229
230 var tpl = tpl_prefab.instantiate()
231 tpl.teleport_point = Vector3(0, 1, 0)
232 tpl.teleport_rotate = Vector3(-45, 180, 0)
233 tpl.target_path = panel
234 tpl.name = "Teleport"
235
236 if previous_panel == null:
237 tpl.senders.append(NodePath("/root/scene/Panels/End/panel_24"))
238 else:
239 tpl.senders.append(NodePath("../../%s" % previous_panel.name))
240
241 var reversing = reverse_prefab.instantiate()
242 reversing.senders.append(NodePath(".."))
243 reversing.name = "Reversing"
244 tpl.senders.append(NodePath("../Reversing"))
245
246 panel.add_child.call_deferred(tpl)
247 panel.add_child.call_deferred(reversing)
248 get_parent().get_node("Panels").add_child.call_deferred(panel)
249
250 previous_panel = panel
251
252 # Duplicate the doors that usually wait on EQUINOX. We can't set the senders
253 # here for some reason so we actually set them in the door ready function.
254 var endplat = get_node("/root/scene/Components/Doors/EndPlatform")
255 var endplat2 = endplat.duplicate()
256 endplat2.name = "spe_EndPlatform"
257 endplat.get_parent().add_child.call_deferred(endplat2)
258 endplat.queue_free()
259
260 var entry2 = get_node("/root/scene/Components/Doors/entry_2")
261 var entry22 = entry2.duplicate()
262 entry22.name = "spe_entry_2"
263 entry2.get_parent().add_child.call_deferred(entry22)
264 entry2.queue_free()
265
266 # Add the strict cyan ending validation.
267 if global.map == "the_parthenon" and ap.strict_cyan_ending:
268 var panel_prefab = preload("res://objects/nodes/panel.tscn")
269 var tpl_prefab = preload("res://objects/nodes/listeners/teleportListener.tscn")
270 var reverse_prefab = preload("res://objects/nodes/listeners/reversingListener.tscn")
271
272 var previous_panel = null
273 var next_y = -100
274 var words = ["quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"]
275 for word in words:
276 var panel = panel_prefab.instantiate()
277 panel.position = Vector3(0, next_y, 0)
278 next_y -= 10
279 panel.clue = word
280 panel.symbol = "."
281 panel.answer = "%s%s" % [word, word]
282 panel.name = "EndCheck_%s" % word
283
284 var tpl = tpl_prefab.instantiate()
285 tpl.teleport_point = Vector3(0, 1, -11)
286 tpl.teleport_rotate = Vector3(-45, 0, 0)
287 tpl.target_path = panel
288 tpl.name = "Teleport"
289
290 if previous_panel == null:
291 tpl.senderGroup.append(NodePath("/root/scene/Panels/Rulers"))
292 else:
293 tpl.senders.append(NodePath("../../%s" % previous_panel.name))
294
295 var reversing = reverse_prefab.instantiate()
296 reversing.senders.append(NodePath(".."))
297 reversing.name = "Reversing"
298 tpl.senders.append(NodePath("../Reversing"))
299
300 panel.add_child.call_deferred(tpl)
301 panel.add_child.call_deferred(reversing)
302 get_parent().get_node("Panels").add_child.call_deferred(panel)
303
304 previous_panel = panel
305
306 # Duplicate the door that usually waits on the rulers. We can't set the
307 # senders here for some reason so we actually set them in the door ready
308 # function.
309 var entry1 = get_node("/root/scene/Components/Doors/entry_1")
310 var entry12 = entry1.duplicate()
311 entry12.name = "spe_entry_1"
312 entry1.get_parent().add_child.call_deferred(entry12)
313 entry1.queue_free()
314
315 var minimap = ap.SCRIPT_minimap.new() 167 var minimap = ap.SCRIPT_minimap.new()
316 minimap.name = "Minimap" 168 minimap.name = "Minimap"
317 minimap.visible = ap.show_minimap 169 minimap.visible = ap.show_minimap
@@ -325,22 +177,5 @@ func _ready():
325 ap.stop_batching_locations() 177 ap.stop_batching_locations()
326 178
327 179
328func _set_up_invis_wall(x, y, z, sx, sy, sz):
329 var prefab = preload("res://objects/nodes/block.tscn")
330 var newwall = prefab.instantiate()
331 newwall.position.x = x
332 newwall.position.y = y
333 newwall.position.z = z
334 newwall.scale.x = sz
335 newwall.scale.y = sy
336 newwall.scale.z = sx
337 newwall.set_surface_override_material(0, preload("res://assets/materials/blackMatte.material"))
338 newwall.visibility_range_end = 3
339 newwall.visibility_range_end_margin = 1
340 newwall.visibility_range_fade_mode = RenderingServer.VISIBILITY_RANGE_FADE_SELF
341 newwall.skeleton = ".."
342 get_parent().add_child.call_deferred(newwall)
343
344
345func _process(_dt): 180func _process(_dt):
346 compass.update_rotation(global_rotation.y) 181 compass.update_rotation(global_rotation.y)
diff --git a/apworld/client/settings_screen.gd b/apworld/client/settings_screen.gd index b430b17..89e8b68 100644 --- a/apworld/client/settings_screen.gd +++ b/apworld/client/settings_screen.gd
@@ -100,7 +100,7 @@ func _ready():
100 server_box.offset_top = 295.0 100 server_box.offset_top = 295.0
101 server_box.offset_right = 1144.0 101 server_box.offset_right = 1144.0
102 server_box.offset_bottom = 445.0 102 server_box.offset_bottom = 445.0
103 server_box.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER 103 server_box.alignment = HORIZONTAL_ALIGNMENT_CENTER
104 server_box.caret_blink = true 104 server_box.caret_blink = true
105 panel.add_child(server_box) 105 panel.add_child(server_box)
106 106
@@ -110,7 +110,7 @@ func _ready():
110 player_box.offset_top = 477.0 110 player_box.offset_top = 477.0
111 player_box.offset_right = 1144.0 111 player_box.offset_right = 1144.0
112 player_box.offset_bottom = 627.0 112 player_box.offset_bottom = 627.0
113 player_box.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER 113 player_box.alignment = HORIZONTAL_ALIGNMENT_CENTER
114 player_box.caret_blink = true 114 player_box.caret_blink = true
115 panel.add_child(player_box) 115 panel.add_child(player_box)
116 116
@@ -120,20 +120,16 @@ func _ready():
120 password_box.offset_top = 659.0 120 password_box.offset_top = 659.0
121 password_box.offset_right = 1144.0 121 password_box.offset_right = 1144.0
122 password_box.offset_bottom = 809.0 122 password_box.offset_bottom = 809.0
123 password_box.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER 123 password_box.alignment = HORIZONTAL_ALIGNMENT_CENTER
124 password_box.caret_blink = true 124 password_box.caret_blink = true
125 panel.add_child(password_box) 125 panel.add_child(password_box)
126 126
127 var accept_dialog = AcceptDialog.new() 127 var accept_dialog = AcceptDialog.new()
128 accept_dialog.name = "AcceptDialog" 128 accept_dialog.name = "AcceptDialog"
129 accept_dialog.offset_right = 83.0
130 accept_dialog.offset_bottom = 58.0
131 panel.add_child(accept_dialog) 129 panel.add_child(accept_dialog)
132 130
133 var version_mismatch = ConfirmationDialog.new() 131 var version_mismatch = ConfirmationDialog.new()
134 version_mismatch.name = "VersionMismatch" 132 version_mismatch.name = "VersionMismatch"
135 version_mismatch.offset_right = 83.0
136 version_mismatch.offset_bottom = 58.0
137 panel.add_child(version_mismatch) 133 panel.add_child(version_mismatch)
138 134
139 var connection_history = MenuButton.new() 135 var connection_history = MenuButton.new()
diff --git a/apworld/client/source_runtime.gd b/apworld/client/source_runtime.gd index 35428ea..146587a 100644 --- a/apworld/client/source_runtime.gd +++ b/apworld/client/source_runtime.gd
@@ -7,6 +7,10 @@ func _init(path):
7 source_path = path 7 source_path = path
8 8
9 9
10func path_exists(path):
11 return FileAccess.file_exists("%s/%s" % [source_path, path])
12
13
10func load_script(path): 14func load_script(path):
11 return ResourceLoader.load("%s/%s" % [source_path, path]) 15 return ResourceLoader.load("%s/%s" % [source_path, path])
12 16
diff --git a/apworld/client/textclient.gd b/apworld/client/textclient.gd index 530eddb..ce28a3a 100644 --- a/apworld/client/textclient.gd +++ b/apworld/client/textclient.gd
@@ -4,7 +4,6 @@ var tabs
4var panel 4var panel
5var label 5var label
6var entry 6var entry
7var tracker_label
8var is_open = false 7var is_open = false
9 8
10var locations_overlay 9var locations_overlay
@@ -12,11 +11,23 @@ var location_texture
12var worldport_texture 11var worldport_texture
13var goal_texture 12var goal_texture
14 13
14var tracker_tree
15var tracker_loc_tree_item_by_id = {}
16var tracker_port_tree_item_by_id = {}
17var tracker_goal_tree_item = null
18var tracker_object_by_index = {}
19var tracker_object_by_ignored_index = {}
20var tracker_ignored_group = null
21
15var worldports_tab 22var worldports_tab
16var worldports_tree 23var worldports_tree
17var port_tree_item_by_map = {} 24var port_tree_item_by_map = {}
18var port_tree_item_by_map_port = {} 25var port_tree_item_by_map_port = {}
19 26
27const kLocation = 0
28const kWorldport = 1
29const kGoal = 2
30
20 31
21func _ready(): 32func _ready():
22 process_mode = ProcessMode.PROCESS_MODE_ALWAYS 33 process_mode = ProcessMode.PROCESS_MODE_ALWAYS
@@ -61,7 +72,7 @@ func _ready():
61 label.size_flags_horizontal = Control.SIZE_EXPAND_FILL 72 label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
62 label.size_flags_vertical = Control.SIZE_EXPAND_FILL 73 label.size_flags_vertical = Control.SIZE_EXPAND_FILL
63 label.push_font(preload("res://assets/fonts/Lingo2.ttf")) 74 label.push_font(preload("res://assets/fonts/Lingo2.ttf"))
64 label.push_font_size(36) 75 label.push_font_size(30)
65 76
66 var entry_style = StyleBoxFlat.new() 77 var entry_style = StyleBoxFlat.new()
67 entry_style.bg_color = Color(0.9, 0.9, 0.9, 1) 78 entry_style.bg_color = Color(0.9, 0.9, 0.9, 1)
@@ -89,8 +100,20 @@ func _ready():
89 tracker_margins.add_theme_constant_override("margin_bottom", 60) 100 tracker_margins.add_theme_constant_override("margin_bottom", 60)
90 tabs.add_child(tracker_margins) 101 tabs.add_child(tracker_margins)
91 102
92 tracker_label = RichTextLabel.new() 103 tracker_tree = Tree.new()
93 tracker_margins.add_child(tracker_label) 104 tracker_tree.columns = 4
105 tracker_tree.hide_root = true
106 tracker_tree.add_theme_font_size_override("font_size", 24)
107 tracker_tree.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8, 1))
108 tracker_tree.add_theme_constant_override("v_separation", 1)
109 tracker_tree.item_edited.connect(_on_tracker_button_clicked)
110 tracker_tree.set_column_expand(0, false)
111 tracker_tree.set_column_expand(1, true)
112 tracker_tree.set_column_expand(2, false)
113 tracker_tree.set_column_expand(3, false)
114 tracker_tree.set_column_custom_minimum_width(2, 200)
115 tracker_tree.set_column_custom_minimum_width(3, 200)
116 tracker_margins.add_child(tracker_tree)
94 117
95 worldports_tab = MarginContainer.new() 118 worldports_tab = MarginContainer.new()
96 worldports_tab.name = "Worldports" 119 worldports_tab.name = "Worldports"
@@ -167,14 +190,10 @@ func text_entered(text):
167 ap.client.say(cmd) 190 ap.client.say(cmd)
168 191
169 192
170func update_locations(): 193func update_locations(reset_locations = true):
171 var ap = global.get_node("Archipelago") 194 var ap = global.get_node("Archipelago")
172 var gamedata = global.get_node("Gamedata") 195 var gamedata = global.get_node("Gamedata")
173 196
174 tracker_label.clear()
175 tracker_label.push_font(preload("res://assets/fonts/Lingo2.ttf"))
176 tracker_label.push_font_size(24)
177
178 locations_overlay.clear() 197 locations_overlay.clear()
179 locations_overlay.push_font(preload("res://assets/fonts/Lingo2.ttf")) 198 locations_overlay.push_font(preload("res://assets/fonts/Lingo2.ttf"))
180 locations_overlay.push_font_size(24) 199 locations_overlay.push_font_size(24)
@@ -182,58 +201,226 @@ func update_locations():
182 locations_overlay.push_outline_color(Color(0, 0, 0, 1)) 201 locations_overlay.push_outline_color(Color(0, 0, 0, 1))
183 locations_overlay.push_outline_size(2) 202 locations_overlay.push_outline_size(2)
184 203
185 const kLocation = 0 204 var locations = []
186 const kWorldport = 1
187 const kGoal = 2
188
189 var location_names = []
190 var type_by_name = {}
191 for location_id in ap.client._accessible_locations: 205 for location_id in ap.client._accessible_locations:
192 if not ap.client._checked_locations.has(location_id): 206 if not ap.client._checked_locations.has(location_id):
193 var location_name = gamedata.location_name_by_id.get(location_id, "(Unknown)") 207 var location_name = gamedata.location_name_by_id.get(location_id, "(Unknown)")
194 location_names.append(location_name) 208 (
195 type_by_name[location_name] = kLocation 209 locations
210 . append(
211 {
212 "name": location_name,
213 "type": kLocation,
214 "id": location_id,
215 "ignored": ap._ignored_locations.has(location_id),
216 "hint": ap.client._hinted_locations.has(location_id),
217 }
218 )
219 )
196 220
197 for port_id in ap.client._accessible_worldports: 221 for port_id in ap.client._accessible_worldports:
198 if not ap.client._checked_worldports.has(port_id): 222 if not ap.client._checked_worldports.has(port_id):
199 var port_name = gamedata.get_worldport_display_name(port_id) 223 var port_name = gamedata.get_worldport_display_name(port_id)
200 location_names.append(port_name) 224 (
201 type_by_name[port_name] = kWorldport 225 locations
202 226 . append(
203 location_names.sort() 227 {
228 "name": port_name,
229 "type": kWorldport,
230 "id": port_id,
231 "ignored": false,
232 "hint": false,
233 }
234 )
235 )
236
237 locations.sort_custom(_cmp_tracker_objects)
204 238
205 if ap.client._goal_accessible: 239 if ap.client._goal_accessible:
206 var location_name = gamedata.ending_display_name_by_name[ap.kEndingNameByVictoryValue[ 240 var location_name = gamedata.ending_display_name_by_name[ap.kEndingNameByVictoryValue[
207 ap.victory_condition 241 ap.victory_condition
208 ]] 242 ]]
209 location_names.push_front(location_name) 243 (
210 type_by_name[location_name] = kGoal 244 locations
245 . push_front(
246 {
247 "name": location_name,
248 "type": kGoal,
249 "ignored": false,
250 "hint": false,
251 }
252 )
253 )
211 254
212 var count = 0 255 var count = 0
213 for location_name in location_names: 256 for location in locations:
214 tracker_label.append_text("[p]%s[/p]" % location_name) 257 if count < 18 and not location["ignored"]:
215 if count < 18:
216 locations_overlay.push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT) 258 locations_overlay.push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT)
217 locations_overlay.append_text(location_name) 259 if location["hint"]:
260 locations_overlay.push_color(Color("#fafad2"))
261 locations_overlay.append_text(location["name"])
218 locations_overlay.append_text(" ") 262 locations_overlay.append_text(" ")
219 if type_by_name[location_name] == kLocation: 263 if location["type"] == kLocation:
220 locations_overlay.add_image(location_texture) 264 locations_overlay.add_image(location_texture)
221 elif type_by_name[location_name] == kWorldport: 265 elif location["type"] == kWorldport:
222 locations_overlay.add_image(worldport_texture) 266 locations_overlay.add_image(worldport_texture)
223 elif type_by_name[location_name] == kGoal: 267 elif location["type"] == kGoal:
224 locations_overlay.add_image(goal_texture) 268 locations_overlay.add_image(goal_texture)
269 if location["hint"]:
270 locations_overlay.pop()
225 locations_overlay.pop() 271 locations_overlay.pop()
226 count += 1 272 count += 1
227 273
228 if count > 18: 274 if count > 18:
229 locations_overlay.append_text("[p align=right][lb]...[rb][/p]") 275 locations_overlay.append_text("[p align=right][lb]...[rb][/p]")
230 276
277 if reset_locations:
278 reset_tracker_tab()
279
280 var root_ti = tracker_tree.create_item(null)
281
282 for location in locations:
283 var loc_row
284
285 if location["ignored"]:
286 if tracker_ignored_group == null:
287 tracker_ignored_group = root_ti.create_child()
288 tracker_ignored_group.set_text(1, "Ignored Locations")
289 tracker_ignored_group.set_selectable(0, false)
290 tracker_ignored_group.set_selectable(1, false)
291 tracker_ignored_group.set_selectable(2, false)
292 tracker_ignored_group.set_selectable(3, false)
293
294 loc_row = tracker_ignored_group.create_child()
295 else:
296 loc_row = root_ti.create_child()
297
298 loc_row.set_cell_mode(0, TreeItem.CELL_MODE_ICON)
299 loc_row.set_selectable(0, false)
300 loc_row.set_text(1, location["name"])
301 loc_row.set_selectable(1, false)
302 if location["hint"]:
303 loc_row.set_custom_color(1, Color("#fafad2"))
304 loc_row.set_cell_mode(2, TreeItem.CELL_MODE_CUSTOM)
305 loc_row.set_text(2, "Show Path")
306 loc_row.set_custom_as_button(2, true)
307 loc_row.set_editable(2, true)
308 loc_row.set_selectable(2, false)
309 loc_row.set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER)
310 loc_row.set_selectable(3, false)
311 if location["type"] == kLocation:
312 loc_row.set_cell_mode(3, TreeItem.CELL_MODE_CUSTOM)
313 if location["ignored"]:
314 loc_row.set_text(3, "Unignore")
315 else:
316 loc_row.set_text(3, "Ignore")
317 loc_row.set_custom_as_button(3, true)
318 loc_row.set_editable(3, true)
319 loc_row.set_text_alignment(3, HORIZONTAL_ALIGNMENT_CENTER)
320
321 if location["type"] == kLocation:
322 loc_row.set_icon(0, location_texture)
323 tracker_loc_tree_item_by_id[location["id"]] = loc_row
324 elif location["type"] == kWorldport:
325 loc_row.set_icon(0, worldport_texture)
326 tracker_port_tree_item_by_id[location["id"]] = loc_row
327 elif location["type"] == kGoal:
328 loc_row.set_icon(0, goal_texture)
329 tracker_goal_tree_item = loc_row
330
331 if location["ignored"]:
332 tracker_object_by_ignored_index[loc_row.get_index()] = location
333 else:
334 tracker_object_by_index[loc_row.get_index()] = location
335 else:
336 for loc_row in tracker_tree.get_root().get_children():
337 loc_row.visible = false
338
339 for location_id in tracker_loc_tree_item_by_id.keys():
340 if (
341 ap.client._accessible_locations.has(location_id)
342 and not ap.client._checked_locations.has(location_id)
343 ):
344 tracker_loc_tree_item_by_id[location_id].visible = true
345
346 for port_id in tracker_port_tree_item_by_id.keys():
347 if (
348 ap.client._accessible_worldports.has(port_id)
349 and not ap.client._checked_worldports.has(port_id)
350 ):
351 tracker_port_tree_item_by_id[port_id].visible = true
352
353 if tracker_goal_tree_item != null and ap.client._goal_accessible:
354 tracker_goal_tree_item.visible = true
355
356 if tracker_ignored_group != null:
357 tracker_ignored_group.visible = true
358
359
360func _cmp_tracker_objects(a, b) -> bool:
361 if a["ignored"] != b["ignored"]:
362 return !a["ignored"]
363 elif a["hint"] != b["hint"]:
364 return a["hint"]
365 else:
366 return a["name"] < b["name"]
367
231 368
232func update_locations_visibility(): 369func update_locations_visibility():
233 var ap = global.get_node("Archipelago") 370 var ap = global.get_node("Archipelago")
234 locations_overlay.visible = ap.show_locations 371 locations_overlay.visible = ap.show_locations
235 372
236 373
374func _on_tracker_button_clicked():
375 var ap = global.get_node("Archipelago")
376
377 var edited_item = tracker_tree.get_edited()
378 var edited_index = edited_item.get_index()
379
380 if edited_item.get_parent() == tracker_tree.get_root():
381 if tracker_object_by_index.has(edited_index):
382 var tracker_object = tracker_object_by_index[edited_index]
383 if tracker_tree.get_edited_column() == 2:
384 var type_str = ""
385 if tracker_object["type"] == kLocation:
386 type_str = "location"
387 elif tracker_object["type"] == kWorldport:
388 type_str = "worldport"
389 elif tracker_object["type"] == kGoal:
390 type_str = "goal"
391 ap.client.getLogicalPath(type_str, tracker_object.get("id", null))
392 elif tracker_tree.get_edited_column() == 3:
393 ap.toggle_ignored_location(tracker_object["id"])
394 elif edited_item.get_parent() == tracker_ignored_group:
395 # This is the ignored locations group.
396 if (
397 tracker_object_by_ignored_index.has(edited_index)
398 and tracker_tree.get_edited_column() == 3
399 ):
400 var tracker_object = tracker_object_by_ignored_index[edited_index]
401 ap.toggle_ignored_location(tracker_object["id"])
402
403
404func display_logical_path(object_type, object_id, paths):
405 var ap = global.get_node("Archipelago")
406 var gamedata = global.get_node("Gamedata")
407
408 var location_name = "(Unknown)"
409 if object_type == "location" and object_id != null:
410 location_name = gamedata.location_name_by_id.get(object_id, "(Unknown)")
411 elif object_type == "worldport" and object_id != null:
412 location_name = gamedata.get_worldport_display_name(object_id)
413 elif object_type == "goal":
414 location_name = gamedata.ending_display_name_by_name[ap.kEndingNameByVictoryValue[
415 ap.victory_condition
416 ]]
417
418 label.append_text("[p]Path to %s:[/p]" % location_name)
419 label.append_text("[ol]" + "\n".join(paths) + "[/ol]")
420
421 panel.visible = true
422
423
237func setup_worldports(): 424func setup_worldports():
238 tabs.set_tab_hidden(2, false) 425 tabs.set_tab_hidden(2, false)
239 426
@@ -308,3 +495,14 @@ func reset():
308 port_tree_item_by_map.clear() 495 port_tree_item_by_map.clear()
309 port_tree_item_by_map_port.clear() 496 port_tree_item_by_map_port.clear()
310 worldports_tree.clear() 497 worldports_tree.clear()
498 reset_tracker_tab()
499
500
501func reset_tracker_tab():
502 tracker_loc_tree_item_by_id.clear()
503 tracker_port_tree_item_by_id.clear()
504 tracker_goal_tree_item = null
505 tracker_object_by_index.clear()
506 tracker_object_by_ignored_index.clear()
507 tracker_ignored_group = null
508 tracker_tree.clear()
diff --git a/apworld/client/unlockReaderListener.gd b/apworld/client/unlockReaderListener.gd new file mode 100644 index 0000000..a5754b9 --- /dev/null +++ b/apworld/client/unlockReaderListener.gd
@@ -0,0 +1,46 @@
1extends "res://scripts/nodes/listeners/unlockReaderListener.gd"
2
3var item_id = null
4var item_amount
5
6
7func _ready():
8 var node_path = String(
9 get_tree().get_root().get_node("scene").get_path_to(self).get_concatenated_names()
10 )
11
12 var gamedata = global.get_node("Gamedata")
13 var door_id = gamedata.get_door_for_map_node_path(global.map, node_path)
14 if door_id != null:
15 var ap = global.get_node("Archipelago")
16 var item_lock = ap.get_item_id_for_door(door_id)
17
18 if item_lock != null:
19 item_id = item_lock[0]
20 item_amount = item_lock[1]
21
22 self.senders = []
23 self.senderGroup = []
24 self.nested = false
25 self.complete_at = 0
26 self.max_length = 0
27 self.excludeSenders = []
28
29 super._ready()
30
31
32func _readier():
33 if item_id != null:
34 var ap = global.get_node("Archipelago")
35
36 if ap.client.getItemAmount(item_id) >= item_amount:
37 handleTriggered()
38 else:
39 super._readier()
40
41
42func handleTriggered():
43 if item_id != null:
44 emit_signal("trigger")
45 else:
46 super.handleTriggered()
diff --git a/apworld/client/worldportListener.gd b/apworld/client/worldportListener.gd index 5c2faff..4cff8e9 100644 --- a/apworld/client/worldportListener.gd +++ b/apworld/client/worldportListener.gd
@@ -2,7 +2,7 @@ extends "res://scripts/nodes/listeners/worldportListener.gd"
2 2
3 3
4func handleTriggered(): 4func handleTriggered():
5 if exit == "menus/credits": 5 if exit.begins_with("menus/credits"):
6 return 6 return
7 7
8 super.handleTriggered() 8 super.handleTriggered()
diff --git a/apworld/context.py b/apworld/context.py index c367b6c..86392f9 100644 --- a/apworld/context.py +++ b/apworld/context.py
@@ -2,6 +2,7 @@ import asyncio
2import os 2import os
3import pkgutil 3import pkgutil
4import subprocess 4import subprocess
5import sys
5from typing import Any 6from typing import Any
6 7
7import websockets 8import websockets
@@ -29,6 +30,16 @@ KEY_STORAGE_MAPPING = {
29REVERSE_KEY_STORAGE_MAPPING = {t: k for k, t in KEY_STORAGE_MAPPING.items()} 30REVERSE_KEY_STORAGE_MAPPING = {t: k for k, t in KEY_STORAGE_MAPPING.items()}
30 31
31 32
33# There is a distinction between an object's ID and its AP ID. The latter is stable between releases, whereas the former
34# can change and is also namespaced based on the object type. We should only store AP IDs in multiworld state (such as
35# slot data and data storage) to increase compatability between releases. The data we currently store is:
36# - Port pairings for worldport shuffle (slot data)
37# - Checked worldports for worldport shuffle (data storage)
38# - Latched doors (data storage)
39# The client generally deals in the actual object IDs rather than the stable IDs, although it does have to convert the
40# port pairing IDs when reading them from slot data. The context (this file here) does the work of converting back and
41# forth between the values. AP IDs are converted to IDs after reading them from data storage, and IDs are converted to
42# AP IDs before sending them to data storage.
32class Lingo2Manager: 43class Lingo2Manager:
33 game_ctx: "Lingo2GameContext" 44 game_ctx: "Lingo2GameContext"
34 client_ctx: "Lingo2ClientContext" 45 client_ctx: "Lingo2ClientContext"
@@ -37,6 +48,8 @@ class Lingo2Manager:
37 keyboard: dict[str, int] 48 keyboard: dict[str, int]
38 worldports: set[int] 49 worldports: set[int]
39 goaled: bool 50 goaled: bool
51 latches: set[int]
52 hinted_locations: set[int]
40 53
41 def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"): 54 def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"):
42 self.game_ctx = game_ctx 55 self.game_ctx = game_ctx
@@ -45,7 +58,6 @@ class Lingo2Manager:
45 self.client_ctx.manager = self 58 self.client_ctx.manager = self
46 self.tracker = Tracker(self) 59 self.tracker = Tracker(self)
47 self.keyboard = {} 60 self.keyboard = {}
48 self.worldports = set()
49 61
50 self.reset() 62 self.reset()
51 63
@@ -55,6 +67,8 @@ class Lingo2Manager:
55 67
56 self.worldports = set() 68 self.worldports = set()
57 self.goaled = False 69 self.goaled = False
70 self.latches = set()
71 self.hinted_locations = set()
58 72
59 def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]: 73 def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]:
60 ret: dict[str, int] = {} 74 ret: dict[str, int] = {}
@@ -70,6 +84,7 @@ class Lingo2Manager:
70 84
71 return ret 85 return ret
72 86
87 # Input should be real IDs, not AP IDs
73 def update_worldports(self, new_worldports: set[int]) -> set[int]: 88 def update_worldports(self, new_worldports: set[int]) -> set[int]:
74 ret = new_worldports.difference(self.worldports) 89 ret = new_worldports.difference(self.worldports)
75 self.worldports.update(new_worldports) 90 self.worldports.update(new_worldports)
@@ -80,6 +95,18 @@ class Lingo2Manager:
80 95
81 return ret 96 return ret
82 97
98 def update_latches(self, new_latches: set[int]) -> set[int]:
99 ret = new_latches.difference(self.latches)
100 self.latches.update(new_latches)
101
102 return ret
103
104 def update_hinted_locations(self, new_locs: set[int]) -> set[int]:
105 ret = new_locs.difference(self.hinted_locations)
106 self.hinted_locations.update(new_locs)
107
108 return ret
109
83 110
84class Lingo2GameContext: 111class Lingo2GameContext:
85 server: Endpoint | None 112 server: Endpoint | None
@@ -106,6 +133,17 @@ class Lingo2GameContext:
106 133
107 async_start(self.send_msgs([msg]), name="game Connected") 134 async_start(self.send_msgs([msg]), name="game Connected")
108 135
136 def send_connection_refused(self, text):
137 if self.server is None:
138 return
139
140 msg = {
141 "cmd": "ConnectionRefused",
142 "text": text,
143 }
144
145 async_start(self.send_msgs([msg]), name="game ConnectionRefused")
146
109 def send_item_sent_notification(self, item_name, receiver_name, item_flags): 147 def send_item_sent_notification(self, item_name, receiver_name, item_flags):
110 if self.server is None: 148 if self.server is None:
111 return 149 return
@@ -206,6 +244,7 @@ class Lingo2GameContext:
206 244
207 async_start(self.send_msgs([msg]), name="update keyboard") 245 async_start(self.send_msgs([msg]), name="update keyboard")
208 246
247 # Input should be real IDs, not AP IDs
209 def send_update_worldports(self, worldports): 248 def send_update_worldports(self, worldports):
210 if self.server is None: 249 if self.server is None:
211 return 250 return
@@ -217,6 +256,54 @@ class Lingo2GameContext:
217 256
218 async_start(self.send_msgs([msg]), name="update worldports") 257 async_start(self.send_msgs([msg]), name="update worldports")
219 258
259 def send_path_reply(self, object_type: str, object_id: int | None, path: list[str]):
260 if self.server is None:
261 return
262
263 msg = {
264 "cmd": "PathReply",
265 "type": object_type,
266 "path": path,
267 }
268
269 if object_id is not None:
270 msg["id"] = object_id
271
272 async_start(self.send_msgs([msg]), name="path reply")
273
274 def send_update_latches(self, latches):
275 if self.server is None:
276 return
277
278 msg = {
279 "cmd": "UpdateLatches",
280 "latches": latches,
281 }
282
283 async_start(self.send_msgs([msg]), name="update latches")
284
285 def send_ignored_locations(self, ignored_locations):
286 if self.server is None:
287 return
288
289 msg = {
290 "cmd": "SetIgnoredLocations",
291 "locations": ignored_locations,
292 }
293
294 async_start(self.send_msgs([msg]), name="set ignored locations")
295
296 def send_update_hinted_locations(self, hinted_locations):
297 if self.server is None:
298 return
299
300 msg = {
301 "cmd": "UpdateHintedLocations",
302 "locations": hinted_locations,
303 }
304
305 async_start(self.send_msgs([msg]), name="update hinted locations")
306
220 async def send_msgs(self, msgs: list[Any]) -> None: 307 async def send_msgs(self, msgs: list[Any]) -> None:
221 """ `msgs` JSON serializable """ 308 """ `msgs` JSON serializable """
222 if not self.server or not self.server.socket.open or self.server.socket.closed: 309 if not self.server or not self.server.socket.open or self.server.socket.closed:
@@ -231,6 +318,7 @@ class Lingo2ClientContext(CommonContext):
231 items_handling = 0b111 318 items_handling = 0b111
232 319
233 slot_data: dict[str, Any] | None 320 slot_data: dict[str, Any] | None
321 hints_data_storage_key: str
234 victory_data_storage_key: str 322 victory_data_storage_key: str
235 323
236 def __init__(self, server_address: str | None = None, password: str | None = None): 324 def __init__(self, server_address: str | None = None, password: str | None = None):
@@ -242,8 +330,17 @@ class Lingo2ClientContext(CommonContext):
242 return ui 330 return ui
243 331
244 async def server_auth(self, password_requested: bool = False): 332 async def server_auth(self, password_requested: bool = False):
245 self.auth = self.username 333 if password_requested and not self.password:
246 await self.send_connect() 334 self.manager.game_ctx.send_connection_refused("Invalid password.")
335 else:
336 self.auth = self.username
337 await self.send_connect()
338
339 def handle_connection_loss(self, msg: str):
340 super().handle_connection_loss(msg)
341
342 exc_info = sys.exc_info()
343 self.manager.game_ctx.send_connection_refused(str(exc_info[1]))
247 344
248 def on_package(self, cmd: str, args: dict): 345 def on_package(self, cmd: str, args: dict):
249 if cmd == "RoomInfo": 346 if cmd == "RoomInfo":
@@ -259,10 +356,12 @@ class Lingo2ClientContext(CommonContext):
259 self.manager.tracker.set_checked_locations(self.checked_locations) 356 self.manager.tracker.set_checked_locations(self.checked_locations)
260 self.manager.game_ctx.send_accessible_locations() 357 self.manager.game_ctx.send_accessible_locations()
261 358
359 self.hints_data_storage_key = f"_read_hints_{self.team}_{self.slot}"
262 self.victory_data_storage_key = f"_read_client_status_{self.team}_{self.slot}" 360 self.victory_data_storage_key = f"_read_client_status_{self.team}_{self.slot}"
263 361
264 self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2"), 362 self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2"),
265 self.victory_data_storage_key) 363 self.victory_data_storage_key, self.get_datastorage_key("latches"),
364 self.get_datastorage_key("ignored_locations"))
266 msg_batch = [{ 365 msg_batch = [{
267 "cmd": "Set", 366 "cmd": "Set",
268 "key": self.get_datastorage_key("keyboard1"), 367 "key": self.get_datastorage_key("keyboard1"),
@@ -275,6 +374,18 @@ class Lingo2ClientContext(CommonContext):
275 "default": 0, 374 "default": 0,
276 "want_reply": True, 375 "want_reply": True,
277 "operations": [{"operation": "default", "value": 0}] 376 "operations": [{"operation": "default", "value": 0}]
377 }, {
378 "cmd": "Set",
379 "key": self.get_datastorage_key("latches"),
380 "default": [],
381 "want_reply": True,
382 "operations": [{"operation": "default", "value": []}]
383 }, {
384 "cmd": "Set",
385 "key": self.get_datastorage_key("ignored_locations"),
386 "default": [],
387 "want_reply": True,
388 "operations": [{"operation": "default", "value": []}]
278 }] 389 }]
279 390
280 if self.slot_data.get("shuffle_worldports", False): 391 if self.slot_data.get("shuffle_worldports", False):
@@ -373,17 +484,29 @@ class Lingo2ClientContext(CommonContext):
373 for k, v in args["keys"].items(): 484 for k, v in args["keys"].items():
374 if k == self.victory_data_storage_key: 485 if k == self.victory_data_storage_key:
375 self.handle_status_update(v) 486 self.handle_status_update(v)
487 elif k == self.hints_data_storage_key:
488 self.update_hints()
376 elif cmd == "SetReply": 489 elif cmd == "SetReply":
377 if args["key"] == self.get_datastorage_key("keyboard1"): 490 if args["key"] == self.get_datastorage_key("keyboard1"):
378 self.handle_keyboard_update(1, args) 491 self.handle_keyboard_update(1, args)
379 elif args["key"] == self.get_datastorage_key("keyboard2"): 492 elif args["key"] == self.get_datastorage_key("keyboard2"):
380 self.handle_keyboard_update(2, args) 493 self.handle_keyboard_update(2, args)
381 elif args["key"] == self.get_datastorage_key("worldports"): 494 elif args["key"] == self.get_datastorage_key("worldports"):
382 updates = self.manager.update_worldports(set(args["value"])) 495 port_ids = set(Lingo2World.static_logic.port_id_by_ap_id[ap_id] for ap_id in args["value"])
496 updates = self.manager.update_worldports(port_ids)
383 if len(updates) > 0: 497 if len(updates) > 0:
384 self.manager.game_ctx.send_update_worldports(updates) 498 self.manager.game_ctx.send_update_worldports(updates)
385 elif args["key"] == self.victory_data_storage_key: 499 elif args["key"] == self.victory_data_storage_key:
386 self.handle_status_update(args["value"]) 500 self.handle_status_update(args["value"])
501 elif args["key"] == self.get_datastorage_key("latches"):
502 door_ids = set(Lingo2World.static_logic.door_id_by_ap_id[ap_id] for ap_id in args["value"])
503 updates = self.manager.update_latches(door_ids)
504 if len(updates) > 0:
505 self.manager.game_ctx.send_update_latches(updates)
506 elif args["key"] == self.get_datastorage_key("ignored_locations"):
507 self.manager.game_ctx.send_ignored_locations(args["value"])
508 elif args["key"] == self.hints_data_storage_key:
509 self.update_hints()
387 510
388 def get_datastorage_key(self, name: str): 511 def get_datastorage_key(self, name: str):
389 return f"Lingo2_{self.slot}_{name}" 512 return f"Lingo2_{self.slot}_{name}"
@@ -449,14 +572,16 @@ class Lingo2ClientContext(CommonContext):
449 if len(updates) > 0: 572 if len(updates) > 0:
450 self.manager.game_ctx.send_update_keyboard(updates) 573 self.manager.game_ctx.send_update_keyboard(updates)
451 574
575 # Input should be real IDs, not AP IDs
452 async def update_worldports(self, updates: set[int]): 576 async def update_worldports(self, updates: set[int]):
577 port_ap_ids = [Lingo2World.static_logic.objects.ports[port_id].ap_id for port_id in updates]
453 await self.send_msgs([{ 578 await self.send_msgs([{
454 "cmd": "Set", 579 "cmd": "Set",
455 "key": self.get_datastorage_key("worldports"), 580 "key": self.get_datastorage_key("worldports"),
456 "want_reply": True, 581 "want_reply": True,
457 "operations": [{ 582 "operations": [{
458 "operation": "update", 583 "operation": "update",
459 "value": updates 584 "value": port_ap_ids
460 }] 585 }]
461 }]) 586 }])
462 587
@@ -465,6 +590,48 @@ class Lingo2ClientContext(CommonContext):
465 self.manager.tracker.refresh_state() 590 self.manager.tracker.refresh_state()
466 self.manager.game_ctx.send_accessible_locations() 591 self.manager.game_ctx.send_accessible_locations()
467 592
593 async def update_latches(self, updates: set[int]):
594 door_ap_ids = [Lingo2World.static_logic.objects.doors[door_id].ap_id for door_id in updates]
595 await self.send_msgs([{
596 "cmd": "Set",
597 "key": self.get_datastorage_key("latches"),
598 "want_reply": True,
599 "operations": [{
600 "operation": "update",
601 "value": door_ap_ids
602 }]
603 }])
604
605 async def add_ignored_location(self, loc_id: int):
606 await self.send_msgs([{
607 "cmd": "Set",
608 "key": self.get_datastorage_key("ignored_locations"),
609 "want_reply": True,
610 "operations": [{
611 "operation": "update",
612 "value": [loc_id]
613 }]
614 }])
615
616 async def remove_ignored_location(self, loc_id: int):
617 await self.send_msgs([{
618 "cmd": "Set",
619 "key": self.get_datastorage_key("ignored_locations"),
620 "want_reply": True,
621 "operations": [{
622 "operation": "remove",
623 "value": loc_id
624 }]
625 }])
626
627 def update_hints(self):
628 hints = self.stored_data.get(self.hints_data_storage_key, [])
629
630 hinted_locations = set(hint["location"] for hint in hints if hint["finding_player"] == self.slot)
631 updates = self.manager.update_hinted_locations(hinted_locations)
632 if len(updates) > 0:
633 self.manager.game_ctx.send_update_hinted_locations(updates)
634
468 635
469async def pipe_loop(manager: Lingo2Manager): 636async def pipe_loop(manager: Lingo2Manager):
470 while not manager.client_ctx.exit_event.is_set(): 637 while not manager.client_ctx.exit_event.is_set():
@@ -489,6 +656,8 @@ async def process_game_cmd(manager: Lingo2Manager, args: dict):
489 cmd = args["cmd"] 656 cmd = args["cmd"]
490 657
491 if cmd == "Connect": 658 if cmd == "Connect":
659 manager.client_ctx.seed_name = None
660
492 server = args.get("server") 661 server = args.get("server")
493 player = args.get("player") 662 player = args.get("player")
494 password = args.get("password") 663 password = args.get("password")
@@ -500,6 +669,8 @@ async def process_game_cmd(manager: Lingo2Manager, args: dict):
500 669
501 async_start(manager.client_ctx.connect(server_address), name="client connect") 670 async_start(manager.client_ctx.connect(server_address), name="client connect")
502 elif cmd == "Disconnect": 671 elif cmd == "Disconnect":
672 manager.client_ctx.seed_name = None
673
503 async_start(manager.client_ctx.disconnect(), name="client disconnect") 674 async_start(manager.client_ctx.disconnect(), name="client disconnect")
504 elif cmd in ["Sync", "LocationChecks", "Say", "StatusUpdate", "LocationScouts"]: 675 elif cmd in ["Sync", "LocationChecks", "Say", "StatusUpdate", "LocationScouts"]:
505 async_start(manager.client_ctx.send_msgs([args]), name="client forward") 676 async_start(manager.client_ctx.send_msgs([args]), name="client forward")
@@ -509,14 +680,38 @@ async def process_game_cmd(manager: Lingo2Manager, args: dict):
509 async_start(manager.client_ctx.update_keyboard(updates), name="client update keyboard") 680 async_start(manager.client_ctx.update_keyboard(updates), name="client update keyboard")
510 elif cmd == "CheckWorldport": 681 elif cmd == "CheckWorldport":
511 port_id = args["port_id"] 682 port_id = args["port_id"]
683 port_ap_id = Lingo2World.static_logic.objects.ports[port_id].ap_id
512 worldports = {port_id} 684 worldports = {port_id}
513 if str(port_id) in manager.client_ctx.slot_data["port_pairings"]: 685
514 worldports.add(manager.client_ctx.slot_data["port_pairings"][str(port_id)]) 686 # Also check the reverse port if it's a two-way connection.
687 port_pairings = manager.client_ctx.slot_data["port_pairings"]
688 if str(port_ap_id) in port_pairings and\
689 port_pairings.get(str(port_pairings[str(port_ap_id)]), None) == port_ap_id:
690 worldports.add(Lingo2World.static_logic.port_id_by_ap_id[port_pairings[str(port_ap_id)]])
515 691
516 updates = manager.update_worldports(worldports) 692 updates = manager.update_worldports(worldports)
517 if len(updates) > 0: 693 if len(updates) > 0:
518 async_start(manager.client_ctx.update_worldports(updates), name="client update worldports") 694 async_start(manager.client_ctx.update_worldports(updates), name="client update worldports")
519 manager.game_ctx.send_update_worldports(updates) 695 manager.game_ctx.send_update_worldports(updates)
696 elif cmd == "GetPath":
697 path = None
698
699 if args["type"] == "location":
700 path = manager.tracker.get_path_to_location(args["id"])
701 elif args["type"] == "worldport":
702 path = manager.tracker.get_path_to_port(args["id"])
703 elif args["type"] == "goal":
704 path = manager.tracker.get_path_to_goal()
705
706 manager.game_ctx.send_path_reply(args["type"], args.get("id", None), path)
707 elif cmd == "LatchDoor":
708 updates = manager.update_latches({args["door"]})
709 if len(updates) > 0:
710 async_start(manager.client_ctx.update_latches(updates), name="client update latches")
711 elif cmd == "IgnoreLocation":
712 async_start(manager.client_ctx.add_ignored_location(args["id"]), name="client ignore loc")
713 elif cmd == "UnignoreLocation":
714 async_start(manager.client_ctx.remove_ignored_location(args["id"]), name="client unignore loc")
520 elif cmd == "Quit": 715 elif cmd == "Quit":
521 manager.client_ctx.exit_event.set() 716 manager.client_ctx.exit_event.set()
522 717
@@ -564,9 +759,12 @@ async def run_game():
564 759
565def client_main(*launch_args: str) -> None: 760def client_main(*launch_args: str) -> None:
566 async def main(args): 761 async def main(args):
567 async_start(run_game()) 762 if settings.get_settings().lingo2_options.start_game:
763 async_start(run_game())
568 764
569 client_ctx = Lingo2ClientContext(args.connect, args.password) 765 client_ctx = Lingo2ClientContext(args.connect, args.password)
766 client_ctx.auth = args.name
767
570 game_ctx = Lingo2GameContext() 768 game_ctx = Lingo2GameContext()
571 manager = Lingo2Manager(game_ctx, client_ctx) 769 manager = Lingo2Manager(game_ctx, client_ctx)
572 770
diff --git a/apworld/options.py b/apworld/options.py index 795010a..f687434 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 3from Options import PerGameCommonOptions, Toggle, Choice, DefaultOnToggle, Range, OptionSet
4 4
5 5
6class ShuffleDoors(DefaultOnToggle): 6class ShuffleDoors(DefaultOnToggle):
@@ -56,8 +56,7 @@ class ShuffleWorldports(Toggle):
56 """ 56 """
57 Randomizes the connections between maps. This affects worldports only, which are the loading zones you walk into in 57 Randomizes the connections between maps. This affects worldports only, which are the loading zones you walk into in
58 order to change maps. This does not affect paintings, panels that teleport you, or certain other special connections 58 order to change maps. This does not affect paintings, panels that teleport you, or certain other special connections
59 like the one between The Shop and Control Center. Connections that depend on placing letters in keyholders are also 59 like the one between The Shop and Control Center.
60 currently not shuffled.
61 """ 60 """
62 display_name = "Shuffle Worldports" 61 display_name = "Shuffle Worldports"
63 62
@@ -92,6 +91,36 @@ class CyanDoorBehavior(Choice):
92 option_item = 2 91 option_item = 2
93 92
94 93
94class EnableIcarus(Toggle):
95 """
96 Controls whether Icarus is randomized. If disabled, which is the default, no locations or items will be created for
97 it, and its worldport will not be shuffled when worldport shuffle is on.
98 """
99 display_name = "Enable Icarus"
100
101
102class EnableGiftMaps(OptionSet):
103 """
104 Controls whether the beta tester gift maps are randomized. By default, these are not accessible at all from within
105 the randomizer. This option allows you to enter the maps, and creates items and locations for them. If worldport
106 shuffle is on, their worldports will be included in the randomization.
107
108 The gift maps are accessed via a panel in The Entry's Starting Room, which only appears if at least one gift map is
109 enabled. It is also treated like a cyan door, and will not appear until the condition specified in the Cyan Door
110 Behavior option is satisfied. Solving this panel with the name of one of the beta testers will teleport you to their
111 corresponding gift map.
112
113 In the base game, nothing happens once you complete a gift map. Masteries have been added to the gift maps in the
114 randomizer so that the player can be rewarded for completing them.
115
116 Note that the gift maps were originally only intended to be played by specific people, and as a result may be
117 frustrating or require knowledge of inside jokes. The Crystalline is particularly difficult as it requires
118 completing a parkour course.
119 """
120 display_name = "Enable Gift Maps"
121 valid_keys = ["The Advanced", "The Charismatic", "The Crystalline", "The Fuzzy", "The Stellar"]
122
123
95class DaedalusRoofAccess(Toggle): 124class DaedalusRoofAccess(Toggle):
96 """ 125 """
97 If enabled, the player will be logically expected to be able to go from the castle entrance to any part of Daedalus 126 If enabled, the player will be logically expected to be able to go from the castle entrance to any part of Daedalus
@@ -152,6 +181,26 @@ class VictoryCondition(Choice):
152 option_white_ending = 12 181 option_white_ending = 12
153 182
154 183
184class EndingsRequirement(Range):
185 """The number of endings required to unlock White Ending."""
186 display_name = "Endings Requirement"
187 range_start = 0
188 range_end = 12
189 default = 12
190
191
192class MasteriesRequirement(Range):
193 """The number of masteries required to unlock White Ending.
194
195 There are only 13 masteries in the base game, but some of the other slot options may add more masteries to the
196 world. If the chosen number of masteries is higher than the total in your world, it will be automatically lowered to
197 the maximum."""
198 display_name = "Masteries Requirement"
199 range_start = 0
200 range_end = 19
201 default = 0
202
203
155class TrapPercentage(Range): 204class TrapPercentage(Range):
156 """Replaces junk items with traps, at the specified rate.""" 205 """Replaces junk items with traps, at the specified rate."""
157 display_name = "Trap Percentage" 206 display_name = "Trap Percentage"
@@ -170,8 +219,12 @@ class Lingo2Options(PerGameCommonOptions):
170 shuffle_worldports: ShuffleWorldports 219 shuffle_worldports: ShuffleWorldports
171 keyholder_sanity: KeyholderSanity 220 keyholder_sanity: KeyholderSanity
172 cyan_door_behavior: CyanDoorBehavior 221 cyan_door_behavior: CyanDoorBehavior
222 enable_icarus: EnableIcarus
223 enable_gift_maps: EnableGiftMaps
173 daedalus_roof_access: DaedalusRoofAccess 224 daedalus_roof_access: DaedalusRoofAccess
174 strict_purple_ending: StrictPurpleEnding 225 strict_purple_ending: StrictPurpleEnding
175 strict_cyan_ending: StrictCyanEnding 226 strict_cyan_ending: StrictCyanEnding
176 victory_condition: VictoryCondition 227 victory_condition: VictoryCondition
228 endings_requirement: EndingsRequirement
229 masteries_requirement: MasteriesRequirement
177 trap_percentage: TrapPercentage 230 trap_percentage: TrapPercentage
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 5be066d..3ee8f38 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py
@@ -4,7 +4,7 @@ from .generated import data_pb2 as data_pb2
4from .items import SYMBOL_ITEMS 4from .items import SYMBOL_ITEMS
5from typing import TYPE_CHECKING, NamedTuple 5from typing import TYPE_CHECKING, NamedTuple
6 6
7from .options import VictoryCondition, ShuffleLetters, CyanDoorBehavior 7from .options import ShuffleLetters, CyanDoorBehavior
8 8
9if TYPE_CHECKING: 9if TYPE_CHECKING:
10 from . import Lingo2World 10 from . import Lingo2World
@@ -73,7 +73,7 @@ class AccessRequirements:
73 self.cyans = self.cyans or other.cyans 73 self.cyans = self.cyans or other.cyans
74 74
75 for disjunction in other.or_logic: 75 for disjunction in other.or_logic:
76 self.or_logic.append(disjunction) 76 self.or_logic.append([sub_req.copy() for sub_req in disjunction])
77 77
78 if other.complete_at is not None: 78 if other.complete_at is not None:
79 # Merging multiple requirements that use complete_at sucks, and is part of why we want to minimize use of 79 # Merging multiple requirements that use complete_at sucks, and is part of why we want to minimize use of
@@ -84,7 +84,7 @@ class AccessRequirements:
84 84
85 left_req = AccessRequirements() 85 left_req = AccessRequirements()
86 left_req.complete_at = self.complete_at 86 left_req.complete_at = self.complete_at
87 left_req.possibilities = self.possibilities 87 left_req.possibilities = [sub_req.copy() for sub_req in self.possibilities]
88 self.or_logic.append([left_req]) 88 self.or_logic.append([left_req])
89 89
90 self.complete_at = None 90 self.complete_at = None
@@ -92,11 +92,11 @@ class AccessRequirements:
92 92
93 right_req = AccessRequirements() 93 right_req = AccessRequirements()
94 right_req.complete_at = other.complete_at 94 right_req.complete_at = other.complete_at
95 right_req.possibilities = other.possibilities 95 right_req.possibilities = [sub_req.copy() for sub_req in other.possibilities]
96 self.or_logic.append([right_req]) 96 self.or_logic.append([right_req])
97 else: 97 else:
98 self.complete_at = other.complete_at 98 self.complete_at = other.complete_at
99 self.possibilities = other.possibilities 99 self.possibilities = [sub_req.copy() for sub_req in other.possibilities]
100 100
101 def is_empty(self) -> bool: 101 def is_empty(self) -> bool:
102 return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0 102 return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0
@@ -202,6 +202,8 @@ class LetterBehavior(IntEnum):
202class Lingo2PlayerLogic: 202class Lingo2PlayerLogic:
203 world: "Lingo2World" 203 world: "Lingo2World"
204 204
205 shuffled_maps: set[int]
206
205 locations_by_room: dict[int, list[PlayerLocation]] 207 locations_by_room: dict[int, list[PlayerLocation]]
206 event_loc_item_by_room: dict[int, dict[str, str]] 208 event_loc_item_by_room: dict[int, dict[str, str]]
207 209
@@ -214,6 +216,7 @@ class Lingo2PlayerLogic:
214 real_items: list[str] 216 real_items: list[str]
215 217
216 double_letter_amount: dict[str, int] 218 double_letter_amount: dict[str, int]
219 goal_room_id: int
217 220
218 def __init__(self, world: "Lingo2World"): 221 def __init__(self, world: "Lingo2World"):
219 self.world = world 222 self.world = world
@@ -226,9 +229,45 @@ class Lingo2PlayerLogic:
226 self.real_items = list() 229 self.real_items = list()
227 self.double_letter_amount = dict() 230 self.double_letter_amount = dict()
228 231
232 def should_shuffle_map(game_map) -> bool:
233 if game_map.type == data_pb2.MapType.NORMAL_MAP:
234 return True
235 elif game_map.type == data_pb2.MapType.ICARUS:
236 return bool(world.options.enable_icarus)
237 elif game_map.type == data_pb2.MapType.GIFT_MAP:
238 if game_map.name == "the_advanced":
239 return "The Advanced" in world.options.enable_gift_maps.value
240 elif game_map.name == "the_charismatic":
241 return "The Charismatic" in world.options.enable_gift_maps.value
242 elif game_map.name == "the_crystalline":
243 return "The Crystalline" in world.options.enable_gift_maps.value
244 elif game_map.name == "the_fuzzy":
245 return "The Fuzzy" in world.options.enable_gift_maps.value
246 elif game_map.name == "the_stellar":
247 return "The Stellar" in world.options.enable_gift_maps.value
248
249 return False
250
251 self.shuffled_maps = set(game_map.id for game_map in world.static_logic.objects.maps
252 if should_shuffle_map(game_map))
253
254 maximum_masteries = 13 + len(world.options.enable_gift_maps.value)
255 if world.options.enable_icarus:
256 maximum_masteries += 1
257
258 if world.options.masteries_requirement > maximum_masteries:
259 world.options.masteries_requirement.value = maximum_masteries
260
261 if "The Fuzzy" in world.options.enable_gift_maps.value:
262 self.real_items.append("Numbers")
263
229 if self.world.options.shuffle_doors: 264 if self.world.options.shuffle_doors:
230 for progressive in world.static_logic.objects.progressives: 265 for progressive in world.static_logic.objects.progressives:
231 for i in range(0, len(progressive.doors)): 266 for i in range(0, len(progressive.doors)):
267 door = world.static_logic.objects.doors[progressive.doors[i]]
268 if door.map_id not in self.shuffled_maps:
269 continue
270
232 self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1) 271 self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1)
233 self.real_items.append(progressive.name) 272 self.real_items.append(progressive.name)
234 273
@@ -245,14 +284,21 @@ class Lingo2PlayerLogic:
245 else: 284 else:
246 continue 285 continue
247 286
248 for door in door_group.doors: 287 shuffleable_doors = [door_id for door_id in door_group.doors
249 self.item_by_door[door] = (door_group.name, 1) 288 if world.static_logic.objects.doors[door_id].map_id in self.shuffled_maps]
250 289
251 self.real_items.append(door_group.name) 290 if len(shuffleable_doors) > 0:
291 for door in shuffleable_doors:
292 self.item_by_door[door] = (door_group.name, 1)
293
294 self.real_items.append(door_group.name)
252 295
253 # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled 296 # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled
254 # before we calculate any access requirements. 297 # before we calculate any access requirements.
255 for door in world.static_logic.objects.doors: 298 for door in world.static_logic.objects.doors:
299 if door.map_id not in self.shuffled_maps:
300 continue
301
256 if door.type in [data_pb2.DoorType.EVENT, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: 302 if door.type in [data_pb2.DoorType.EVENT, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]:
257 continue 303 continue
258 304
@@ -281,18 +327,28 @@ class Lingo2PlayerLogic:
281 if door_group.type != data_pb2.DoorGroupType.CYAN_DOORS: 327 if door_group.type != data_pb2.DoorGroupType.CYAN_DOORS:
282 continue 328 continue
283 329
284 for door in door_group.doors: 330 shuffleable_doors = [door_id for door_id in door_group.doors
285 if not door in self.item_by_door: 331 if world.static_logic.objects.doors[door_id].map_id in self.shuffled_maps
332 and door_id not in self.item_by_door]
333
334 if len(shuffleable_doors) > 0:
335 for door in shuffleable_doors:
286 self.item_by_door[door] = (door_group.name, 1) 336 self.item_by_door[door] = (door_group.name, 1)
287 337
288 self.real_items.append(door_group.name) 338 self.real_items.append(door_group.name)
289 339
290 for door in world.static_logic.objects.doors: 340 for door in world.static_logic.objects.doors:
341 if door.map_id not in self.shuffled_maps:
342 continue
343
291 if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: 344 if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]:
292 self.locations_by_room.setdefault(door.room_id, []).append(PlayerLocation(door.ap_id, 345 self.locations_by_room.setdefault(door.room_id, []).append(PlayerLocation(door.ap_id,
293 self.get_door_reqs(door.id))) 346 self.get_door_reqs(door.id)))
294 347
295 for letter in world.static_logic.objects.letters: 348 for letter in world.static_logic.objects.letters:
349 if world.static_logic.get_room_object_map_id(letter) not in self.shuffled_maps:
350 continue
351
296 self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id, 352 self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id,
297 AccessRequirements())) 353 AccessRequirements()))
298 behavior = self.get_letter_behavior(letter.key, letter.level2) 354 behavior = self.get_letter_behavior(letter.key, letter.level2)
@@ -312,27 +368,42 @@ class Lingo2PlayerLogic:
312 self.double_letter_amount[letter.key.upper()] = self.double_letter_amount.get(letter.key.upper(), 0) + 1 368 self.double_letter_amount[letter.key.upper()] = self.double_letter_amount.get(letter.key.upper(), 0) + 1
313 369
314 for mastery in world.static_logic.objects.masteries: 370 for mastery in world.static_logic.objects.masteries:
371 if world.static_logic.get_room_object_map_id(mastery) not in self.shuffled_maps:
372 continue
373
315 self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id, 374 self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id,
316 AccessRequirements())) 375 AccessRequirements()))
317 376
377 if world.options.masteries_requirement > 0:
378 event_name = f"{world.static_logic.get_room_object_map_name(mastery)} - Mastery (Collected)"
379 self.event_loc_item_by_room.setdefault(mastery.room_id, {})[event_name] = "Mastery"
380
318 for ending in world.static_logic.objects.endings: 381 for ending in world.static_logic.objects.endings:
319 # Don't create a location for your selected ending, and never create a location for White Ending. 382 if world.static_logic.get_room_object_map_id(ending) not in self.shuffled_maps:
383 continue
384
385 # Don't create a location for your selected ending. Also don't create a location for White Ending if it's
386 # necessarily in the postgame, i.e. it requires all 12 other endings.
320 if world.options.victory_condition.current_key.removesuffix("_ending").upper() != ending.name\ 387 if world.options.victory_condition.current_key.removesuffix("_ending").upper() != ending.name\
321 and ending.name != "WHITE": 388 and (ending.name != "WHITE" or world.options.endings_requirement < 12):
322 self.locations_by_room.setdefault(ending.room_id, []).append(PlayerLocation(ending.ap_id, 389 self.locations_by_room.setdefault(ending.room_id, []).append(PlayerLocation(ending.ap_id,
323 AccessRequirements())) 390 AccessRequirements()))
324 391
325 event_name = f"{ending.name.capitalize()} Ending (Achieved)"
326 item_name = event_name
327
328 if world.options.victory_condition.current_key.removesuffix("_ending").upper() == ending.name: 392 if world.options.victory_condition.current_key.removesuffix("_ending").upper() == ending.name:
329 item_name = "Victory" 393 event_name = f"{ending.name.capitalize()} Ending (Goal)"
394 self.event_loc_item_by_room.setdefault(ending.room_id, {})[event_name] = "Victory"
395 self.goal_room_id = ending.room_id
330 396
331 self.event_loc_item_by_room.setdefault(ending.room_id, {})[event_name] = item_name 397 if ending.name != "WHITE":
398 event_name = f"{ending.name.capitalize()} Ending (Achieved)"
399 self.event_loc_item_by_room.setdefault(ending.room_id, {})[event_name] = "Ending"
332 400
333 if self.world.options.keyholder_sanity: 401 if self.world.options.keyholder_sanity:
334 for keyholder in world.static_logic.objects.keyholders: 402 for keyholder in world.static_logic.objects.keyholders:
335 if keyholder.HasField("key"): 403 if keyholder.HasField("key"):
404 if world.static_logic.get_room_object_map_id(keyholder) not in self.shuffled_maps:
405 continue
406
336 reqs = AccessRequirements() 407 reqs = AccessRequirements()
337 408
338 if self.get_letter_behavior(keyholder.key, False) != LetterBehavior.UNLOCKED: 409 if self.get_letter_behavior(keyholder.key, False) != LetterBehavior.UNLOCKED:
@@ -440,7 +511,6 @@ class Lingo2PlayerLogic:
440 reqs.possibilities.append(panel_reqs) 511 reqs.possibilities.append(panel_reqs)
441 512
442 if door.HasField("control_center_color"): 513 if door.HasField("control_center_color"):
443 # TODO: Logic for ensuring two CC states aren't needed at once.
444 reqs.rooms.add("Control Center - Main Area") 514 reqs.rooms.add("Control Center - Main Area")
445 self.add_solution_reqs(reqs, door.control_center_color) 515 self.add_solution_reqs(reqs, door.control_center_color)
446 516
@@ -466,13 +536,12 @@ class Lingo2PlayerLogic:
466 for room in door.rooms: 536 for room in door.rooms:
467 reqs.rooms.add(self.world.static_logic.get_room_region_name(room)) 537 reqs.rooms.add(self.world.static_logic.get_room_region_name(room))
468 538
469 for ending_id in door.endings: 539 if door.white_ending:
470 ending = self.world.static_logic.objects.endings[ending_id] 540 if self.world.options.endings_requirement > 0:
541 reqs.progressives["Ending"] = self.world.options.endings_requirement.value
471 542
472 if self.world.options.victory_condition.current_key.removesuffix("_ending").upper() == ending.name: 543 if self.world.options.masteries_requirement > 0:
473 reqs.items.add("Victory") 544 reqs.progressives["Mastery"] = self.world.options.masteries_requirement.value
474 else:
475 reqs.items.add(f"{ending.name.capitalize()} Ending (Achieved)")
476 545
477 for sub_door_id in door.doors: 546 for sub_door_id in door.doors:
478 sub_reqs = self.get_door_open_reqs(sub_door_id) 547 sub_reqs = self.get_door_open_reqs(sub_door_id)
@@ -534,3 +603,6 @@ class Lingo2PlayerLogic:
534 603
535 if needed > 0: 604 if needed > 0:
536 reqs.letters[l] = max(reqs.letters.get(l, 0), needed) 605 reqs.letters[l] = max(reqs.letters.get(l, 0), needed)
606
607 if any(l.isnumeric() for l in solution):
608 reqs.items.add("Numbers")
diff --git a/apworld/regions.py b/apworld/regions.py index 3735858..1118603 100644 --- a/apworld/regions.py +++ b/apworld/regions.py
@@ -62,6 +62,9 @@ def create_regions(world: "Lingo2World"):
62 # locations. This allows us to reference the actual region objects in the access rules for the locations, which is 62 # locations. This allows us to reference the actual region objects in the access rules for the locations, which is
63 # faster than having to look them up during access checking. 63 # faster than having to look them up during access checking.
64 for room in world.static_logic.objects.rooms: 64 for room in world.static_logic.objects.rooms:
65 if room.map_id not in world.player_logic.shuffled_maps:
66 continue
67
65 region = create_region(room, world) 68 region = create_region(room, world)
66 regions[region.name] = region 69 regions[region.name] = region
67 region_and_room.append((region, room)) 70 region_and_room.append((region, room))
@@ -97,7 +100,7 @@ def create_regions(world: "Lingo2World"):
97 100
98 if connection.HasField("port"): 101 if connection.HasField("port"):
99 port = world.static_logic.objects.ports[connection.port] 102 port = world.static_logic.objects.ports[connection.port]
100 connection_name = f"{connection_name} (via port {port.name})" 103 connection_name = f"{connection_name} (via {port.display_name})"
101 104
102 if world.options.shuffle_worldports and not port.no_shuffle: 105 if world.options.shuffle_worldports and not port.no_shuffle:
103 continue 106 continue
@@ -132,6 +135,12 @@ def create_regions(world: "Lingo2World"):
132 reqs.simplify() 135 reqs.simplify()
133 reqs.remove_room(from_region) 136 reqs.remove_room(from_region)
134 137
138 if to_region in reqs.rooms:
139 # This connection can't ever increase access because you're required to have access to the other side in
140 # order for it to be usable. We will just not create the connection at all, in order to help GER figure out
141 # what regions are dead ends.
142 continue
143
135 connection = Entrance(world.player, connection_name, regions[from_region]) 144 connection = Entrance(world.player, connection_name, regions[from_region])
136 connection.access_rule = make_location_lambda(reqs, world, regions) 145 connection.access_rule = make_location_lambda(reqs, world, regions)
137 146
@@ -150,14 +159,46 @@ def shuffle_entrances(world: "Lingo2World"):
150 159
151 port_id_by_name: dict[str, int] = {} 160 port_id_by_name: dict[str, int] = {}
152 161
153 for port in world.static_logic.objects.ports: 162 shuffleable_ports = [port for port in world.static_logic.objects.ports
154 if port.no_shuffle: 163 if not port.no_shuffle
155 continue 164 and world.static_logic.get_room_object_map_id(port) in world.player_logic.shuffled_maps]
165
166 if len(shuffleable_ports) % 2 == 1:
167 # We have an odd number of shuffleable ports! Pick a port from a room that has more than one, and make it a
168 # redundant warp to another port.
169 redundant_rooms = set(room.id for room in world.static_logic.objects.rooms if len(room.ports) > 1)
170 redundant_ports = [port for port in shuffleable_ports if port.room_id in redundant_rooms]
171 chosen_port = world.random.choice(redundant_ports)
172
173 shuffleable_ports.remove(chosen_port)
174
175 chosen_destination = world.random.choice(shuffleable_ports)
176
177 world.port_pairings[chosen_port.id] = chosen_destination.id
178
179 from_region_name = world.static_logic.get_room_region_name(chosen_port.room_id)
180 to_region_name = world.static_logic.get_room_region_name(chosen_destination.room_id)
181
182 from_region = world.multiworld.get_region(from_region_name, world.player)
183 to_region = world.multiworld.get_region(to_region_name, world.player)
184
185 connection = Entrance(world.player, f"{from_region_name} - {chosen_port.display_name}", from_region)
186 from_region.exits.append(connection)
187 connection.connect(to_region)
188
189 if chosen_port.HasField("required_door"):
190 door_reqs = world.player_logic.get_door_open_reqs(chosen_port.required_door)
191 connection.access_rule = make_location_lambda(door_reqs, world, None)
192
193 for region in door_reqs.get_referenced_rooms():
194 world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player),
195 connection)
156 196
197 for port in shuffleable_ports:
157 port_region_name = world.static_logic.get_room_region_name(port.room_id) 198 port_region_name = world.static_logic.get_room_region_name(port.room_id)
158 port_region = world.multiworld.get_region(port_region_name, world.player) 199 port_region = world.multiworld.get_region(port_region_name, world.player)
159 200
160 connection_name = f"{port_region_name} - {port.name}" 201 connection_name = f"{port_region_name} - {port.display_name}"
161 port_id_by_name[connection_name] = port.id 202 port_id_by_name[connection_name] = port.id
162 203
163 entrance = port_region.create_er_target(connection_name) 204 entrance = port_region.create_er_target(connection_name)
@@ -195,7 +236,7 @@ def connect_ports_from_ut(port_pairings: dict[int, int], world: "Lingo2World"):
195 from_region = world.multiworld.get_region(from_region_name, world.player) 236 from_region = world.multiworld.get_region(from_region_name, world.player)
196 to_region = world.multiworld.get_region(to_region_name, world.player) 237 to_region = world.multiworld.get_region(to_region_name, world.player)
197 238
198 connection = Entrance(world.player, f"{from_region_name} - {from_port.name}", from_region) 239 connection = Entrance(world.player, f"{from_region_name} - {from_port.display_name}", from_region)
199 240
200 reqs = AccessRequirements() 241 reqs = AccessRequirements()
201 if from_port.HasField("required_door"): 242 if from_port.HasField("required_door"):
diff --git a/apworld/requirements.txt b/apworld/requirements.txt index dbc395b..b0c79cc 100644 --- a/apworld/requirements.txt +++ b/apworld/requirements.txt
@@ -1 +1 @@
protobuf==3.20.3 protobuf
diff --git a/apworld/static_logic.py b/apworld/static_logic.py index e59a47d..715178e 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py
@@ -15,6 +15,9 @@ class Lingo2StaticLogic:
15 15
16 letter_weights: dict[str, int] 16 letter_weights: dict[str, int]
17 17
18 door_id_by_ap_id: dict[int, int]
19 port_id_by_ap_id: dict[int, int]
20
18 def __init__(self): 21 def __init__(self):
19 self.item_id_to_name = {} 22 self.item_id_to_name = {}
20 self.location_id_to_name = {} 23 self.location_id_to_name = {}
@@ -68,6 +71,7 @@ class Lingo2StaticLogic:
68 self.location_name_groups.setdefault("Keyholders", []).append(location_name) 71 self.location_name_groups.setdefault("Keyholders", []).append(location_name)
69 72
70 self.item_id_to_name[self.objects.special_ids["A Job Well Done"]] = "A Job Well Done" 73 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"
71 75
72 for symbol_name in SYMBOL_ITEMS.values(): 76 for symbol_name in SYMBOL_ITEMS.values():
73 self.item_id_to_name[self.objects.special_ids[symbol_name]] = symbol_name 77 self.item_id_to_name[self.objects.special_ids[symbol_name]] = symbol_name
@@ -80,7 +84,11 @@ class Lingo2StaticLogic:
80 84
81 for panel in self.objects.panels: 85 for panel in self.objects.panels:
82 for letter in panel.answer.upper(): 86 for letter in panel.answer.upper():
83 self.letter_weights[letter] = self.letter_weights.get(letter, 0) + 1 87 if letter.isalpha():
88 self.letter_weights[letter] = self.letter_weights.get(letter, 0) + 1
89
90 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")}
84 92
85 def get_door_item_name(self, door: data_pb2.Door) -> str: 93 def get_door_item_name(self, door: data_pb2.Door) -> str:
86 return f"{self.get_map_object_map_name(door)} - {door.name}" 94 return f"{self.get_map_object_map_name(door)} - {door.name}"
@@ -105,7 +113,7 @@ class Lingo2StaticLogic:
105 if door.type != data_pb2.DoorType.STANDARD: 113 if door.type != data_pb2.DoorType.STANDARD:
106 return None 114 return None
107 115
108 if len(door.keyholders) > 0 or len(door.endings) > 0 or door.HasField("complete_at"): 116 if len(door.keyholders) > 0 or door.white_ending or door.HasField("complete_at"):
109 return None 117 return None
110 118
111 if len(door.panels) > 4: 119 if len(door.panels) > 4:
@@ -166,6 +174,9 @@ class Lingo2StaticLogic:
166 else: 174 else:
167 return game_map.display_name 175 return game_map.display_name
168 176
177 def get_room_object_map_id(self, obj) -> int:
178 return self.objects.rooms[obj.room_id].map_id
179
169 def get_data_version(self) -> list[int]: 180 def get_data_version(self) -> list[int]:
170 version = self.objects.version 181 version = self.objects.version
171 return [version.major, version.minor, version.patch] 182 return [version.major, version.minor, version.patch]
diff --git a/apworld/tracker.py b/apworld/tracker.py index 7239b65..a84c3f8 100644 --- a/apworld/tracker.py +++ b/apworld/tracker.py
@@ -1,6 +1,6 @@
1from typing import TYPE_CHECKING 1from typing import TYPE_CHECKING, Iterator
2 2
3from BaseClasses import MultiWorld, CollectionState, ItemClassification 3from BaseClasses import MultiWorld, CollectionState, ItemClassification, Region, Entrance
4from NetUtils import NetworkItem 4from NetUtils import NetworkItem
5from . import Lingo2World, Lingo2Item 5from . import Lingo2World, Lingo2Item
6from .regions import connect_ports_from_ut 6from .regions import connect_ports_from_ut
@@ -47,7 +47,10 @@ class Tracker:
47 self.world.create_regions() 47 self.world.create_regions()
48 48
49 if self.world.options.shuffle_worldports: 49 if self.world.options.shuffle_worldports:
50 port_pairings = {int(fp): int(tp) for fp, tp in slot_data["port_pairings"].items()} 50 port_pairings = {
51 self.world.static_logic.port_id_by_ap_id[int(fp)]: self.world.static_logic.port_id_by_ap_id[int(tp)]
52 for fp, tp in slot_data["port_pairings"].items()
53 }
51 connect_ports_from_ut(port_pairings, self.world) 54 connect_ports_from_ut(port_pairings, self.world)
52 55
53 self.refresh_state() 56 self.refresh_state()
@@ -93,6 +96,7 @@ class Tracker:
93 PLAYER_NUM), prevent_sweep=True) 96 PLAYER_NUM), prevent_sweep=True)
94 97
95 self.state.sweep_for_advancements() 98 self.state.sweep_for_advancements()
99 self.state.update_reachable_regions(PLAYER_NUM)
96 100
97 self.accessible_locations = set() 101 self.accessible_locations = set()
98 self.accessible_worldports = set() 102 self.accessible_worldports = set()
@@ -110,3 +114,34 @@ class Tracker:
110 elif hasattr(location, "goal") and location.goal: 114 elif hasattr(location, "goal") and location.goal:
111 if not self.manager.goaled: 115 if not self.manager.goaled:
112 self.goal_accessible = True 116 self.goal_accessible = True
117
118 def get_path_to_location(self, location_id: int) -> list[str] | None:
119 location_name = self.world.location_id_to_name.get(location_id)
120 location = self.multiworld.get_location(location_name, PLAYER_NUM)
121 return self.get_logical_path(location.parent_region)
122
123 def get_path_to_port(self, port_id: int) -> list[str] | None:
124 port = self.world.static_logic.objects.ports[port_id]
125 region_name = self.world.static_logic.get_room_region_name(port.room_id)
126 region = self.multiworld.get_region(region_name, PLAYER_NUM)
127 return self.get_logical_path(region)
128
129 def get_path_to_goal(self):
130 room_id = self.world.player_logic.goal_room_id
131 region_name = self.world.static_logic.get_room_region_name(room_id)
132 region = self.multiworld.get_region(region_name, PLAYER_NUM)
133 return self.get_logical_path(region)
134
135 def get_logical_path(self, region: Region) -> list[str] | None:
136 if region not in self.state.path:
137 return None
138
139 def flist_to_iter(path_value) -> Iterator[str]:
140 while path_value:
141 region_or_entrance, path_value = path_value
142 yield region_or_entrance
143
144 reversed_path = self.state.path.get(region)
145 flat_path = reversed(list(map(str, flist_to_iter(reversed_path))))
146
147 return list(flat_path)[1::2]